Merge branch 'ps/odb-pluggable-housekeeping'

Object database housekeeping in 'git gc' and 'git maintenance' has
been refactored to be pluggable.  The files-backend-specific logic,
including incremental and geometric repacking as well as object
pruning, has been moved out of the command implementation and into the
files object database source, enabling future alternative object
database backends to implement their own housekeeping services.

* ps/odb-pluggable-housekeeping:
  odb: make optimizations pluggable
  builtin/gc: fix signedness issues in ODB-related functionality
  builtin/gc: refactor ODB optimizations to operate on "files" source
  builtin/gc: introduce `odb_optimize_required()`
  builtin/gc: move geometric repacking into `odb_optimize()`
  builtin/gc: introduce object database optimization options
  builtin/gc: inline config values specific to the "files" backend
  builtin/gc: make repack arguments self-contained
  builtin/gc: extract object database optimizations into separate function
  builtin/gc: move worktree and rerere tasks before object optimizations
  odb: run "pre-auto-gc" hook for all maintenance tasks
  t7900: simplify how we check for maintenance tasks
diff --git a/.b4-cover-template b/.b4-cover-template
index ab86493..8168d8a 100644
--- a/.b4-cover-template
+++ b/.b4-cover-template
@@ -8,4 +8,5 @@
 ${range_diff}
 ---
 base-commit: ${base_commit}
+change-id: ${change_id}
 ${prerequisites}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index cf341d7..85cfedf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -420,7 +420,9 @@
       CI_JOB_IMAGE: ${{matrix.vector.image}}
       CUSTOM_PATH: /custom
     runs-on: ubuntu-latest
-    container: ${{matrix.vector.image}}
+    container:
+      image: ${{ matrix.vector.image }}
+      options: ${{ github.repository_visibility == 'private' && '--pids-limit 16384 --ulimit nproc=16384:16384 --ulimit nofile=32768:32768' || '' }}
     steps:
     - name: prepare libc6 for actions
       if: matrix.vector.jobname == 'linux32'
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1a8e909..1c4d04d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -88,13 +88,8 @@
   tags:
     - saas-macos-large-m2pro
   variables:
-    TEST_OUTPUT_DIRECTORY: "/Volumes/RAMDisk"
+    TEST_OUTPUT_DIRECTORY: "/tmp/test-output"
   before_script:
-    # Create a 4GB RAM disk that we use to store test output on. This small hack
-    # significantly speeds up tests by more than a factor of 2 because the
-    # macOS runners use network-attached storage as disks, which is _really_
-    # slow with the many small writes that our tests do.
-    - sudo diskutil apfs create $(hdiutil attach -nomount ram://8192000) RAMDisk
     - ./ci/install-dependencies.sh
   script:
     - ./ci/run-build-and-tests.sh
@@ -152,6 +147,9 @@
   needs:
     - job: "build:mingw64"
       artifacts: true
+  variables:
+    # Windows runners don't have enough RAM to run EXPENSIVE tests.
+    GIT_TEST_LONG: false
   before_script:
     - *windows_before_script
     - git-sdk/usr/bin/bash.exe -l -c 'tar xf artifacts/artifacts.tar.gz'
@@ -200,6 +198,9 @@
   script:
     - |
       & "C:/Program Files/Git/usr/bin/bash.exe" -l -c 'ci/run-test-slice-meson.sh build $CI_NODE_INDEX $CI_NODE_TOTAL'
+  variables:
+    # Windows runners don't have enough RAM to run EXPENSIVE tests.
+    GIT_TEST_LONG: false
   after_script:
     - |
       if ($env:CI_JOB_STATUS -ne "success") {
diff --git a/.mailmap b/.mailmap
index c2e3939..48c3479 100644
--- a/.mailmap
+++ b/.mailmap
@@ -39,12 +39,13 @@
 Chris Wright <chrisw@sous-sol.org> <chrisw@osdl.org>
 Christian Ludwig <chrissicool@gmail.com> <chrissicool@googlemail.com>
 Cord Seele <cowose@gmail.com> <cowose@googlemail.com>
-Christian Couder <chriscool@tuxfamily.org> <christian.couder@gmail.com>
+Christian Couder <christian.couder@gmail.com> <chriscool@tuxfamily.org>
 Christian Stimming <stimming@tuhh.de> <chs@ckiste.goetheallee>
 Christopher Díaz Riveros <chrisadr@gentoo.org> Christopher Diaz Riveros
 Clemens Buchacher <drizzd@gmx.net> <drizzd@aon.at>
 Clemens Buchacher <drizzd@gmx.net> <clemens.buchacher@intel.com>
 Csaba Henk <csaba@gluster.com> <csaba@lowlife.hu>
+D. Ben Knoble <ben.knoble@gmail.com> <ben.knoble+github@gmail.com>
 Dan Johnson <computerdruid@gmail.com>
 Dana L. How <danahow@gmail.com> <how@deathvalley.cswitch.com>
 Dana L. How <danahow@gmail.com> Dana How
@@ -277,6 +278,7 @@
 SZEDER Gábor <szeder.dev@gmail.com> <szeder@ira.uka.de>
 Tao Qingyun <taoqy@ls-a.me> <845767657@qq.com>
 Tay Ray Chuan <rctay89@gmail.com>
+Taylor Blau <me@ttaylorr.com> <ttaylorr@openai.com>
 Ted Percival <ted@midg3t.net> <ted.percival@quest.com>
 Theodore Ts'o <tytso@mit.edu>
 Thomas Ackermann <th.acker@arcor.de> <th.acker66@arcor.de>
diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc
index 4832e5b..34bfa2e 100644
--- a/Documentation/MyFirstContribution.adoc
+++ b/Documentation/MyFirstContribution.adoc
@@ -1477,6 +1477,11 @@
 if you aren't sure why a change was suggested, or what the reviewer is asking
 you to do.
 
+When replying to review comments, quote only the parts of the message that are
+relevant to your response. It is usually helpful to trim away unrelated context,
+such as large portions of the patch that are not being discussed, while keeping
+enough quoted text for readers to understand what you are responding to.
+
 Make sure your email client has a plaintext email mode and it is turned on; the
 Git list rejects HTML email. Please also follow the mailing list etiquette
 outlined in the
diff --git a/Documentation/RelNotes/2.55.0.adoc b/Documentation/RelNotes/2.55.0.adoc
index f564353..e7e77a8 100644
--- a/Documentation/RelNotes/2.55.0.adoc
+++ b/Documentation/RelNotes/2.55.0.adoc
@@ -85,8 +85,8 @@
  * Promisor remote handling has been refactored and fixed in
    preparation for auto-configuration of advertised remotes.
 
- * Rust support is enabled by default (but still allows opting out) in
-   some future version of Git.
+ * Rust support is enabled by default (but still allows opting out);
+   in Git version 3.0, Rust will become mandatory.
 
  * Preparation of the xdiff/ codebase to work with Rust.
 
diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc
index fda10a0..a57ca54 100644
--- a/Documentation/RelNotes/2.56.0.adoc
+++ b/Documentation/RelNotes/2.56.0.adoc
@@ -27,6 +27,55 @@
    absolute and relative paths for "gitdir" and "commondir", supported by
    a new path-formatting helper extracted from "git rev-parse".
 
+ * When 'git push origin/main' or 'git branch origin main' is run, the
+   command is now recognized as a potential typo, and advice has been
+   added to offer a typo fix.
+
+ * The 'git refs' toolbox has been extended with new 'create', 'delete',
+   'update', and 'rename' subcommands to create, delete, update, and
+   rename references, respectively.
+
+ * The experimental 'git history' command has been taught a new 'drop'
+   subcommand to remove a commit, with its descendants replayed onto its
+   parent.
+
+ * The alignment of commit object name abbreviations in 'git blame'
+   output has been optimized to reserve a column for marks (caret,
+   question mark, or asterisk) only when such marks are actually shown.
+
+ * Option parsing with 'git rev-parse --parseopt' and in most 'git'
+   subcommands has been updated to exit with 0 (instead of 129) when the
+   help option ('-h' or '--help') is requested directly by the user,
+   aligning with standard Unix convention.
+
+ * The '[includeIf "condition"]' conditional inclusion facility for
+   configuration files has been taught to use the location of the
+   worktree in its condition.
+
+ * The usage string and SYNOPSIS for 'git fast-export' have been
+   standardized to make them consistent with each other and with other
+   commands.
+
+ * 'git log --graph' has been modified to visually distinguish parentless
+   'root' commits (and commits that become roots due to history
+   simplification) by indenting them, preventing them from appearing
+   falsely related to unrelated commits rendered immediately above them.
+
+ * Userdiff patterns for Swift have been added, with support for
+   Swift-specific constructs such as attributes, modifiers, failable
+   initializers, and generics.
+
+ * Configuration file locking has been updated to retry for a short
+   period, avoiding failures when multiple processes attempt to update
+   the configuration simultaneously.
+
+ * The 'remote-object-info' command has been added to 'git cat-file
+   --batch-command', allowing clients to request object metadata
+   (currently size) from a remote server via protocol v2 without
+   downloading the entire object.  Format placeholders are dynamically
+   filtered on the client based on server-advertised capabilities,
+   returning empty strings for inapplicable or unsupported fields.
+
 
 Performance, Internal Implementation, Development Support etc.
 --------------------------------------------------------------
@@ -74,6 +123,163 @@
    a default limit of at most one reroll per day to give reviewers across
    different time zones enough time to participate.
 
+ * The lazy priority queue optimization pattern (deferring actual removal
+   in 'prio_queue_get()' to allow get+put fusion) has been folded
+   directly into 'prio_queue' itself, speeding up commit traversal
+   workflows and simplifying callers.
+
+ * The 'reprepare()' callback for object database sources has been
+   generalized into a 'prepare()' callback with an optional flush cache
+   flag, and a new 'odb_prepare()' wrapper has been introduced to allow
+   pre-opening object database sources.
+
+ * The 'whence' field in 'struct object_info' has been removed.  The
+   backend-specific object information retrieval has been refactored into
+   an opt-in 'struct object_info_source' structure.
+
+ * A racy build failure under Meson has been corrected by ensuring that
+   the generated header file 'hook-list.h' is built before compiling
+   files in 'builtin_sources' that depend on it.
+
+ * The repository discovery and repository configuration phases, which
+   were previously intertwined in 'setup.c', have been split.  Repository
+   discovery has been updated to populate a 'struct repo_discovery'
+   without modifying the repository state, which is then taken by
+   repository configuration to initialize the repository, paving the way
+   for clean unification of repository configuration.
+
+ * The 'SubmittingPatches' document has been updated to explicitly
+   describe the expectation for contributors to retract or abandon their
+   patch series when they are no longer pursuing it.
+
+ * The contributor guide has been updated to advise new contributors to
+   trim irrelevant quoted text when replying to review comments, matching
+   the existing advice given to reviewers.
+
+ * The build system has been updated to support building universal macOS
+   binaries when 'Rust' is enabled, by compiling separate static archives
+   for each target triple listed in 'RUST_TARGETS' and combining them
+   using the macOS 'lipo' tool.  The 'git-credential-osxkeychain' helper
+   has been updated to link against '$(RUST_LIB)' when 'Rust' is enabled.
+
+ * The test suite has been updated to use the 'test_grep' helper instead
+   of bare 'grep' for test assertions, allowing file contents to be
+   printed on failure for easier debugging.  A new 'greplint' linter has
+   been introduced to detect and prevent new bare 'grep' assertions from
+   being added to the test suite.
+
+ * The pipelines in 't1410-reflog.sh' have been replaced with the
+   'test_stdout_line_count' helper to avoid suppressing the exit code of
+   'git' commands, ensuring failures are not hidden from the test suite.
+
+ * The cache-scanning loop in 'next_cache_entry()' has been optimized
+   to avoid rescanning already-unpacked index entries, preventing a
+   quadratic performance slow-down when diffing the working tree
+   against a commit with a pathspec matching early index entries.
+
+ * The global configuration variable 'ignore_case' (representing the
+   'core.ignorecase' configuration) has been migrated into 'struct
+   repo_config_values' to tie it to a specific repository instance.
+
+ * The performance of ref updates and reads using the 'reftable' backend
+   in the presence of many deletion tombstone records has been optimized
+   by removing the tombstone suppression flag from the merged iterator
+   and instead skipping tombstones at higher-level call sites where
+   iteration bounds are known.
+
+ * Various code paths have been hardened against potential NULL-pointer
+   dereferences and invalid file descriptor accesses flagged by
+   Coverity.
+
+ * The in-tree 'b4' cover letter template has been updated to include the
+   'change-id' trailer, ensuring that sent tags generated by 'b4' contain
+   the required tracking information for subsequent runs.
+
+ * 'git receive-pack' has been refactored to use ODB transaction
+   interfaces instead of directly managing 'tmp_objdir' for staging
+   incoming objects, bringing it closer to being ODB backend agnostic.
+
+ * The test script 't/t9811-git-p4-label-import.sh' has been
+   modernized to use 'test_path_is_file' and 'test_path_is_missing'
+   instead of raw 'test -f' and '! test -f' calls.
+
+ * A redundant strbuf_reset() call in the 'HAVE_GETDELIM' path of
+   strbuf_getwholeline() has been removed, as getdelim() overwrites the
+   buffer and the length is updated afterward.
+
+ * The object database enumeration interface odb_for_each_object() has
+   been taught to accept object filters, allowing the underlying backends
+   to optimize the traversal by using reachability bitmaps when
+   available.  'git cat-file --batch-all-objects' has been updated to use
+   this generic interface, simplifying its code and avoiding direct
+   access to ODB backend internals.
+
+ * The test script 't/t1100-commit-tree-options.sh' has been modernized
+   by converting test cases to the modern style (using single quotes and
+   tab indentation) and moving the creation of the expected file inside
+   the setup test so it runs under the protection of the test harness.
+
+ * The test script 't/t7614-merge-signoff.sh' has been updated to avoid
+   suppressing the exit code of 'git' commands in a pipe.
+
+ * The 'git rev-list --no-walk' command has been corrected to restore
+   pathspec filtering, which was lost when the streaming walk was
+   refactored.
+
+ * The ref subsystem and the worktree API have been refactored to pass a
+   repository pointer down the call chain, allowing them to drop
+   references to the global 'the_repository' variable.  As part of this,
+   the handling of the 'core.packedRefsTimeout' configuration has been
+   moved into the per-repository ref store structure.
+
+ * 'git branch --contains' and 'git for-each-ref --contains' have been
+   optimized to use the memoized commit traversal previously used only by
+   'git tag --contains', significantly speeding up connectivity checks
+   across many candidate refs with shared history.
+
+ * The passing of push destination specifications in the 'remote-curl'
+   helper has been simplified by removing the explicit 'count' parameter
+   and relying on the NULL-termination of the array.
+
+ * The dependency on the global 'the_repository' variable in the
+   'refspec.c' API has been removed by passing the hash algorithm
+   explicitly to refspec-parsing functions and storing it in 'struct
+   refspec'.
+
+ * The enumeration of untracked and ignored files in 'git status' has
+   been optimized by avoiding quadratic complexity when inserting into
+   string lists, reducing the construction cost from O(n^2) to O(n log
+   n).
+
+ * The copy_file() and copy_file_with_time() functions have been
+   refactored to take a repository parameter, allowing the removal of the
+   implicit dependency on the global 'the_repository' variable in
+   'copy.c'.
+
+ * The tempfile and lockfile APIs have been refactored to stop depending
+   on the 'the_repository' global variable, and their callers have been
+   updated to use the repository-aware variants.
+
+ * The 'trust_executable_bit' (coming from the 'core.filemode'
+   configuration) has been migrated into 'struct repo_config_values' to
+   tie it to a specific repository instance.
+
+ * The 'excludes_file' and various other global configuration variables
+   (including 'editor_program', 'pager_program', 'askpass_program', and
+   'push_default') have been migrated into the per-repository structure.
+
+ * The 'git stash push' command has been optimized to avoid unnecessary
+   sparse index expansion when pathspecs are wholly inside the
+   sparse-checkout cone.  Also, a potential out-of-bounds read in the
+   sparse-index expansion check helper pathspec_needs_expanded_index()
+   has been fixed by consistently using the parsed, prefixed path.
+
+ * The logic to write loose objects has been refactored and moved from
+   'object-file.c' to the loose backend source file 'odb/source-loose.c',
+   making the loose backend more self-contained.  This is achieved by
+   first refactoring force_object_loose() to use generic ODB write
+   interfaces instead of loose-backend internals.
+
 
 Fixes since v2.55
 -----------------
@@ -111,3 +317,152 @@
    test_path_is_missing instead of ! grep on a file that shouldn't
    exist in the conflicted state.
    (merge eaad121fef sg/t3420-do-not-grep-in-missing-file later to maint).
+
+ * The GPG and SSH signature parsing code has been corrected to strip
+   carriage return characters only when they immediately precede line
+   feeds, instead of unconditionally stripping all carriage returns.
+   (merge 5dea8b690b ad/gpg-strip-cr-before-lf later to maint).
+
+ * A memory leak in the 'reftable_writer_new()' initialization function
+   has been fixed by delaying the allocation of 'struct reftable_writer'
+   until after input options are validated.
+   (merge c6fb3b9c3e jk/reftable-leakfix later to maint).
+
+ * A memory leak in the '--base' handling of 'git format-patch' has been
+   plugged, and the leak reporting of the test suite when running under a
+   TAP harness has been improved.
+   (merge 973a0373ff jk/format-patch-leakfix later to maint).
+
+ * A write file stream resource leak has been fixed as part of a code
+   cleanup.
+   (merge ebb4d2ffa3 jc/history-message-prep-fix later to maint).
+
+ * Various memory leaks in the Bloom-filter code paths that are exposed
+   when running tests with the 'GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1'
+   environment variable have been plugged.
+   (merge 459088ec2e jk/bloom-leak-fixes later to maint).
+
+ * The wincred credential helper has been updated to avoid memory
+   corruption when erasing credentials and to prevent silent
+   credential loss when storing OAuth tokens, by correcting buffer
+   allocations and arguments passed to safe-CRT APIs.
+   (merge f635ab9ab4 js/wincred-fixes later to maint).
+
+ * Various code paths that initialize a cryptographic hash context but
+   bail out or finish without calling 'git_hash_final()' have been taught
+   to call 'git_hash_discard()' to release allocated resources, fixing
+   memory leaks when Git is built with non-default backends like
+   'OpenSSL' or 'libgcrypt'.
+   (merge 600588d2aa jk/hash-algo-leak-fixes later to maint).
+
+ * Various resource leaks, invalid file descriptor closures, and process
+   handle ownership issues flagged by Coverity have been fixed.
+   (merge 9184231173 js/coverity-fixes later to maint).
+
+ * Dockerized CI jobs running in private GitHub repositories have been
+   adjusted to use explicit process and file limits, preventing resource
+   exhaustion errors on private runners.
+   (merge bad766fbac js/ci-dockerized-pid-limit later to maint).
+
+ * Various test scripts have been updated to clean up large temporary
+   files and repositories, reducing peak disk usage during testing.
+   Also, expensive tests have been disabled on platforms that lack
+   sufficient resources (like 32-bit platforms and Windows CI runners),
+   and the long test suite has been enabled in GitLab CI.
+   (merge 84248444ad ps/t-fixes-for-git-test-long later to maint).
+
+ * The UTF-8 precomposition wrapper on macOS has been updated to use a
+   flexible array member to represent the name of a directory entry,
+   preventing fortified libc checks from failing when the name is
+   reallocated to be larger than 'NAME_MAX' bytes.
+   (merge 1eb281159f ih/precompose-flex-array later to maint).
+
+ * The 'git_hash_*()' wrappers have been updated to be used consistently
+   across the codebase instead of direct calls to members of 'struct
+   git_hash_algo', and 'git_hash_discard()' has been made idempotent to
+   simplify cleanups.
+   (merge 9e396aa553 jk/git-hash-cleanups later to maint).
+
+ * The sideband demultiplexer has been updated to recognize ANSI SGR
+   escape sequences that use colon-separated subfields (e.g., for
+   256-color or true-color codes).
+   (merge 3792b2aea4 mm/sideband-ansi-sgr-colon-fix later to maint).
+
+ * The 'reftable' code has been hardened against corrupted tables by
+   fixing out-of-bounds writes, out-of-bounds reads, and abort calls
+   during parsing.
+   (merge ca93c27328 ps/reftable-hardening later to maint).
+
+ * A description in the release notes for Git 2.55.0 has been
+   retroactively updated to clarify that Rust support is enabled by
+   default, but still optional, and will become mandatory in Git 3.0.
+   (merge 18b2009d14 jc/relnotes-2.55-rust-fix later to maint).
+
+ * The early-exit optimization in 'paint_down_to_common()' has been
+   gated on the queue being generation-ordered, fixing a bug where
+   'git merge-base' (without '--all') could return incorrect results
+   on repositories with v1 commit graphs and clock skew.
+   (merge ae68032a8d kk/commit-reach-find-all-fix later to maint).
+
+ * The client-side parser of the server-advertised bundle-URI list has
+   been updated to drain the remaining response in order to avoid
+   protocol desynchronization when the server sends a misconfigured list.
+   Also, the server-side has been taught to omit empty configuration
+   values instead of sending invalid key-value lines.
+   (merge 50de1169e4 tc/bundle-uri-empty-fix later to maint).
+
+ * The 'topo_levels' slab was propagated only to the topmost layer of a
+   split commit-graph chain, causing topological levels for commits in
+   base layers to be recomputed during incremental writes.  This has been
+   corrected.
+
+ * The stream-based object signature verification path has been
+   corrected to avoid double-closing the stream on read errors.
+   (merge cfd52a74a0 ps/odb-stream-double-close-fix later to maint).
+
+ * The '-i' shorthand for the '--init' option, which was accepted by the
+   'git submodule update' command until it was broken in a modernization
+   of the option-parsing code, has been restored.
+   (merge ff1da37f58 dm/submodule-update-i-shorthand later to maint).
+
+ * An accidental use of the '%zu' format specifier in 'git
+   submodule--helper' has been corrected to use 'PRIuMAX' and cast the
+   value to 'uintmax_t' to avoid portability issues.
+   (merge 3279c13c00 jc/submodule-helper-avoid-zu later to maint).
+
+ * The rebase post-rewrite notes-copying logic has been corrected.  When
+   a commit is dropped during rebase (e.g., because its changes are
+   already upstream), it is no longer recorded as rewritten, preventing
+   its notes from being copied to an unrelated commit.
+   (merge 42554b78fd pw/rebase-drop-notes-with-commit later to maint).
+
+ * A few memory problems in the Rust interface to C hash functions have
+   been corrected.  The 'Clone' implementation of 'CryptoHasher' now
+   properly initializes the context before cloning, and its 'Drop'
+   implementation now discards the context to prevent leaks.
+
+ * The object ID shortening and linking in the 'commitdiff' view of
+   'gitweb' has been corrected to work even when the index line carries
+   a trailing file mode.
+   (merge fda513d6fe tl/gitweb-shorten-hashes-with-modes later to maint).
+
+ * When the push remote is specified as a URL, the fetch refspec of a
+   uniquely matching configured remote is now used to find and update
+   the remote-tracking branch (e.g., '@{push}').
+
+ * Traversals with '--exclude-first-parent-only' have been corrected
+   to properly stop after the first parent even when it has already
+   been marked as 'SEEN'.
+   (merge 47382f7398 jc/exclude-first-parent-seen later to maint).
+
+ * A segfault when 'git clone --revision' talks to a server that does not
+   support protocol v2 (falling back to protocol v0) has been corrected.
+   (merge 1034ad383f af/clone-revision-v0-segfault-fix later to maint).
+
+ * rewrites_release() in 'remote.c' has been updated to free 'struct
+   rewrite' instances, their '.instead_of' arrays, and their contents.
+   (merge dcef3bf041 jc/remote-insteadof-leakfix later to maint).
+
+ * The remote-matching logic for submodules has been corrected to resolve
+   'url.*.insteadOf' aliases before comparing the inventoried URL from
+   '.gitmodules' with the URLs of configured remotes.
diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
index d2d82eb..c269e47 100644
--- a/Documentation/SubmittingPatches
+++ b/Documentation/SubmittingPatches
@@ -123,6 +123,20 @@
   top, it gets merged to the 'master' branch and waits to become part
   of the next major release.
 
+But sometimes things do not work as planned:
+
+. If a mailing list discussion convinces you that your changes aren't
+  ideal, please explicitly retract the topic to save the maintainer
+  time and effort.
+
+. If you must drop a topic due to shifting priorities, lack of time,
+  or other commitments, notify the list as a courtesy so others can
+  take over. Anyone can resurrect the topic later when they have the
+  capacity to do so.
+
+. Topics with unaddressed review comments that remain inactive for
+  four weeks may be discarded by the maintainer.
+
 In the following sections, many techniques and conventions are listed
 to help your patches get reviewed effectively in such a life cycle.
 
diff --git a/Documentation/config.adoc b/Documentation/config.adoc
index 15b1a4d..1ef72de 100644
--- a/Documentation/config.adoc
+++ b/Documentation/config.adoc
@@ -146,6 +146,51 @@
 	This is the same as `gitdir` except that matching is done
 	case-insensitively (e.g. on case-insensitive file systems)
 
+`worktree`::
+	The data that follows the keyword `worktree` and a colon is used as a
+	glob pattern. If the working directory of the current worktree matches
+	the pattern, the include condition is met.
++
+The worktree location is the path where files are checked out (as returned
+by `git rev-parse --show-toplevel`). This is different from `gitdir`, which
+matches the `.git` directory path. In a linked worktree, the worktree path
+is the directory where that worktree's files are located, not the main
+repository's `.git` directory.
++
+The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`,
+`**/`, and trailing-`/` prefix matching). This condition will never match
+in a bare repository (which has no worktree).
++
+Unlike `gitdir`, the `worktree` condition currently matches only the
+realpath-resolved worktree location. If the working tree was entered via a
+symbolic link, a pattern that uses the symbolic-link spelling may not match;
+use the real path instead.
++
+This is useful when you want to apply configuration based on where the
+working tree is located on the filesystem. For example, a contributor who
+works on the same project both personally and as an employee can use
+different `user.name` and `user.email` values depending on which directory
+the worktree is checked out under:
++
+----
+[includeIf "worktree:/home/user/work/"]
+    path = ~/.config/git/work.inc
+[includeIf "worktree:/home/user/personal/"]
+    path = ~/.config/git/personal.inc
+----
++
+While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports
+per-worktree configuration, it stores the config inside each repository's
+`.git/config.worktree` file and requires running `git config --worktree`
+inside each worktree individually. In contrast, `includeIf "worktree:..."`
+can be set once in a global or system-level configuration file (e.g.
+`~/.config/git/config`) and applies to all repositories at once based on
+their worktree location.
+
+`worktree/i`::
+	This is the same as `worktree` except that matching is done
+	case-insensitively (e.g. on case-insensitive file systems)
+
 `onbranch`::
 	The data that follows the keyword `onbranch` and a colon is taken to be a
 	pattern with standard globbing wildcards and two additional
@@ -244,6 +289,14 @@
 [includeIf "gitdir:~/to/group/"]
 	path = /path/to/foo.inc
 
+; include if the worktree is at /path/to/project-build
+[includeIf "worktree:/path/to/project-build"]
+	path = build-config.inc
+
+; include for all worktrees inside /path/to/group
+[includeIf "worktree:/path/to/group/"]
+	path = group-config.inc
+
 ; relative paths are always relative to the including
 ; file (if the condition is true); their location is not
 ; affected by the condition
diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc
index c3c190b..81f80a9 100644
--- a/Documentation/config/advice.adoc
+++ b/Documentation/config/advice.adoc
@@ -94,6 +94,11 @@
 		Shown when linkgit:git-push[1] rejects a forced update of
 		a branch when its remote-tracking ref has updates that we
 		do not have locally.
+	pushRepoLooksLikeRef::
+		Shown when the repository given to linkgit:git-push[1] is not
+		a configured remote but looks like a `<remote>/<branch>` ref,
+		suggesting that the remote and branch be given as separate
+		arguments.
 	pushUnqualifiedRefname::
 		Shown when linkgit:git-push[1] gives up trying to
 		guess based on the source and destination refs what
diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc
index a4db9fa..5a85fde 100644
--- a/Documentation/config/branch.adoc
+++ b/Documentation/config/branch.adoc
@@ -55,6 +55,7 @@
 	repository), you would want to set `remote.pushDefault` to
 	specify the remote to push to for all branches, and use this
 	option to override it for a specific branch.
+	The value may be the name of a configured remote or a repository URL.
 
 `branch.<name>.merge`::
 	Defines, together with `branch.<name>.remote`, the upstream branch
diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc
index a0ebf03..340329e 100644
--- a/Documentation/config/core.adoc
+++ b/Documentation/config/core.adoc
@@ -589,6 +589,14 @@
 	all; -1 means to try indefinitely. Default is 1000 (i.e.,
 	retry for 1 second).
 
+core.configLockTimeout::
+	The length of time, in milliseconds, to retry when trying to
+	lock a configuration file for writing. Value 0 means not to
+	retry at all; -1 means to try indefinitely. Default is 1000
+	(i.e., retry for 1 second). This is read from the configuration
+	that is already on disk before the lock is taken, so it can be
+	set persistently like any other option.
+
 core.pager::
 	Text viewer for use by Git commands (e.g., 'less').  The value
 	is meant to be interpreted by the shell.  The order of preference
diff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc
index 757a7be..f7dfce6 100644
--- a/Documentation/config/log.adoc
+++ b/Documentation/config/log.adoc
@@ -59,6 +59,10 @@
 	A list of colors, separated by commas, that can be used to draw
 	history lines in `git log --graph`.
 
+`log.graphIndent`::
+	If `true`, indent visual roots when rendering the graphs with `--graph`.
+	Set true by default. It can be overriden with `--[no-]graph-indent`.
+
 `log.showRoot`::
 	If true, the initial commit will be shown as a big creation event.
 	This is equivalent to a diff against an empty tree.
diff --git a/Documentation/git-blame.adoc b/Documentation/git-blame.adoc
index 8808009..2b74e45 100644
--- a/Documentation/git-blame.adoc
+++ b/Documentation/git-blame.adoc
@@ -88,11 +88,12 @@
 include::diff-algorithm-option.adoc[]
 
 `--abbrev=<n>`::
-	Instead of using the default _7+1_ hexadecimal digits as the
-	abbreviated object name, use _<m>+1_ digits, where _<m>_ is at
-	least _<n>_ but ensures the commit object names are unique.
-	Note that 1 column
-	is used for a caret to mark the boundary commit.
+	Instead of using the default _7_ hexadecimal digits as the
+	abbreviated object name, use at least _<n>_ digits, but ensure
+	the commit object names are unique.
+	If commits marked with caret (boundary), question mark (ignored)
+	or asterisk (unblamable) are shown, extend unmarked object names
+	to align them.
 
 
 THE DEFAULT FORMAT
diff --git a/Documentation/git-cat-file.adoc b/Documentation/git-cat-file.adoc
index 86b9181..ac3b528 100644
--- a/Documentation/git-cat-file.adoc
+++ b/Documentation/git-cat-file.adoc
@@ -169,6 +169,13 @@
 	Print object info for object reference `<object>`. This corresponds to the
 	output of `--batch-check`.
 
+remote-object-info <remote> <object>...::
+	Print object info for object references `<object>` at specified
+	`<remote>` without downloading objects from the remote.
+	Raise an error when the `object-info` capability is not supported by the remote.
+	Raise an error when no object references are provided.
+	This command may be combined with `--buffer`.
+
 flush::
 	Used with `--buffer` to execute all preceding commands that were issued
 	since the beginning or since the last flush was issued. When `--buffer`
@@ -301,7 +308,8 @@
 `--batch-command`, the `info` command followed by an object will print
 information about the object the same way `--batch-check` would, and the
 `contents` command followed by an object prints contents in the same way
-`--batch` would.
+`--batch` would. The `remote-object-info` command followed by a remote and
+object IDs prints object info from the remote without downloading the objects.
 
 You can specify the information shown for each object by using a custom
 `<format>`. The `<format>` is copied literally to stdout for each
@@ -340,8 +348,15 @@
 	after that first run of whitespace (i.e., the "rest" of the
 	line) are output in place of the `%(rest)` atom.
 
+The command `remote-object-info` only supports the `%(objectname)` and
+`%(objectsize)` placeholders. See `CAVEATS` below for more information.
+
 If no format is specified, the default format is `%(objectname)
-%(objecttype) %(objectsize)`.
+%(objecttype) %(objectsize)`, except for `remote-object-info` commands which
+use `%(objectname) %(objectsize)` because `%(objecttype)` is not supported yet.
+
+WARNING: When "%(objecttype)" is supported, the default format WILL be unified,
+so DO NOT RELY on the current default format to stay the same!!!
 
 If `--batch` is specified, or if `--batch-command` is used with the `contents`
 command, the object information is followed by the object contents (consisting
@@ -438,6 +453,10 @@
 CAVEATS
 -------
 
+Note that only `%(objectname)` and `%(objectsize)` are currently
+supported by the `remote-object-info` command. Using any other placeholder in
+the format string will return an empty string in its position.
+
 Note that the sizes of objects on disk are reported accurately, but care
 should be taken in drawing conclusions about which refs or objects are
 responsible for disk usage. The size of a packed non-delta object may be
diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 297b57b..719aeca 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -9,7 +9,7 @@
 SYNOPSIS
 --------
 [verse]
-'git fast-export' [<options>] | 'git fast-import'
+'git fast-export' [<options>] [<revision-range>] [[--] <path>...]
 
 DESCRIPTION
 -----------
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 2ba8121..28b477c 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,6 +8,7 @@
 SYNOPSIS
 --------
 [synopsis]
+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]
 git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
 git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
 git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
@@ -51,13 +52,28 @@
 first-class conflicts.
 
 When using `fixup` with `--empty=drop`, dropping the root commit is not yet
-supported.
+supported. Likewise, `drop` cannot remove the root commit or a merge commit.
 
 COMMANDS
 --------
 
 The following commands are available to rewrite history in different ways:
 
+`drop <commit>`::
+	Remove the specified commit from the history. All descendants of the
+	commit are replayed directly onto its parent.
++
+The root commit cannot be dropped as that may lead to edge cases where refs
+end up with no commits anymore. Merge commits cannot be dropped either; see
+LIMITATIONS.
++
+If `HEAD` points at a commit that is to be rewritten, the index and working
+tree are updated to match the new `HEAD`. The command aborts before any
+references are updated in case local modifications would be overwritten.
++
+If replaying any descendant would result in a conflict, the command aborts
+with an error.
+
 `fixup <commit>`::
 	Apply the currently staged changes to the specified commit. This is
 	similar in nature to `git commit --fixup=<commit>` followed by `git
@@ -170,6 +186,26 @@
 commit. All descendant commits have been replayed on top of the rewritten
 history.
 
+Drop a commit
+~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+abc1234 (HEAD -> main) third
+def5678 second
+ghi9012 first
+
+$ git history drop 'main^{/second}'
+
+$ git log --oneline
+jkl3456 (HEAD -> main) third
+ghi9012 first
+----------
+
+The `second` commit has been removed from the history, and `third` has been
+replayed directly on top of `first`. All branches that pointed at the dropped
+commit have been moved to its parent.
+
 Split a commit
 ~~~~~~~~~~~~~~
 
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680..ce278c5 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -20,6 +20,10 @@
 		   [ --stdin | (<pattern>...)]
 git refs exists <ref>
 git refs optimize [--all] [--no-prune] [--auto] [--include <pattern>] [--exclude <pattern>]
+git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>
+git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]
+git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]
+git refs rename [--message=<reason>] <old-ref> <new-ref>
 
 DESCRIPTION
 -----------
@@ -51,6 +55,28 @@
 	usage. This subcommand is an alias for linkgit:git-pack-refs[1] and
 	offers identical functionality.
 
+create::
+	Create the given reference, which must not already exist, pointing at
+	`<new-value>`.
+
+delete::
+	Delete the given reference. This subcommand mirrors `git update-ref -d`
+	(see linkgit:git-update-ref[1]). When `<old-value>` is given, the
+	reference is only deleted after verifying that it currently contains
+	`<old-value>`.
+
+update::
+	Update the given reference to point at `<new-value>`. If `<old-value>`
+	is given, the reference is only updated after verifying that it
+	currently contains `<old-value>`. As a special case, an all-zeroes
+	`<new-value>` deletes the branch, whereas an all-zeroes `<old-value>`
+	ensures that the branch does not yet exist.
+
+rename::
+	Rename the reference `<oldref>` to `<newref>`. The old reference must
+	exist and the new reference must not yet exist, and both must have a
+	well-formed name (see linkgit:git-check-ref-format[1]).
+
 OPTIONS
 -------
 
@@ -90,6 +116,20 @@
 
 include::pack-refs-options.adoc[]
 
+The following options are specific to commands which write references:
+
+`--create-reflog`::
+	Create a reflog for the reference even if one would not ordinarily be
+	created.
+
+`--message=<reason>`::
+	Use the given <reason> string for the reflog entry associated with the
+	update. An empty message is rejected.
+
+`--no-deref`::
+	Operate on <ref> itself rather than the reference it points to via a
+	symbolic ref.
+
 KNOWN LIMITATIONS
 -----------------
 
diff --git a/Documentation/gitattributes.adoc b/Documentation/gitattributes.adoc
index bd76167..9fea75f 100644
--- a/Documentation/gitattributes.adoc
+++ b/Documentation/gitattributes.adoc
@@ -914,6 +914,8 @@
 - `scheme` suitable for source code in most Lisp dialects,
   including Scheme, Emacs Lisp, Common Lisp, and Clojure.
 
+- `swift` suitable for source code in the Swift language.
+
 - `tex` suitable for source code for LaTeX documents.
 
 
diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc
index 2beb705..7bf6201 100644
--- a/Documentation/gitprotocol-v2.adoc
+++ b/Documentation/gitprotocol-v2.adoc
@@ -568,21 +568,26 @@
 
 	oid <oid>
 	Indicates to the server an object which the client wants to obtain
-	information for.
+	information for. They must be full OIDs.
 
-The response of `object-info` is a list of the requested object ids
-and associated requested information, each separated by a single space.
+The response of `object-info` consists of one pkt-line per requested attribute,
+echoing the attributes the server will report, followed by one pkt-line per
+requested object id with its information, each field separated by a single
+space.
 
 	output = info flush-pkt
 
-	info = PKT-LINE(attrs) LF)
-		*PKT-LINE(obj-info LF)
-
-	attrs = attr | attrs SP attrs
+	info = *PKT-LINE(attr LF)
+	       *PKT-LINE(obj-info LF)
 
 	attr = "size"
 
-	obj-info = obj-id SP obj-size
+	obj-size = 1*DIGIT
+
+	obj-info = obj-id [SP [obj-size]]
+
+If the server does not recognize the OID, the response will be `<oid> SP`
+regardless of the number of attributes requested.
 
 bundle-uri
 ~~~~~~~~~~
diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index eaee6ee..fd831f0 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -1269,6 +1269,14 @@
 	By default it is set to 0 (no limit), zero and negative values
 	are ignored and treated as no limit.
 
+`--no-graph-indent`::
+`--graph-indent`::
+	When used with `--graph`, indent visual roots (commits with no parents
+	or whose parents are not shown) to differentiate them from commits that
+	are vertically adjacent but unrelated. Enabled by default. Use
+	`--no-graph-indent` to disable or set `log.graphIndent` to set a
+	default preference.
+
 ifdef::git-rev-list[]
 `--count`::
 	Print a number stating how many commits would have been
diff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc
index 6ea6c7c..3fbfbd3 100644
--- a/Documentation/revisions.adoc
+++ b/Documentation/revisions.adoc
@@ -127,6 +127,9 @@
   `git push` were run while `branchname` was checked out (or the current
   `HEAD` if no branchname is specified). Like for '@\{upstream\}', we report
   the remote-tracking branch that corresponds to that branch at the remote.
+  If the push destination is a URL and exactly one configured remote uses
+  that URL for pushing, '@\{push}' reports that remote's remote-tracking
+  branch.
 +
 Here's an example to make it more clear:
 +
diff --git a/Makefile b/Makefile
index 1f3f099..fac3e88 100644
--- a/Makefile
+++ b/Makefile
@@ -500,6 +500,14 @@
 #
 # Building Rust code requires Cargo.
 #
+# Define RUST_TARGETS if you want to cross-compile. If left unspecified, it uses
+# the default Rust target on the system.
+#
+# On macOS, this supports specifying multiple targets, separated by a space.
+# This will produce a Universal static library using `lipo`.
+#
+# Example: RUST_TARGETS="aarch64-apple-darwin x86_64-apple-darwin"
+#
 # == SHA-1 and SHA-256 defines ==
 #
 # === SHA-1 backend ===
@@ -939,16 +947,19 @@
 
 LIB_FILE = libgit.a
 
+ifndef NO_RUST
 ifdef DEBUG
-RUST_TARGET_DIR = target/debug
+RUST_BUILD_CONFIG = debug
 else
-RUST_TARGET_DIR = target/release
+RUST_BUILD_CONFIG = release
 endif
 
 ifeq ($(uname_S),Windows)
-RUST_LIB = $(RUST_TARGET_DIR)/gitcore.lib
+RUST_LIB_NAME = gitcore.lib
 else
-RUST_LIB = $(RUST_TARGET_DIR)/libgitcore.a
+RUST_LIB_NAME = libgitcore.a
+endif
+RUST_LIB = target/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME)
 endif
 
 GITLIBS = common-main.o $(LIB_FILE)
@@ -1158,6 +1169,7 @@
 LIB_OBJS += ewah/ewah_rlw.o
 LIB_OBJS += exec-cmd.o
 LIB_OBJS += fetch-negotiator.o
+LIB_OBJS += fetch-object-info.o
 LIB_OBJS += fetch-pack.o
 LIB_OBJS += fmt-merge-msg.o
 LIB_OBJS += fsck.o
@@ -2601,6 +2613,7 @@
 FUZZ_OBJS += oss-fuzz/fuzz-pack-headers.o
 FUZZ_OBJS += oss-fuzz/fuzz-pack-idx.o
 FUZZ_OBJS += oss-fuzz/fuzz-parse-attr-line.o
+FUZZ_OBJS += oss-fuzz/fuzz-reftable.o
 FUZZ_OBJS += oss-fuzz/fuzz-url-decode-mem.o
 .PHONY: fuzz-objs
 fuzz-objs: $(FUZZ_OBJS)
@@ -3019,11 +3032,35 @@
 $(LIB_FILE): $(LIB_OBJS)
 	$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
 
+ifndef NO_RUST
+ifeq ($(RUST_TARGETS),)
 $(RUST_LIB): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
 	$(QUIET_CARGO)cargo build $(CARGO_ARGS)
+else
+ifneq ($(words $(RUST_TARGETS)),1)
+ifneq ($(uname_S),Darwin)
+$(error Building universal Rust libraries requires macOS (lipo is not available on $(uname_S)))
+endif
+endif
+
+RUST_MEMBER_LIBS = $(foreach target,$(RUST_TARGETS),target/$(target)/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME))
+$(RUST_MEMBER_LIBS): target/%/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
+	$(QUIET_CARGO)cargo build $(CARGO_ARGS) --target $*
+
+$(RUST_LIB): $(RUST_MEMBER_LIBS)
+	$(call mkdir_p_parent_template)
+	$(QUIET_GEN)\
+	if test $(words $(RUST_TARGETS)) -gt 1; \
+	then \
+		lipo -create $^ -output $@; \
+	else \
+		cp $< $@; \
+	fi
+endif
 
 .PHONY: rust
 rust: $(RUST_LIB)
+endif
 
 export DEFAULT_EDITOR DEFAULT_PAGER
 
@@ -4074,7 +4111,8 @@
 contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
 	$(AR) $(ARFLAGS) $@ $^
 
-contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) GIT-LDFLAGS
+# When Rust is enabled, git-credential-osxkeychain depends on Rust symbols in $(RUST_LIB)
+contrib/credential/osxkeychain/git-credential-osxkeychain: contrib/credential/osxkeychain/git-credential-osxkeychain.o $(LIB_FILE) $(RUST_LIB) GIT-LDFLAGS
 	$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
 		$(filter %.o,$^) $(LIBS) -framework Security -framework CoreFoundation
 
diff --git a/README.md b/README.md
index d87bca1..46489b0 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-[![Build status](https://github.com/git/git/workflows/CI/badge.svg)](https://github.com/git/git/actions?query=branch%3Amaster+event%3Apush)
+[![GitHub build status](https://github.com/git/git/workflows/CI/badge.svg)](https://github.com/git/git/actions?query=branch%3Amaster+event%3Apush)
+[![GitLab build status](https://gitlab.com/git-scm/git/badges/master/pipeline.svg)](https://gitlab.com/git-scm/git/-/pipelines?ref=master)
 
 Git - fast, scalable, distributed revision control system
 =========================================================
diff --git a/advice.c b/advice.c
index 0018501..63bf8b0 100644
--- a/advice.c
+++ b/advice.c
@@ -69,6 +69,7 @@ static struct {
 	[ADVICE_PUSH_NON_FF_CURRENT]			= { "pushNonFFCurrent" },
 	[ADVICE_PUSH_NON_FF_MATCHING]			= { "pushNonFFMatching" },
 	[ADVICE_PUSH_REF_NEEDS_UPDATE]			= { "pushRefNeedsUpdate" },
+	[ADVICE_PUSH_REPO_LOOKS_LIKE_REF]		= { "pushRepoLooksLikeRef" },
 	[ADVICE_PUSH_UNQUALIFIED_REF_NAME]		= { "pushUnqualifiedRefName" },
 	[ADVICE_PUSH_UPDATE_REJECTED]			= { "pushUpdateRejected" },
 	[ADVICE_PUSH_UPDATE_REJECTED_ALIAS]		= { "pushNonFastForward" }, /* backwards compatibility */
diff --git a/advice.h b/advice.h
index 8def280..66f6cd6 100644
--- a/advice.h
+++ b/advice.h
@@ -36,6 +36,7 @@ enum advice_type {
 	ADVICE_PUSH_NON_FF_CURRENT,
 	ADVICE_PUSH_NON_FF_MATCHING,
 	ADVICE_PUSH_REF_NEEDS_UPDATE,
+	ADVICE_PUSH_REPO_LOOKS_LIKE_REF,
 	ADVICE_PUSH_UNQUALIFIED_REF_NAME,
 	ADVICE_PUSH_UPDATE_REJECTED,
 	ADVICE_PUSH_UPDATE_REJECTED_ALIAS,
diff --git a/apply.c b/apply.c
index 5e54453..f00b7ba 100644
--- a/apply.c
+++ b/apply.c
@@ -47,11 +47,17 @@ struct gitdiff_data {
 	int p_value;
 };
 
-static void git_apply_config(void)
+static void git_apply_config(struct repository *repo)
 {
-	repo_config_get_string(the_repository, "apply.whitespace", &apply_default_whitespace);
-	repo_config_get_string(the_repository, "apply.ignorewhitespace", &apply_default_ignorewhitespace);
-	repo_config(the_repository, git_xmerge_config, NULL);
+	struct repo_config_values *cfg = repo_config_values(repo);
+
+	FREE_AND_NULL(cfg->apply_default_whitespace);
+	repo_config_get_string(repo, "apply.whitespace",
+			       &cfg->apply_default_whitespace);
+	FREE_AND_NULL(cfg->apply_default_ignorewhitespace);
+	repo_config_get_string(repo, "apply.ignorewhitespace",
+			       &cfg->apply_default_ignorewhitespace);
+	repo_config(repo, git_xmerge_config, NULL);
 }
 
 static int parse_whitespace_option(struct apply_state *state, const char *option)
@@ -109,6 +115,8 @@ int init_apply_state(struct apply_state *state,
 		     struct repository *repo,
 		     const char *prefix)
 {
+	struct repo_config_values *cfg = repo_config_values(repo);
+
 	memset(state, 0, sizeof(*state));
 	state->prefix = prefix;
 	state->repo = repo;
@@ -126,10 +134,13 @@ int init_apply_state(struct apply_state *state,
 	strset_init(&state->kept_symlinks);
 	strbuf_init(&state->root, 0);
 
-	git_apply_config();
-	if (apply_default_whitespace && parse_whitespace_option(state, apply_default_whitespace))
+	git_apply_config(repo);
+
+	if (cfg->apply_default_whitespace &&
+	    parse_whitespace_option(state, cfg->apply_default_whitespace))
 		return -1;
-	if (apply_default_ignorewhitespace && parse_ignorewhitespace_option(state, apply_default_ignorewhitespace))
+	if (cfg->apply_default_ignorewhitespace &&
+	    parse_ignorewhitespace_option(state, cfg->apply_default_ignorewhitespace))
 		return -1;
 	return 0;
 }
@@ -192,7 +203,8 @@ int check_apply_state(struct apply_state *state, int force_apply)
 
 static void set_default_whitespace_mode(struct apply_state *state)
 {
-	if (!state->whitespace_option && !apply_default_whitespace)
+	if (!state->whitespace_option &&
+	    !repo_config_values(state->repo)->apply_default_whitespace)
 		state->ws_error_action = (state->apply ? warn_on_ws_error : nowarn_ws_error);
 }
 
@@ -3893,8 +3905,8 @@ static int check_preimage(struct apply_state *state,
 		if (*ce && !(*ce)->ce_mode)
 			BUG("ce_mode == 0 for path '%s'", old_name);
 
-		if (trust_executable_bit || !S_ISREG(st->st_mode))
-			st_mode = ce_mode_from_stat(*ce, st->st_mode);
+		if (repo_trust_executable_bit(state->repo) || !S_ISREG(st->st_mode))
+			st_mode = ce_mode_from_stat(state->repo, *ce, st->st_mode);
 		else if (*ce)
 			st_mode = (*ce)->ce_mode;
 		else
@@ -4008,7 +4020,7 @@ static int path_is_beyond_symlink_1(struct apply_state *state, struct strbuf *na
 			struct cache_entry *ce;
 
 			ce = index_file_exists(state->repo->index, name->buf,
-					       name->len, ignore_case);
+					       name->len, repo_ignore_case(the_repository));
 			if (ce && S_ISLNK(ce->ce_mode))
 				return 1;
 		} else {
@@ -4287,7 +4299,8 @@ static int build_fake_ancestor(struct apply_state *state, struct patch *list)
 		}
 	}
 
-	hold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR);
+	repo_hold_lock_file_for_update(state->repo, &lock, state->fake_ancestor,
+				       LOCK_DIE_ON_ERROR);
 	res = write_locked_index(&result, &lock, COMMIT_LOCK);
 	discard_index(&result);
 
@@ -4511,7 +4524,7 @@ static int try_create_file(struct apply_state *state, const char *path,
 		return !!mkdir(path, 0777);
 	}
 
-	if (has_symlinks && S_ISLNK(mode))
+	if (repo_has_symlinks(state->repo) && S_ISLNK(mode))
 		/* Although buf:size is counted string, it also is NUL
 		 * terminated.
 		 */
@@ -4945,9 +4958,10 @@ static int apply_patch(struct apply_state *state,
 	state->update_index = (state->check_index || state->ita_only) && state->apply;
 	if (state->update_index && !is_lock_file_locked(&state->lock_file)) {
 		if (state->index_file)
-			hold_lock_file_for_update(&state->lock_file,
-						  state->index_file,
-						  LOCK_DIE_ON_ERROR);
+			repo_hold_lock_file_for_update(state->repo,
+						       &state->lock_file,
+						       state->index_file,
+						       LOCK_DIE_ON_ERROR);
 		else
 			repo_hold_locked_index(state->repo, &state->lock_file,
 					       LOCK_DIE_ON_ERROR);
diff --git a/bloom.c b/bloom.c
index a805ac0..c98d167 100644
--- a/bloom.c
+++ b/bloom.c
@@ -16,6 +16,7 @@
 define_commit_slab(bloom_filter_slab, struct bloom_filter);
 
 static struct bloom_filter_slab bloom_filters;
+static int bloom_filter_slab_initialized;
 
 struct pathmap_hash_entry {
     struct hashmap_entry entry;
@@ -263,7 +264,10 @@ void add_key_to_filter(const struct bloom_key *key,
 
 void init_bloom_filters(void)
 {
+	if (bloom_filter_slab_initialized)
+		return;
 	init_bloom_filter_slab(&bloom_filters);
+	bloom_filter_slab_initialized = 1;
 }
 
 static void free_one_bloom_filter(struct bloom_filter *filter)
@@ -276,6 +280,7 @@ static void free_one_bloom_filter(struct bloom_filter *filter)
 void deinit_bloom_filters(void)
 {
 	deep_clear_bloom_filter_slab(&bloom_filters, free_one_bloom_filter);
+	bloom_filter_slab_initialized = 0;
 }
 
 struct bloom_keyvec *bloom_keyvec_new(const char *path, size_t len,
diff --git a/branch.c b/branch.c
index 243db7d..3a12e2a 100644
--- a/branch.c
+++ b/branch.c
@@ -61,7 +61,7 @@ static int find_tracked_branch(struct remote *remote, void *priv)
 
 static int should_setup_rebase(const char *origin)
 {
-	switch (autorebase) {
+	switch (repo_config_values(the_repository)->autorebase) {
 	case AUTOREBASE_NEVER:
 		return 0;
 	case AUTOREBASE_LOCAL:
@@ -372,7 +372,7 @@ int read_branch_desc(struct strbuf *buf, const char *branch_name)
  */
 int validate_branchname(const char *name, struct strbuf *ref)
 {
-	if (check_branch_ref(ref, name)) {
+	if (check_branch_ref(the_repository, ref, name)) {
 		int code = die_message(_("'%s' is not a valid branch name"), name);
 		advise_if_enabled(ADVICE_REF_SYNTAX,
 				  _("See 'git help check-ref-format'"));
@@ -394,7 +394,7 @@ static void prepare_checked_out_branches(void)
 		return;
 	initialized_checked_out_branches = 1;
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 
 	while (worktrees[i]) {
 		char *old, *wt_gitdir;
@@ -846,7 +846,7 @@ void remove_branch_state(struct repository *r, int verbose)
 
 void die_if_checked_out(const char *branch, int ignore_current_worktree)
 {
-	struct worktree **worktrees = get_worktrees();
+	struct worktree **worktrees = get_worktrees(the_repository);
 
 	for (int i = 0; worktrees[i]; i++) {
 		if (worktrees[i]->is_current && ignore_current_worktree)
diff --git a/builtin/add.c b/builtin/add.c
index c859f66..60ffbed 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -581,7 +581,7 @@ int cmd_add(int argc,
 		string_list_clear(&only_match_skip_worktree, 0);
 	}
 
-	transaction = odb_transaction_begin(repo->objects);
+	odb_transaction_begin_or_die(repo->objects, &transaction, 0);
 
 	ps_matched = xcalloc(pathspec.nr, 1);
 	if (add_renormalize)
diff --git a/builtin/bisect.c b/builtin/bisect.c
index 798e28f..3264e2d 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -663,6 +663,11 @@ static int bisect_successful(struct bisect_terms *terms)
 
 	refs_read_ref(get_main_ref_store(the_repository), bad_ref, &oid);
 	commit = lookup_commit_reference_by_name(bad_ref);
+	if (!commit) {
+		error(_("could not find commit for '%s'"), bad_ref);
+		free(bad_ref);
+		return BISECT_FAILED;
+	}
 	repo_format_commit_message(the_repository, commit, "%s", &commit_name,
 				   &pp);
 
@@ -806,9 +811,11 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
 	 */
 	head = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
 				       "HEAD", 0, &head_oid, &flags);
-	if (!head)
+	if (!head) {
 		if (repo_get_oid(the_repository, "HEAD", &head_oid))
 			return error(_("bad HEAD - I need a HEAD"));
+		head = "HEAD";
+	}
 
 	/*
 	 * Check if we are bisecting
diff --git a/builtin/blame.c b/builtin/blame.c
index 553f4cb..48d5251 100644
--- a/builtin/blame.c
+++ b/builtin/blame.c
@@ -453,6 +453,36 @@ static void determine_line_heat(struct commit_info *ci, const char **dest_color)
 	*dest_color = colorfield[i].col;
 }
 
+static inline int maybe_putc(int c, FILE *out)
+{
+	return out ? putc(c, out) : 0;
+}
+
+static size_t print_marks(FILE *out, const struct blame_entry *ent, int opt)
+{
+	size_t len = 0;
+
+	if ((ent->suspect->commit->object.flags & UNINTERESTING) &&
+	    !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) {
+		maybe_putc('^', out);
+		len++;
+	}
+	if (mark_unblamable_lines && ent->unblamable) {
+		maybe_putc('*', out);
+		len++;
+	}
+	if (mark_ignored_lines && ent->ignored) {
+		maybe_putc('?', out);
+		len++;
+	}
+	return len;
+}
+
+static size_t count_marks(const struct blame_entry *ent, int opt)
+{
+	return print_marks(NULL, ent, opt);
+}
+
 static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,
 		       int opt, struct blame_entry *prev_ent)
 {
@@ -499,23 +529,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent,
 		if (color)
 			fputs(color, stdout);
 
-		if (suspect->commit->object.flags & UNINTERESTING) {
-			if (blank_boundary) {
-				memset(hex, ' ', strlen(hex));
-			} else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) {
-				length--;
-				putchar('^');
-			}
-		}
-
-		if (mark_unblamable_lines && ent->unblamable) {
-			length--;
-			putchar('*');
-		}
-		if (mark_ignored_lines && ent->ignored) {
-			length--;
-			putchar('?');
-		}
+		if ((suspect->commit->object.flags & UNINTERESTING) &&
+		    blank_boundary)
+			memset(hex, ' ', strlen(hex));
+		length -= print_marks(stdout, ent, opt);
 
 		printf("%.*s", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex);
 		if (opt & OUTPUT_ANNOTATE_COMPAT) {
@@ -647,11 +664,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)
 	struct blame_entry *e;
 	int compute_auto_abbrev = (abbrev < 0);
 	int auto_abbrev = DEFAULT_ABBREV;
+	size_t max_marks_count = 0;
 
 	for (e = sb->ent; e; e = e->next) {
 		struct blame_origin *suspect = e->suspect;
 		int num;
+		size_t marks_count = count_marks(e, *option);
 
+		if (max_marks_count < marks_count)
+			max_marks_count = marks_count;
 		if (compute_auto_abbrev)
 			auto_abbrev = update_auto_abbrev(auto_abbrev, suspect);
 		if (strcmp(suspect->path, sb->path))
@@ -685,8 +706,12 @@ static void find_alignment(struct blame_scoreboard *sb, int *option)
 	max_score_digits = decimal_width(largest_score);
 
 	if (compute_auto_abbrev)
-		/* one more abbrev length is needed for the boundary commit */
-		abbrev = auto_abbrev + 1;
+		abbrev = auto_abbrev;
+	if (abbrev < (int)the_hash_algo->hexsz) {
+		abbrev += max_marks_count;
+		if (abbrev > (int)the_hash_algo->hexsz)
+			abbrev = the_hash_algo->hexsz;
+	}
 }
 
 static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa)
@@ -1013,6 +1038,8 @@ int cmd_blame(int argc,
 		case PARSE_OPT_UNKNOWN:
 			break;
 		case PARSE_OPT_HELP:
+			exit(0);
+		case PARSE_OPT_HELP_ERROR:
 		case PARSE_OPT_ERROR:
 		case PARSE_OPT_SUBCOMMAND:
 			exit(129);
@@ -1047,10 +1074,7 @@ int cmd_blame(int argc,
 	} else if (show_progress < 0)
 		show_progress = isatty(2);
 
-	if (0 < abbrev && abbrev < (int)the_hash_algo->hexsz)
-		/* one more abbrev length is needed for the boundary commit */
-		abbrev++;
-	else if (!abbrev)
+	if (!abbrev)
 		abbrev = the_hash_algo->hexsz;
 
 	if (revs_file && read_ancestry(revs_file))
diff --git a/builtin/branch.c b/builtin/branch.c
index 1572a4f..031a4a9 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -259,7 +259,8 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 		char *target = NULL;
 		int flags = 0;
 
-		copy_branchname(&bname, argv[i], allowed_interpret);
+		copy_branchname(the_repository, &bname,
+				argv[i], allowed_interpret);
 		free(name);
 		name = mkpathdup(fmt, bname.buf);
 
@@ -579,9 +580,9 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
 	const char *interpreted_oldname = NULL;
 	const char *interpreted_newname = NULL;
 	int recovery = 0, oldref_usage = 0;
-	struct worktree **worktrees = get_worktrees();
+	struct worktree **worktrees = get_worktrees(the_repository);
 
-	if (check_branch_ref(&oldref, oldname)) {
+	if (check_branch_ref(the_repository, &oldref, oldname)) {
 		/*
 		 * Bad name --- this could be an attempt to rename a
 		 * ref that we used to allow to be created by accident.
@@ -706,6 +707,29 @@ static int edit_branch_description(const char *branch_name)
 	return 0;
 }
 
+static void die_if_upstream_looks_like_remote(const char *new_upstream, const char *branch_name)
+{
+	struct strbuf remote_ref = STRBUF_INIT;
+	int code;
+
+	if (strchr(new_upstream, '/') ||
+	    !remote_is_configured(remote_get(new_upstream), 0))
+		return;
+
+	strbuf_addf(&remote_ref, "refs/remotes/%s/%s", new_upstream, branch_name);
+	if (!refs_ref_exists(get_main_ref_store(the_repository), remote_ref.buf)) {
+		strbuf_release(&remote_ref);
+		return;
+	}
+
+	code = die_message(_("--set-upstream-to takes a single <remote>/<branch> argument"));
+	advise_if_enabled(ADVICE_SET_UPSTREAM_FAILURE,
+			  _("Did you mean to use: git branch --set-upstream-to=%s/%s?"),
+			  new_upstream, branch_name);
+	strbuf_release(&remote_ref);
+	exit(code);
+}
+
 int cmd_branch(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -898,7 +922,8 @@ int cmd_branch(int argc,
 				die(_("cannot give description to detached HEAD"));
 			branch_name = head;
 		} else if (argc == 1) {
-			copy_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL);
+			copy_branchname(the_repository, &buf, argv[0],
+					INTERPRET_BRANCH_LOCAL);
 			branch_name = buf.buf;
 		} else {
 			die(_("cannot edit description of more than one branch"));
@@ -941,7 +966,8 @@ int cmd_branch(int argc,
 		if (!argc)
 			branch = branch_get(NULL);
 		else if (argc == 1) {
-			copy_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL);
+			copy_branchname(the_repository, &buf, argv[0],
+					INTERPRET_BRANCH_LOCAL);
 			branch = branch_get(buf.buf);
 		} else
 			die(_("too many arguments to set new upstream"));
@@ -957,6 +983,15 @@ int cmd_branch(int argc,
 		if (!refs_ref_exists(get_main_ref_store(the_repository), branch->refname)) {
 			if (!argc || branch_checked_out(branch->refname))
 				die(_("no commit on branch '%s' yet"), branch->name);
+			/*
+			 * Check the advice up front to avoid the ref
+			 * lookups when the hint is off. The helper still
+			 * calls advise_if_enabled() so the hint carries the
+			 * standard "disable this message" instructions.
+			 */
+			if (argc == 1 &&
+			    advice_enabled(ADVICE_SET_UPSTREAM_FAILURE))
+				die_if_upstream_looks_like_remote(new_upstream, argv[0]);
 			die(_("branch '%s' does not exist"), branch->name);
 		}
 
@@ -971,7 +1006,8 @@ int cmd_branch(int argc,
 		if (!argc)
 			branch = branch_get(NULL);
 		else if (argc == 1) {
-			copy_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL);
+			copy_branchname(the_repository, &buf, argv[0],
+					INTERPRET_BRANCH_LOCAL);
 			branch = branch_get(buf.buf);
 		} else
 			die(_("too many arguments to unset upstream"));
diff --git a/builtin/cat-file.c b/builtin/cat-file.c
index 60869b8..884b6d5 100644
--- a/builtin/cat-file.c
+++ b/builtin/cat-file.c
@@ -20,7 +20,6 @@
 #include "userdiff.h"
 #include "oid-array.h"
 #include "packfile.h"
-#include "pack-bitmap.h"
 #include "object-file.h"
 #include "object-name.h"
 #include "odb.h"
@@ -29,6 +28,22 @@
 #include "promisor-remote.h"
 #include "mailmap.h"
 #include "write-or-die.h"
+#include "alias.h"
+#include "remote.h"
+#include "transport.h"
+
+/*
+ * Maximum length for a remote URL. While no universal standard exists,
+ * 8K is assumed to be a reasonable limit.
+ */
+#define MAX_REMOTE_URL_LEN (8 * 1024)
+
+/* Maximum number of objects allowed in a single remote-object-info request. */
+#define MAX_ALLOWED_OBJ_LIMIT 10000
+
+/* Maximum input size permitted for the remote-object-info command. */
+#define MAX_REMOTE_OBJ_INFO_LINE \
+	(MAX_REMOTE_URL_LEN + MAX_ALLOWED_OBJ_LIMIT * (GIT_MAX_HEXSZ + 1))
 
 enum batch_mode {
 	BATCH_MODE_CONTENTS,
@@ -317,8 +332,24 @@ struct expand_data {
 	 * optimized out.
 	 */
 	unsigned skip_object_info : 1;
+
+	/*
+	 * Flags about when an object info is being fetched from remote.
+	 */
+	unsigned is_remote:1;
+
+	/*
+	 * List of atoms (i.e. "objectsize") that the server supports. Built
+	 * from the server's object-info advertised capabilities.
+	 */
+	struct string_list remote_allowed_atoms;
 };
-#define EXPAND_DATA_INIT  { .mode = S_IFINVALID }
+
+#define EXPAND_DATA_INIT  {  \
+	.mode = S_IFINVALID, \
+	.type = OBJ_BAD,     \
+	.remote_allowed_atoms = STRING_LIST_INIT_NODUP, \
+}
 
 static int is_atom(const char *atom, const char *s, int slen)
 {
@@ -329,14 +360,26 @@ static int is_atom(const char *atom, const char *s, int slen)
 static int expand_atom(struct strbuf *sb, const char *atom, int len,
 		       struct expand_data *data)
 {
+	if (data->is_remote) {
+		size_t i;
+		for (i = 0; i < data->remote_allowed_atoms.nr; i++)
+			if (is_atom(data->remote_allowed_atoms.items[i].string,
+				    atom, len))
+				break;
+		if (i == data->remote_allowed_atoms.nr)
+			return 1;
+	}
+
 	if (is_atom("objectname", atom, len)) {
 		if (!data->mark_query)
 			strbuf_add_oid_hex(sb, &data->oid);
 	} else if (is_atom("objecttype", atom, len)) {
-		if (data->mark_query)
+		if (data->mark_query) {
 			data->info.typep = &data->type;
-		else
-			strbuf_addstr(sb, type_name(data->type));
+		} else {
+			const char *t = type_name(data->type);
+			strbuf_addstr(sb, t ? t : "");
+		}
 	} else if (is_atom("objectsize", atom, len)) {
 		if (data->mark_query)
 			data->info.sizep = &data->size;
@@ -500,7 +543,7 @@ static void batch_object_write(const char *obj_name,
 			data->info.sizep = &data->size;
 
 		if (pack)
-			ret = packed_object_info(pack, offset, &data->info);
+			ret = packed_object_info(NULL, pack, offset, &data->info);
 		else
 			ret = odb_read_object_info_extended(the_repository->objects,
 							    &data->oid, &data->info,
@@ -636,6 +679,62 @@ static void batch_one_object(const char *obj_name,
 	object_context_release(&ctx);
 }
 
+static int get_remote_info(int argc,
+			   const char **argv,
+			   struct object_info **remote_object_info,
+			   struct oid_array *object_info_oids,
+			   struct string_list *object_info_options)
+{
+	int retval = 0;
+	struct remote *remote = NULL;
+	struct object_id oid;
+	struct transport *gtransport;
+
+	remote = remote_get(argv[0]);
+	if (!remote)
+		die(_("must supply valid remote when using remote-object-info"));
+
+	oid_array_clear(object_info_oids);
+	for (size_t i = 1; i < argc; i++) {
+		if (get_oid_hex(argv[i], &oid)) {
+			size_t len = strlen(argv[i]);
+
+			if (len < the_hash_algo->hexsz && len >= 4) {
+				size_t j;
+				for (j = 0; j < len; j++)
+					if (!isxdigit(argv[i][j]))
+						break;
+				if (j == len)
+					die(_("remote-object-info does not support "
+					      "short oids, %d characters required"),
+					    (int)the_hash_algo->hexsz);
+			}
+			die(_("not a valid object name '%s'"), argv[i]);
+		}
+		oid_array_append(object_info_oids, &oid);
+	}
+
+	if (!object_info_oids->nr)
+		die(_("remote-object-info requires objects"));
+
+	gtransport = transport_get(remote, NULL);
+
+	if (!gtransport->smart_options) {
+		retval = -1;
+		goto cleanup;
+	}
+
+	CALLOC_ARRAY(*remote_object_info, object_info_oids->nr);
+	gtransport->smart_options->object_info_oids = object_info_oids;
+
+	gtransport->smart_options->object_info_options = object_info_options;
+	gtransport->smart_options->object_info_data = *remote_object_info;
+	retval = transport_fetch_object_info(gtransport);
+cleanup:
+	transport_disconnect(gtransport);
+	return retval;
+}
+
 struct object_cb_data {
 	struct batch_options *opt;
 	struct expand_data *expand;
@@ -717,18 +816,115 @@ static void parse_cmd_mailmap(struct batch_options *opt UNUSED,
 		load_mailmap();
 }
 
+struct protocol_placeholder_entry {
+	const char *option;
+	const char *atom;
+};
+
+static const struct protocol_placeholder_entry remote_atom_map[] = {
+	{"size", "objectsize"},
+	{"type", "objecttype"},
+	/*
+	 * Add new protocol options here. Even if the server doesn't support
+	 * them the allow_list will drop them if the server doesn't advertise
+	 * them.
+	 */
+};
+
+static void parse_cmd_remote_object_info(struct batch_options *opt,
+					 const char *line, struct strbuf *output,
+					 struct expand_data *data)
+{
+	int count;
+	const char **argv;
+	char *line_to_split;
+	struct object_info *remote_object_info = NULL;
+	struct oid_array object_info_oids = OID_ARRAY_INIT;
+	struct string_list object_info_options = STRING_LIST_INIT_NODUP;
+	const char *saved_format = opt->format;
+
+	if (strlen(line) >= MAX_REMOTE_OBJ_INFO_LINE)
+		die(_("remote-object-info command too long"));
+	/*
+	 * TODO: Use the default format once %(objecttype) is supported.
+	 */
+	if (!opt->format)
+		opt->format = "%(objectname) %(objectsize)";
+
+	line_to_split = xstrdup(line);
+	count = split_cmdline(line_to_split, &argv);
+	if (count < 0)
+		die(_("remote-object-info: failed to parse command line: %s"),
+		    split_cmdline_strerror(count));
+	if (count - 1 > MAX_ALLOWED_OBJ_LIMIT)
+		die(_("remote-object-info supports at most %d objects"),
+		    MAX_ALLOWED_OBJ_LIMIT);
+
+	if (data->info.sizep)
+		string_list_append(&object_info_options, "size");
+	if (data->info.typep)
+		string_list_append(&object_info_options, "type");
+
+	if (get_remote_info(count, argv, &remote_object_info,
+			    &object_info_oids, &object_info_options))
+		die(_("failed to get object info from the remote: %s"), argv[0]);
+
+	string_list_clear(&data->remote_allowed_atoms, 0);
+	string_list_append(&data->remote_allowed_atoms, "objectname");
+	for (size_t i = 0; i < ARRAY_SIZE(remote_atom_map); i++)
+		if (unsorted_string_list_has_string(&object_info_options, remote_atom_map[i].option))
+			string_list_append(&data->remote_allowed_atoms,
+					   remote_atom_map[i].atom);
+
+	data->skip_object_info = 1;
+	for (size_t i = 0; i < object_info_oids.nr; i++) {
+		data->oid = object_info_oids.oid[i];
+
+		if (remote_object_info[i].unrecognized) {
+			report_object_status(opt, oid_to_hex(&data->oid),
+					     &data->oid, "missing");
+			continue;
+		}
+
+		/*
+		 * When reaching here, it means remote-object-info can retrieve
+		 * information from server without downloading them.
+		 */
+		if (remote_object_info[i].sizep) {
+			data->size = *remote_object_info[i].sizep;
+		}
+
+		if (remote_object_info[i].typep) {
+			data->type = *remote_object_info[i].typep;
+		}
+
+		opt->batch_mode = BATCH_MODE_INFO;
+		data->is_remote = 1;
+		batch_object_write(argv[i + 1], output, opt, data, NULL, 0);
+		data->is_remote = 0;
+	}
+	data->skip_object_info = 0;
+	opt->format = saved_format;
+
+	for (size_t i = 0; i < object_info_oids.nr; i++)
+		free_object_info_contents(&remote_object_info[i]);
+	string_list_clear(&object_info_options, 0);
+	free(line_to_split);
+	free(argv);
+	free(remote_object_info);
+	oid_array_clear(&object_info_oids);
+}
+
 static void dispatch_calls(struct batch_options *opt,
 		struct strbuf *output,
 		struct expand_data *data,
 		struct queued_cmd *cmd,
-		int nr)
+		size_t nr)
 {
-	int i;
-
 	if (!opt->buffer_output)
 		die(_("flush is only for --buffer mode"));
 
-	for (i = 0; i < nr; i++)
+	for (size_t i = 0; i < nr; i++)
 		cmd[i].fn(opt, cmd[i].line, output, data);
 
 	fflush(stdout);
@@ -736,9 +932,7 @@ static void dispatch_calls(struct batch_options *opt,
 
 static void free_cmds(struct queued_cmd *cmd, size_t *nr)
 {
-	size_t i;
-
-	for (i = 0; i < *nr; i++)
+	for (size_t i = 0; i < *nr; i++)
 		FREE_AND_NULL(cmd[i].line);
 
 	*nr = 0;
@@ -751,9 +945,10 @@ static const struct parse_cmd {
 	unsigned takes_args;
 } commands[] = {
 	{ "contents", parse_cmd_contents, 1 },
-	{ "info", parse_cmd_info, 1 },
 	{ "flush", NULL, 0 },
+	{ "info", parse_cmd_info, 1 },
 	{ "mailmap", parse_cmd_mailmap, 1 },
+	{ "remote-object-info", parse_cmd_remote_object_info, 1 },
 };
 
 static void batch_objects_command(struct batch_options *opt,
@@ -765,7 +960,6 @@ static void batch_objects_command(struct batch_options *opt,
 	size_t alloc = 0, nr = 0;
 
 	while (strbuf_getdelim_strip_crlf(&input, stdin, opt->input_delim) != EOF) {
-		int i;
 		const struct parse_cmd *cmd = NULL;
 		const char *p = NULL, *cmd_end;
 		struct queued_cmd call = {0};
@@ -775,7 +969,7 @@ static void batch_objects_command(struct batch_options *opt,
 		if (isspace(*input.buf))
 			die(_("whitespace before command: '%s'"), input.buf);
 
-		for (i = 0; i < ARRAY_SIZE(commands); i++) {
+		for (size_t i = 0; i < ARRAY_SIZE(commands); i++) {
 			if (!skip_prefix(input.buf, commands[i].name, &cmd_end))
 				continue;
 
@@ -837,34 +1031,13 @@ static int batch_one_object_oi(const struct object_id *oid,
 			       void *_payload)
 {
 	struct for_each_object_payload *payload = _payload;
-	if (oi && oi->whence == OI_PACKED)
-		return payload->callback(oid, oi->u.packed.pack, oi->u.packed.offset,
+	if (oi && oi->source_infop->source->type == ODB_SOURCE_PACKED)
+		return payload->callback(oid, oi->source_infop->u.packed.pack,
+					 oi->source_infop->u.packed.offset,
 					 payload->payload);
 	return payload->callback(oid, NULL, 0, payload->payload);
 }
 
-static int batch_one_object_packed(const struct object_id *oid,
-				   struct packed_git *pack,
-				   uint32_t pos,
-				   void *_payload)
-{
-	struct for_each_object_payload *payload = _payload;
-	return payload->callback(oid, pack, nth_packed_object_offset(pack, pos),
-				 payload->payload);
-}
-
-static int batch_one_object_bitmapped(const struct object_id *oid,
-				      enum object_type type UNUSED,
-				      int flags UNUSED,
-				      uint32_t hash UNUSED,
-				      struct packed_git *pack,
-				      off_t offset,
-				      void *_payload)
-{
-	struct for_each_object_payload *payload = _payload;
-	return payload->callback(oid, pack, offset, payload->payload);
-}
-
 static void batch_each_object(struct batch_options *opt,
 			      for_each_object_fn callback,
 			      unsigned flags,
@@ -874,53 +1047,17 @@ static void batch_each_object(struct batch_options *opt,
 		.callback = callback,
 		.payload = _payload,
 	};
+	struct odb_source_info source_info;
+	struct object_info oi = {
+		.source_infop = &source_info,
+	};
 	struct odb_for_each_object_options opts = {
 		.flags = flags,
+		.filter = &opt->objects_filter,
 	};
-	struct bitmap_index *bitmap = NULL;
-	struct odb_source *source;
 
-	/*
-	 * TODO: we still need to tap into implementation details of the object
-	 * database sources. Ideally, we should extend `odb_for_each_object()`
-	 * to handle object filters itself so that we can move the filtering
-	 * logic into the individual sources.
-	 */
-	odb_prepare_alternates(the_repository->objects);
-	for (source = the_repository->objects->sources; source; source = source->next) {
-		struct odb_source_files *files = odb_source_files_downcast(source);
-		int ret = odb_source_for_each_object(&files->loose->base, NULL, batch_one_object_oi,
-						     &payload, &opts);
-		if (ret)
-			break;
-	}
-
-	if (opt->objects_filter.choice != LOFC_DISABLED &&
-	    (bitmap = prepare_bitmap_git(the_repository)) &&
-	    !for_each_bitmapped_object(bitmap, &opt->objects_filter,
-				       batch_one_object_bitmapped, &payload)) {
-		struct packed_git *pack;
-
-		repo_for_each_pack(the_repository, pack) {
-			if (bitmap_index_contains_pack(bitmap, pack) ||
-			    open_pack_index(pack))
-				continue;
-			for_each_object_in_pack(pack, batch_one_object_packed,
-						&payload, flags);
-		}
-	} else {
-		struct object_info oi = { 0 };
-
-		for (source = the_repository->objects->sources; source; source = source->next) {
-			struct odb_source_files *files = odb_source_files_downcast(source);
-			int ret = odb_source_for_each_object(&files->packed->base, &oi,
-							     batch_one_object_oi, &payload, &opts);
-			if (ret)
-				break;
-		}
-	}
-
-	free_bitmap_index(bitmap);
+	odb_for_each_object_ext(the_repository->objects, &oi,
+				batch_one_object_oi, &payload, &opts);
 }
 
 static int batch_objects(struct batch_options *opt)
@@ -1030,6 +1167,7 @@ static int batch_objects(struct batch_options *opt)
  cleanup:
 	strbuf_release(&input);
 	strbuf_release(&output);
+	string_list_clear(&data.remote_allowed_atoms, 0);
 	cfg->warn_on_object_refname_ambiguity = save_warning;
 	return retval;
 }
diff --git a/builtin/check-ref-format.c b/builtin/check-ref-format.c
index e42b044..fd1c9c0 100644
--- a/builtin/check-ref-format.c
+++ b/builtin/check-ref-format.c
@@ -45,7 +45,7 @@ static int check_ref_format_branch(const char *arg)
 	int nongit;
 
 	setup_git_directory_gently(the_repository, &nongit);
-	if (check_branch_ref(&sb, arg) ||
+	if (check_branch_ref(the_repository, &sb, arg) ||
 	    !skip_prefix(sb.buf, "refs/heads/", &name))
 		die("'%s' is not a valid branch name", arg);
 	printf("%s\n", name);
diff --git a/builtin/checkout.c b/builtin/checkout.c
index aee84ca..55e3a89 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -805,7 +805,7 @@ static void setup_branch_path(struct branch_info *branch)
 			   &branch->oid, &branch->refname, 0))
 		repo_get_oid_committish(the_repository, branch->name, &branch->oid);
 
-	copy_branchname(&buf, branch->name, INTERPRET_BRANCH_LOCAL);
+	copy_branchname(the_repository, &buf, branch->name, INTERPRET_BRANCH_LOCAL);
 	if (strcmp(buf.buf, branch->name)) {
 		free(branch->name);
 		branch->name = xstrdup(buf.buf);
diff --git a/builtin/clone.c b/builtin/clone.c
index d60d1b6..5b25cca 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -335,7 +335,7 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
 				die_errno(_("failed to create link '%s'"), dest->buf);
 			option_no_hardlinks = 1;
 		}
-		if (copy_file_with_time(dest->buf, src->buf, 0666))
+		if (copy_file_with_time(the_repository, dest->buf, src->buf, 0666))
 			die_errno(_("failed to copy file to '%s'"), dest->buf);
 	}
 
@@ -557,7 +557,7 @@ static void update_remote_refs(const struct ref *refs,
 			write_followtags(refs, msg);
 	}
 
-	if (remote_head_points_at && !option_bare) {
+	if (remote_head_points_at && remote_head_points_at->peer_ref && !option_bare) {
 		struct strbuf head_ref = STRBUF_INIT;
 		strbuf_addstr(&head_ref, branch_top);
 		strbuf_addstr(&head_ref, "HEAD");
@@ -1116,7 +1116,6 @@ int cmd_clone(int argc,
 			die_errno(_("could not create work tree dir '%s'"),
 				  work_tree);
 		junk_work_tree = work_tree;
-		set_git_work_tree(the_repository, work_tree);
 	}
 
 	if (real_git_dir) {
@@ -1186,9 +1185,10 @@ int cmd_clone(int argc,
 	 * repository, and reference backends may persist that information into
 	 * their on-disk data structures.
 	 */
-	init_db(the_repository, git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN,
-		ref_storage_format, NULL,
-		do_not_override_repo_unix_permissions, INIT_DB_QUIET | INIT_DB_SKIP_REFDB);
+	init_db(the_repository, git_dir, real_git_dir, work_tree, option_template,
+		GIT_HASH_UNKNOWN, ref_storage_format, NULL,
+		do_not_override_repo_unix_permissions,
+		INIT_DB_QUIET | INIT_DB_SKIP_REFDB);
 
 	if (real_git_dir) {
 		free((char *)git_dir);
diff --git a/builtin/config.c b/builtin/config.c
index 8d8ec0b..0882899 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -974,7 +974,7 @@ static void location_options_init(struct config_location_options *opts,
 		opts->source.file = opts->file_to_free = repo_git_path(the_repository, "config");
 		opts->source.scope = CONFIG_SCOPE_LOCAL;
 	} else if (opts->use_worktree_config) {
-		struct worktree **worktrees = get_worktrees();
+		struct worktree **worktrees = get_worktrees(the_repository);
 		if (the_repository->repository_format_worktree_config)
 			opts->source.file = opts->file_to_free =
 				repo_git_path(the_repository, "config.worktree");
diff --git a/builtin/describe.c b/builtin/describe.c
index a94dad9..c0abc93 100644
--- a/builtin/describe.c
+++ b/builtin/describe.c
@@ -251,56 +251,19 @@ static int compare_pt(const void *a_, const void *b_)
 	return 0;
 }
 
-struct lazy_queue {
-	struct prio_queue queue;
-	bool get_pending;
-};
-
-#define LAZY_QUEUE_INIT { { compare_commits_by_commit_date }, false }
-
-static void *lazy_queue_get(struct lazy_queue *queue)
-{
-	if (queue->get_pending)
-		prio_queue_get(&queue->queue);
-	else
-		queue->get_pending = true;
-	return prio_queue_peek(&queue->queue);
-}
-
-static void lazy_queue_put(struct lazy_queue *queue, void *thing)
-{
-	if (queue->get_pending)
-		prio_queue_replace(&queue->queue, thing);
-	else
-		prio_queue_put(&queue->queue, thing);
-	queue->get_pending = false;
-}
-
-static bool lazy_queue_empty(const struct lazy_queue *queue)
-{
-	return queue->queue.nr == (queue->get_pending ? 1 : 0);
-}
-
-static void lazy_queue_clear(struct lazy_queue *queue)
-{
-	clear_prio_queue(&queue->queue);
-	queue->get_pending = false;
-}
-
-static unsigned long finish_depth_computation(struct lazy_queue *queue,
+static unsigned long finish_depth_computation(struct prio_queue *queue,
 					      struct possible_tag *best)
 {
 	unsigned long seen_commits = 0;
 	struct oidset unflagged = OIDSET_INIT;
+	struct commit *c;
 
-	for (size_t i = queue->get_pending ? 1 : 0; i < queue->queue.nr; i++) {
-		struct commit *commit = queue->queue.array[i].data;
-		if (!(commit->object.flags & best->flag_within))
-			oidset_insert(&unflagged, &commit->object.oid);
+	prio_queue_for_each(queue, c) {
+		if (!(c->object.flags & best->flag_within))
+			oidset_insert(&unflagged, &c->object.oid);
 	}
 
-	while (!lazy_queue_empty(queue)) {
-		struct commit *c = lazy_queue_get(queue);
+	while ((c = prio_queue_get(queue))) {
 		struct commit_list *parents = c->parents;
 		seen_commits++;
 		if (c->object.flags & best->flag_within) {
@@ -316,7 +279,7 @@ static unsigned long finish_depth_computation(struct lazy_queue *queue,
 			repo_parse_commit(the_repository, p);
 			seen = p->object.flags & SEEN;
 			if (!seen)
-				lazy_queue_put(queue, p);
+				prio_queue_put(queue, p);
 			flag_before = p->object.flags & best->flag_within;
 			p->object.flags |= c->object.flags;
 			flag_after = p->object.flags & best->flag_within;
@@ -364,8 +327,8 @@ static void append_suffix(int depth, const struct object_id *oid, struct strbuf
 
 static void describe_commit(struct commit *cmit, struct strbuf *dst)
 {
-	struct commit *gave_up_on = NULL;
-	struct lazy_queue queue = LAZY_QUEUE_INIT;
+	struct commit *c, *gave_up_on = NULL;
+	struct prio_queue queue = { compare_commits_by_commit_date };
 	struct commit_name *n;
 	struct possible_tag all_matches[MAX_TAGS];
 	unsigned int match_cnt = 0, annotated_cnt = 0, cur_match;
@@ -407,9 +370,8 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
 	}
 
 	cmit->object.flags = SEEN;
-	lazy_queue_put(&queue, cmit);
-	while (!lazy_queue_empty(&queue)) {
-		struct commit *c = lazy_queue_get(&queue);
+	prio_queue_put(&queue, cmit);
+	while ((c = prio_queue_get(&queue))) {
 		struct commit_list *parents = c->parents;
 		struct commit_name **slot;
 
@@ -443,7 +405,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
 				t->depth++;
 		}
 		/* Stop if last remaining path already covered by best candidate(s) */
-		if (annotated_cnt && lazy_queue_empty(&queue)) {
+		if (annotated_cnt && !prio_queue_size(&queue)) {
 			int best_depth = INT_MAX;
 			unsigned best_within = 0;
 			for (cur_match = 0; cur_match < match_cnt; cur_match++) {
@@ -466,7 +428,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
 			struct commit *p = parents->item;
 			repo_parse_commit(the_repository, p);
 			if (!(p->object.flags & SEEN))
-				lazy_queue_put(&queue, p);
+				prio_queue_put(&queue, p);
 			p->object.flags |= c->object.flags;
 			parents = parents->next;
 
@@ -481,7 +443,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
 			strbuf_add_unique_abbrev(dst, cmit_oid, abbrev);
 			if (suffix)
 				strbuf_addstr(dst, suffix);
-			lazy_queue_clear(&queue);
+			clear_prio_queue(&queue);
 			return;
 		}
 		if (unannotated_cnt)
@@ -497,11 +459,11 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
 	QSORT(all_matches, match_cnt, compare_pt);
 
 	if (gave_up_on) {
-		lazy_queue_put(&queue, gave_up_on);
+		prio_queue_put(&queue, gave_up_on);
 		seen_commits--;
 	}
 	seen_commits += finish_depth_computation(&queue, &all_matches[0]);
-	lazy_queue_clear(&queue);
+	clear_prio_queue(&queue);
 
 	if (debug) {
 		static int label_width = -1;
diff --git a/builtin/diff.c b/builtin/diff.c
index 4b46e39..18b1083 100644
--- a/builtin/diff.c
+++ b/builtin/diff.c
@@ -579,9 +579,13 @@ int cmd_diff(int argc,
 		obj = deref_tag(the_repository, obj, NULL, 0);
 		if (!obj)
 			die(_("invalid object '%s' given."), name);
-		if (obj->type == OBJ_COMMIT)
-			obj = &repo_get_commit_tree(the_repository,
-						    ((struct commit *)obj))->object;
+		if (obj->type == OBJ_COMMIT) {
+			struct tree *tree = repo_get_commit_tree(
+				the_repository, (struct commit *)obj);
+			if (!tree)
+				die(_("unable to read tree object for commit '%s'"), name);
+			obj = &tree->object;
+		}
 
 		if (obj->type == OBJ_TREE) {
 			if (sdiff.skip && bitmap_get(sdiff.skip, i))
diff --git a/builtin/difftool.c b/builtin/difftool.c
index 26778f8..bc7b2ea 100644
--- a/builtin/difftool.c
+++ b/builtin/difftool.c
@@ -552,7 +552,7 @@ static int run_dir_diff(struct repository *repo,
 					struct stat st;
 					if (stat(wtdir.buf, &st))
 						st.st_mode = 0644;
-					if (copy_file(rdir.buf, wtdir.buf,
+					if (copy_file(repo, rdir.buf, wtdir.buf,
 						      st.st_mode)) {
 						ret = error("could not copy '%s' to '%s'", wtdir.buf, rdir.buf);
 						goto finish;
@@ -636,7 +636,7 @@ static int run_dir_diff(struct repository *repo,
 			struct lock_file lock = LOCK_INIT;
 			strbuf_reset(&buf);
 			strbuf_addf(&buf, "%s/wtindex", tmpdir.buf);
-			if (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 ||
+			if (repo_hold_lock_file_for_update(repo, &lock, buf.buf, 0) < 0 ||
 			    write_locked_index(&wtindex, &lock, COMMIT_LOCK)) {
 				ret = error("could not write %s", buf.buf);
 				goto finish;
@@ -658,7 +658,7 @@ static int run_dir_diff(struct repository *repo,
 				warning("%s", "");
 				err = 1;
 			} else if (unlink(wtdir.buf) ||
-				   copy_file(wtdir.buf, rdir.buf, st.st_mode))
+				   copy_file(repo, wtdir.buf, rdir.buf, st.st_mode))
 				warning_errno(_("could not copy '%s' to '%s'"),
 					      rdir.buf, wtdir.buf);
 		}
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index 0be4310..50278be 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -33,7 +33,7 @@
 #include "gpg-interface.h"
 
 static const char *const fast_export_usage[] = {
-	N_("git fast-export [<rev-list-opts>]"),
+	N_("git fast-export [<options>] [<revision-range>] [[--] <path>...]"),
 	NULL
 };
 
@@ -51,7 +51,7 @@ static int show_original_ids;
 static int mark_tags;
 static struct string_list extra_refs = STRING_LIST_INIT_DUP;
 static struct string_list tag_refs = STRING_LIST_INIT_DUP;
-static struct refspec refspecs = REFSPEC_INIT_FETCH;
+static struct refspec refspecs;
 static int anonymize;
 static struct hashmap anonymized_seeds;
 static struct revision_sources revision_sources;
@@ -1372,6 +1372,8 @@ int cmd_fast_export(int argc,
 	/* we handle encodings */
 	repo_config(the_repository, git_default_config, NULL);
 
+	refspec_init_fetch(&refspecs, the_hash_algo);
+
 	repo_init_revisions(the_repository, &revs, prefix);
 	init_revision_sources(&revision_sources);
 	revs.topo_order = 1;
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index aa656c5..6692f7c 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -969,7 +969,7 @@ static int store_object(
 
 	hdrlen = format_object_header((char *)hdr, sizeof(hdr), type,
 				      dat->len);
-	the_hash_algo->init_fn(&c);
+	git_hash_init(&c, the_hash_algo);
 	git_hash_update(&c, hdr, hdrlen);
 	git_hash_update(&c, dat->buf, dat->len);
 	git_hash_final_oid(&oid, &c);
@@ -1131,7 +1131,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
 
 	hdrlen = format_object_header((char *)out_buf, out_sz, OBJ_BLOB, len);
 
-	the_hash_algo->init_fn(&c);
+	git_hash_init(&c, the_hash_algo);
 	git_hash_update(&c, out_buf, hdrlen);
 
 	crc32_begin(pack_file);
@@ -1216,6 +1216,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
 out:
 	free(in_buf);
 	free(out_buf);
+	hashfile_checkpoint_release(&checkpoint);
 }
 
 /* All calls must be guarded by find_object() or find_mark() to
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 8e676b7..ab7db2b 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -96,7 +96,7 @@ static struct string_list deepen_not = STRING_LIST_INIT_NODUP;
 static struct strbuf default_rla = STRBUF_INIT;
 static struct transport *gtransport;
 static struct transport *gsecondary;
-static struct refspec refmap = REFSPEC_INIT_FETCH;
+static struct refspec refmap;
 static struct string_list server_options = STRING_LIST_INIT_DUP;
 static struct string_list negotiation_restrict = STRING_LIST_INIT_NODUP;
 static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;
@@ -601,7 +601,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		struct refspec_item tag_refspec;
 
 		/* also fetch all tags */
-		refspec_item_init_push(&tag_refspec, TAG_REFSPEC);
+		refspec_item_init_push(&tag_refspec, TAG_REFSPEC,
+				       the_hash_algo);
 		get_fetch_map(remote_refs, &tag_refspec, &tail, 0);
 		refspec_item_clear(&tag_refspec);
 	} else if (tags == TAGS_DEFAULT && *autotags) {
@@ -1839,7 +1840,7 @@ static void ref_transaction_rejection_handler(const char *refname,
 {
 	struct ref_rejection_data *data = cb_data;
 
-	if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case &&
+	if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && repo_ignore_case(the_repository) &&
 	    !data->case_sensitive_msg_shown) {
 		error(_("You're on a case-insensitive filesystem, and the remote you are\n"
 			"trying to fetch from has references that only differ in casing. It\n"
@@ -2428,7 +2429,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
 		     const struct fetch_config *config,
 		     struct list_objects_filter_options *filter_options)
 {
-	struct refspec rs = REFSPEC_INIT_FETCH;
+	struct refspec rs = REFSPEC_INIT_FETCH(the_hash_algo);
 	int i;
 	int exit_code;
 	int maybe_prune_tags;
@@ -2630,6 +2631,8 @@ int cmd_fetch(int argc,
 
 	filter_options.allow_auto_filter = 1;
 
+	refspec_init_fetch(&refmap, the_hash_algo);
+
 	packet_trace_identity("fetch");
 
 	/* Record the command line for the reflog */
diff --git a/builtin/fsck.c b/builtin/fsck.c
index 76b723f..a6c054e 100644
--- a/builtin/fsck.c
+++ b/builtin/fsck.c
@@ -632,7 +632,7 @@ static void snapshot_refs(struct repository *repo,
 	refs_for_each_ref_ext(get_main_ref_store(repo),
 			      snapshot_ref, &data, &opts);
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(repo);
 	for (p = worktrees; *p; p++) {
 		struct worktree *wt = *p;
 		struct strbuf refname = STRBUF_INIT;
@@ -685,7 +685,7 @@ static void process_refs(struct repository *repo, struct snapshot *snap)
 	}
 
 	if (include_reflogs) {
-		worktrees = get_worktrees();
+		worktrees = get_worktrees(repo);
 		for (p = worktrees; *p; p++) {
 			struct worktree *wt = *p;
 
@@ -1121,7 +1121,7 @@ int cmd_fsck(int argc,
 		verify_index_checksum = 1;
 		verify_ce_order = 1;
 
-		worktrees = get_worktrees();
+		worktrees = get_worktrees(repo);
 		for (p = worktrees; *p; p++) {
 			struct worktree *wt = *p;
 			struct index_state istate =
diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c
index f920cf3..4161dd8 100644
--- a/builtin/fsmonitor--daemon.c
+++ b/builtin/fsmonitor--daemon.c
@@ -1418,6 +1418,8 @@ static int fsmonitor_run_daemon(void)
 	err = fsmonitor_run_daemon_1(&state);
 
 done:
+	fsmonitor_free_token_data(state.current_token_data);
+	state.current_token_data = NULL;
 	pthread_cond_destroy(&state.cookies_cond);
 	pthread_mutex_destroy(&state.main_lock);
 	{
diff --git a/builtin/gc.c b/builtin/gc.c
index ac1a21e..de2f9e7 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -371,7 +371,7 @@ static int worktree_prune_condition(struct gc_config *cfg)
 	while (limit && (d = readdir_skip_dot_and_dotdot(dir))) {
 		char *wtpath;
 		strbuf_reset(&buf);
-		if (should_prune_worktree(d->d_name, &buf, &wtpath, expiry_date))
+		if (should_prune_worktree(the_repository, d->d_name, &buf, &wtpath, expiry_date))
 			limit--;
 		free(wtpath);
 	}
@@ -1414,7 +1414,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,
 	char *lock_path = xstrfmt("%s/maintenance", r->objects->sources->path);
 	enum auto_gc_hook_result auto_gc_hook_result = AUTO_GC_HOOK_UNDECIDED;
 
-	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
+	if (repo_hold_lock_file_for_update(r, &lk, lock_path, LOCK_NO_DEREF) < 0) {
 		/*
 		 * Another maintenance command is running.
 		 *
diff --git a/builtin/grep.c b/builtin/grep.c
index 77f0c4d..d3d86ab 100644
--- a/builtin/grep.c
+++ b/builtin/grep.c
@@ -25,12 +25,11 @@
 #include "setup.h"
 #include "submodule.h"
 #include "submodule-config.h"
-#include "object-file.h"
 #include "object-name.h"
 #include "odb.h"
+#include "odb/source.h"
 #include "oid-array.h"
 #include "oidset.h"
-#include "packfile.h"
 #include "pager.h"
 #include "path.h"
 #include "promisor-remote.h"
@@ -1357,15 +1356,8 @@ int cmd_grep(int argc,
 		if (recurse_submodules)
 			repo_read_gitmodules(the_repository, 1);
 
-		if (startup_info->have_repository) {
-			struct odb_source *source;
-
-			odb_prepare_alternates(the_repository->objects);
-			for (source = the_repository->objects->sources; source; source = source->next) {
-				struct odb_source_files *files = odb_source_files_downcast(source);
-				odb_source_packed_prepare(files->packed);
-			}
-		}
+		if (startup_info->have_repository)
+			odb_prepare(the_repository->objects, 0);
 
 		start_threads(&opt);
 	} else {
diff --git a/builtin/history.c b/builtin/history.c
index fd83de8..000155a 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -17,13 +17,17 @@
 #include "read-cache.h"
 #include "refs.h"
 #include "replay.h"
+#include "reset.h"
 #include "revision.h"
 #include "sequencer.h"
 #include "strvec.h"
 #include "tree.h"
+#include "tree-walk.h"
 #include "unpack-trees.h"
 #include "wt-status.h"
 
+#define GIT_HISTORY_DROP_USAGE \
+	N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
 #define GIT_HISTORY_FIXUP_USAGE \
 	N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
 #define GIT_HISTORY_REWORD_USAGE \
@@ -52,11 +56,6 @@ static int fill_commit_message(struct repository *repo,
 		  " empty message aborts the commit.\n");
 	struct wt_status s;
 
-	strbuf_addstr(out, default_message);
-	strbuf_addch(out, '\n');
-	strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
-	write_file_buf(path, out->buf, out->len);
-
 	wt_status_prepare(repo, &s);
 	FREE_AND_NULL(s.branch);
 	s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
@@ -68,14 +67,22 @@ static int fill_commit_message(struct repository *repo,
 	s.whence = FROM_COMMIT;
 	s.committable = 1;
 
-	s.fp = fopen(git_path_commit_editmsg(), "a");
+	s.fp = fopen(path, "w");
 	if (!s.fp)
-		return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+		return error_errno(_("could not open '%s'"), path);
+
+	strbuf_addstr(out, default_message);
+	strbuf_addch(out, '\n');
+	strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+	if (fwrite(out->buf, 1, out->len, s.fp) != out->len)
+		die_errno(_("could not write to '%s'"), path);
 
 	wt_status_collect_changes_trees(&s, old_tree, new_tree);
 	wt_status_print(&s);
 	wt_status_collect_free_buffers(&s);
 	string_list_clear_func(&s.change, change_data_free);
+	if (fclose(s.fp))
+		die_errno(_("could not write to '%s'"), path);
 
 	strbuf_reset(out);
 	if (launch_editor(path, out, NULL)) {
@@ -333,21 +340,17 @@ static int handle_ref_update(struct ref_transaction *transaction,
 				      NULL, NULL, 0, reflog_msg, err);
 }
 
-static int handle_reference_updates(struct rev_info *revs,
-				    enum ref_action action,
-				    struct commit *original,
-				    struct commit *rewritten,
-				    const char *reflog_msg,
-				    int dry_run,
-				    enum replay_empty_commit_action empty)
+static int compute_pending_ref_updates(struct rev_info *revs,
+				       enum ref_action action,
+				       struct commit *original,
+				       struct commit *rewritten,
+				       enum replay_empty_commit_action empty,
+				       struct replay_result *result)
 {
 	const struct name_decoration *decoration;
 	struct replay_revisions_options opts = {
 		.empty = empty,
 	};
-	struct replay_result result = { 0 };
-	struct ref_transaction *transaction = NULL;
-	struct strbuf err = STRBUF_INIT;
 	char hex[GIT_MAX_HEXSZ + 1];
 	bool detached_head;
 	int head_flags = 0;
@@ -359,34 +362,13 @@ static int handle_reference_updates(struct rev_info *revs,
 
 	opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
 
-	ret = replay_revisions(revs, &opts, &result);
+	ret = replay_revisions(revs, &opts, result);
 	if (ret)
-		goto out;
+		return ret;
 
 	if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
 		BUG("unsupported ref action %d", action);
 
-	if (!dry_run) {
-		transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
-		if (!transaction) {
-			ret = error(_("failed to begin ref transaction: %s"), err.buf);
-			goto out;
-		}
-	}
-
-	for (size_t i = 0; i < result.updates_nr; i++) {
-		ret = handle_ref_update(transaction,
-					result.updates[i].refname,
-					&result.updates[i].new_oid,
-					&result.updates[i].old_oid,
-					reflog_msg, &err);
-		if (ret) {
-			ret = error(_("failed to update ref '%s': %s"),
-				    result.updates[i].refname, err.buf);
-			goto out;
-		}
-	}
-
 	/*
 	 * `replay_revisions()` only updates references that are
 	 * ancestors of `rewritten`, so we need to manually
@@ -414,14 +396,41 @@ static int handle_reference_updates(struct rev_info *revs,
 		    !detached_head)
 			continue;
 
+		replay_result_queue_update(result, decoration->name,
+					   &original->object.oid,
+					   &rewritten->object.oid);
+	}
+
+	return 0;
+}
+
+static int apply_pending_ref_updates(struct repository *repo,
+				     const struct replay_result *result,
+				     const char *reflog_msg,
+				     int dry_run)
+{
+	struct ref_transaction *transaction = NULL;
+	struct strbuf err = STRBUF_INIT;
+	int ret;
+
+	if (!dry_run) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"), err.buf);
+			goto out;
+		}
+	}
+
+	for (size_t i = 0; i < result->updates_nr; i++) {
 		ret = handle_ref_update(transaction,
-					decoration->name,
-					&rewritten->object.oid,
-					&original->object.oid,
+					result->updates[i].refname,
+					&result->updates[i].new_oid,
+					&result->updates[i].old_oid,
 					reflog_msg, &err);
 		if (ret) {
 			ret = error(_("failed to update ref '%s': %s"),
-				    decoration->name, err.buf);
+				    result->updates[i].refname, err.buf);
 			goto out;
 		}
 	}
@@ -435,11 +444,33 @@ static int handle_reference_updates(struct rev_info *revs,
 
 out:
 	ref_transaction_free(transaction);
-	replay_result_release(&result);
 	strbuf_release(&err);
 	return ret;
 }
 
+static int handle_reference_updates(struct rev_info *revs,
+				    enum ref_action action,
+				    struct commit *original,
+				    struct commit *rewritten,
+				    const char *reflog_msg,
+				    int dry_run,
+				    enum replay_empty_commit_action empty)
+{
+	struct replay_result result = { 0 };
+	int ret;
+
+	ret = compute_pending_ref_updates(revs, action, original, rewritten,
+					  empty, &result);
+	if (ret)
+		goto out;
+
+	ret = apply_pending_ref_updates(revs->repo, &result, reflog_msg, dry_run);
+
+out:
+	replay_result_release(&result);
+	return ret;
+}
+
 static int commit_became_empty(struct repository *repo,
 			       struct commit *original,
 			       struct tree *result)
@@ -764,7 +795,7 @@ static int write_ondisk_index(struct repository *repo,
 
 	prime_cache_tree(repo, &index, tree);
 
-	if (hold_lock_file_for_update(&lock, path, 0) < 0) {
+	if (repo_hold_lock_file_for_update(repo, &lock, path, 0) < 0) {
 		ret = error_errno(_("unable to acquire index lock"));
 		goto out;
 	}
@@ -975,12 +1006,191 @@ static int cmd_history_split(int argc,
 	return ret;
 }
 
+static int update_worktree(struct repository *repo,
+			   const struct commit *old_head,
+			   const struct commit *new_head,
+			   bool dry_run)
+{
+	struct reset_working_tree_options opts = {
+		.oid_from = &old_head->object.oid,
+		.oid = &new_head->object.oid,
+	};
+	if (dry_run)
+		opts.flags |= RESET_WORKING_TREE_DRY_RUN;
+	return reset_working_tree(repo, &opts);
+}
+
+static int find_head_tree_change(struct repository *repo,
+				 const struct replay_result *result,
+				 struct commit **old_head,
+				 struct commit **new_head,
+				 bool *changed)
+{
+	const struct replay_ref_update *head_update = NULL;
+	struct commit *old_head_commit, *new_head_commit;
+	struct tree *old_head_tree, *new_head_tree;
+	const char *head_target;
+	int head_flags;
+
+	*changed = false;
+
+	head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo), "HEAD",
+					      RESOLVE_REF_NO_RECURSE | RESOLVE_REF_READING,
+					      NULL, &head_flags);
+	if (!head_target)
+		return error(_("cannot look up HEAD"));
+
+	for (size_t i = 0; i < result->updates_nr; i++) {
+		if (!strcmp(result->updates[i].refname, head_target)) {
+			head_update = &result->updates[i];
+			break;
+		}
+	}
+
+	if (!head_update)
+		return 0;
+
+	old_head_commit = lookup_commit_reference(repo, &head_update->old_oid);
+	new_head_commit = lookup_commit_reference(repo, &head_update->new_oid);
+	if (!old_head_commit || !new_head_commit)
+		return error(_("cannot resolve HEAD commit"));
+
+	old_head_tree = repo_get_commit_tree(repo, old_head_commit);
+	new_head_tree = repo_get_commit_tree(repo, new_head_commit);
+	if (!old_head_tree || !new_head_tree)
+		return error(_("cannot resolve tree for HEAD"));
+
+	if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid))
+		return 0;
+
+	*old_head = old_head_commit;
+	*new_head = new_head_commit;
+	*changed = true;
+
+	return 0;
+}
+
+static int cmd_history_drop(int argc,
+			    const char **argv,
+			    const char *prefix,
+			    struct repository *repo)
+{
+	const char * const usage[] = {
+		GIT_HISTORY_DROP_USAGE,
+		NULL,
+	};
+	enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
+	enum ref_action action = REF_ACTION_DEFAULT;
+	int dry_run = 0;
+	struct option options[] = {
+		OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+			       N_("control which refs should be updated"),
+			       PARSE_OPT_NONEG, parse_ref_action),
+		OPT_BOOL('n', "dry-run", &dry_run,
+			 N_("perform a dry-run without updating any refs")),
+		OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
+			       N_("how to handle descendants that become empty"),
+			       PARSE_OPT_NONEG, parse_opt_empty),
+		OPT_END(),
+	};
+	struct strbuf reflog_msg = STRBUF_INIT;
+	struct commit *original, *rewritten;
+	struct rev_info revs = { 0 };
+	struct replay_result result = { 0 };
+	struct commit *old_head, *new_head;
+	bool head_moves = false;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, options, usage, 0);
+	if (argc != 1) {
+		ret = error(_("command expects a single revision"));
+		goto out;
+	}
+	repo_config(repo, git_default_config, NULL);
+
+	if (action == REF_ACTION_DEFAULT)
+		action = REF_ACTION_BRANCHES;
+
+	original = lookup_commit_reference_by_name(argv[0]);
+	if (!original) {
+		ret = error(_("commit cannot be found: %s"), argv[0]);
+		goto out;
+	}
+
+	if (!original->parents) {
+		ret = error(_("cannot drop root commit %s: "
+			      "it has no parent to replay onto"),
+			    argv[0]);
+		goto out;
+	} else if (original->parents->next) {
+		ret = error(_("cannot drop merge commit: %s"), argv[0]);
+		goto out;
+	}
+
+	ret = setup_revwalk(repo, action, original, &revs);
+	if (ret)
+		goto out;
+
+	rewritten = original->parents->item;
+
+	ret = compute_pending_ref_updates(&revs, action, original, rewritten,
+					  empty, &result);
+	if (ret) {
+		ret = error(_("failed replaying descendants"));
+		goto out;
+	}
+
+	/*
+	 * If HEAD will move as a result of the rewrite then we'll have to
+	 * merge in the changes into the worktree and index. This merge can of
+	 * course conflict, which will cause the whole operation to abort.
+	 *
+	 * If we had already updated the refs at that point then we'd have an
+	 * inconsistent repository state. So we first perform a dry-run merge
+	 * here before updating refs.
+	 */
+	if (!is_bare_repository(repo)) {
+		ret = find_head_tree_change(repo, &result, &old_head,
+					    &new_head, &head_moves);
+		if (ret < 0)
+			goto out;
+
+		if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) {
+			ret = error(_("dropping this commit would "
+				      "overwrite local changes; aborting"));
+			goto out;
+		}
+	}
+
+	strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]);
+	ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run);
+	if (ret < 0) {
+		ret = error(_("failed to update references"));
+		goto out;
+	}
+
+	if (!dry_run && head_moves && update_worktree(repo, old_head, new_head, false) < 0) {
+		ret = error(_("could not update working tree to new commit %s"),
+			    oid_to_hex(&new_head->object.oid));
+		goto out;
+	}
+
+	ret = 0;
+
+out:
+	replay_result_release(&result);
+	strbuf_release(&reflog_msg);
+	release_revisions(&revs);
+	return ret;
+}
+
 int cmd_history(int argc,
 		const char **argv,
 		const char *prefix,
 		struct repository *repo)
 {
 	const char * const usage[] = {
+		GIT_HISTORY_DROP_USAGE,
 		GIT_HISTORY_FIXUP_USAGE,
 		GIT_HISTORY_REWORD_USAGE,
 		GIT_HISTORY_SPLIT_USAGE,
@@ -988,6 +1198,7 @@ int cmd_history(int argc,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
 		OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
 		OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
 		OPT_SUBCOMMAND("split", &fn, cmd_history_split),
diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index f396658..bc86925 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -374,7 +374,7 @@ static const char *open_pack_file(const char *pack_name)
 		output_fd = -1;
 		nothread_data.pack_fd = input_fd;
 	}
-	the_hash_algo->init_fn(&input_ctx);
+	git_hash_init(&input_ctx, the_hash_algo);
 	return pack_name;
 }
 
@@ -481,7 +481,7 @@ static void *unpack_entry_data(off_t offset, size_t size,
 
 	if (!is_delta_type(type)) {
 		hdrlen = format_object_header(hdr, sizeof(hdr), type, size);
-		the_hash_algo->init_fn(&c);
+		git_hash_init(&c, the_hash_algo);
 		git_hash_update(&c, hdr, hdrlen);
 	} else
 		oid = NULL;
@@ -1291,7 +1291,7 @@ static void parse_pack_objects(unsigned char *hash)
 
 	/* Check pack integrity */
 	flush();
-	the_hash_algo->init_fn(&tmp_ctx);
+	git_hash_init(&tmp_ctx, the_hash_algo);
 	git_hash_clone(&tmp_ctx, &input_ctx);
 	git_hash_final(hash, &tmp_ctx);
 	if (!hasheq(fill(the_hash_algo->rawsz), hash, the_repository->hash_algo))
@@ -1825,11 +1825,16 @@ static void repack_local_links(void)
 
 	oidset_iter_init(&outgoing_links, &iter);
 	while ((oid = oidset_iter_next(&iter))) {
-		struct object_info info = OBJECT_INFO_INIT;
+		struct odb_source_info source_info;
+		struct object_info info = {
+			.source_infop = &source_info,
+		};
+
 		if (odb_read_object_info_extended(the_repository->objects, oid, &info, 0))
 			/* Missing; assume it is a promisor object */
 			continue;
-		if (info.whence == OI_PACKED && info.u.packed.pack->pack_promisor)
+		if (source_info.source->type == ODB_SOURCE_PACKED &&
+		    source_info.u.packed.pack->pack_promisor)
 			continue;
 
 		if (!cmd.args.nr) {
diff --git a/builtin/init-db.c b/builtin/init-db.c
index 566732c..e96b128 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -231,39 +231,25 @@ int cmd_init_db(int argc,
 	if (!bare) {
 		const char *git_dir_parent = strrchr(git_dir, '/');
 
-		if (work_tree) {
-			set_git_work_tree(the_repository, work_tree);
-		} else {
-			char *work_tree_cfg = NULL;
-
+		if (!work_tree) {
 			if (git_dir_parent) {
 				char *rel = xstrndup(git_dir, git_dir_parent - git_dir);
-				work_tree_cfg = real_pathdup(rel, 1);
+				work_tree = real_pathdup(rel, 1);
 				free(rel);
+			} else {
+				work_tree = xgetcwd();
 			}
-
-			if (!work_tree_cfg)
-				work_tree_cfg = xgetcwd();
-
-			set_git_work_tree(the_repository, work_tree_cfg);
-
-			free(work_tree_cfg);
 		}
 
-		if (access(repo_get_work_tree(the_repository), X_OK))
-			die_errno (_("Cannot access work tree '%s'"),
-				   repo_get_work_tree(the_repository));
-	}
-	else {
-		if (real_git_dir)
-			die(_("--separate-git-dir incompatible with bare repository"));
-		if (work_tree)
-			set_git_work_tree(the_repository, work_tree);
+		if (access(work_tree, X_OK))
+			die_errno (_("Cannot access work tree '%s'"), work_tree);
+	} else if (real_git_dir) {
+		die(_("--separate-git-dir incompatible with bare repository"));
 	}
 
 	flags |= INIT_DB_EXIST_OK;
-	ret = init_db(the_repository, git_dir, real_git_dir, template_dir, hash_algo,
-		      ref_storage_format, initial_branch,
+	ret = init_db(the_repository, git_dir, real_git_dir, work_tree,
+		      template_dir, hash_algo, ref_storage_format, initial_branch,
 		      init_shared_repository, flags);
 
 	free(template_dir_to_free);
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index 8900cee..5478182 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -344,6 +344,7 @@ static void process_parent(struct last_modified *lm,
 static int last_modified_run(struct last_modified *lm)
 {
 	int max_count, queue_popped = 0;
+	struct commit *c, *n;
 	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
 	struct prio_queue not_queue = { compare_commits_by_gen_then_commit_date };
 	struct commit_list *list;
@@ -389,10 +390,9 @@ static int last_modified_run(struct last_modified *lm)
 		}
 	}
 
-	while (queue.nr) {
+	while ((c = prio_queue_get(&queue))) {
 		int parent_i;
 		struct commit_list *p;
-		struct commit *c = prio_queue_get(&queue);
 		struct bitmap *active_c = active_paths_for(lm, c);
 
 		if ((0 <= max_count && max_count < ++queue_popped) ||
@@ -416,9 +416,8 @@ static int last_modified_run(struct last_modified *lm)
 		 */
 		repo_parse_commit(lm->rev.repo, c);
 
-		while (not_queue.nr) {
+		while ((n = prio_queue_get(&not_queue))) {
 			struct commit_list *np;
-			struct commit *n = prio_queue_get(&not_queue);
 
 			repo_parse_commit(lm->rev.repo, n);
 
diff --git a/builtin/log.c b/builtin/log.c
index d027ce1..350b35c 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -1888,6 +1888,7 @@ static void prepare_bases(struct base_tree_info *bases,
 		bases->nr_patch_id++;
 	}
 	clear_commit_base(&commit_base);
+	release_revisions(&revs);
 }
 
 static void print_bases(struct base_tree_info *bases, FILE *file)
diff --git a/builtin/mailsplit.c b/builtin/mailsplit.c
index 264df62..0993418 100644
--- a/builtin/mailsplit.c
+++ b/builtin/mailsplit.c
@@ -225,14 +225,14 @@ static int split_mbox(const char *file, const char *dir, int allow_bare,
 	FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r");
 	int file_done = 0;
 
-	if (isatty(fileno(f)))
-		warning(_("reading patches from stdin/tty..."));
-
 	if (!f) {
 		error_errno("cannot open mbox %s", file);
 		goto out;
 	}
 
+	if (isatty(fileno(f)))
+		warning(_("reading patches from stdin/tty..."));
+
 	do {
 		peek = fgetc(f);
 		if (peek == EOF) {
diff --git a/builtin/merge.c b/builtin/merge.c
index 5b46a59..58d1b7b 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -553,7 +553,7 @@ static void merge_name(const char *remote, struct strbuf *msg)
 	char *found_ref = NULL;
 	int len, early;
 
-	copy_branchname(&bname, remote, 0);
+	copy_branchname(the_repository, &bname, remote, 0);
 	remote = bname.buf;
 
 	oidclr(&branch_head, the_repository->hash_algo);
diff --git a/builtin/mv.c b/builtin/mv.c
index e038233..a82fc97 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -419,7 +419,7 @@ int cmd_mv(int argc,
 			goto act_on_entry;
 		}
 		if (lstat(dst, &st) == 0 &&
-		    (!ignore_case || strcasecmp(src, dst))) {
+		    (!repo_ignore_case(the_repository) || strcasecmp(src, dst))) {
 			bad = _("destination exists");
 			if (force) {
 				/*
diff --git a/builtin/notes.c b/builtin/notes.c
index 962df86..9f1f0ec 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -989,7 +989,7 @@ static int merge(int argc, const char **argv, const char *prefix,
 				"NOTES_MERGE_PARTIAL", &result_oid, NULL,
 				0, UPDATE_REFS_DIE_ON_ERR);
 		/* Store ref-to-be-updated into .git/NOTES_MERGE_REF */
-		worktrees = get_worktrees();
+		worktrees = get_worktrees(the_repository);
 		wt = find_shared_symref(worktrees, "NOTES_MERGE_REF",
 					notes_ref);
 		if (wt)
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index e3760b3..1ec5b6f 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -32,6 +32,7 @@
 #include "list.h"
 #include "packfile.h"
 #include "object-file.h"
+#include "object-file-convert.h"
 #include "odb.h"
 #include "odb/streaming.h"
 #include "replace-object.h"
@@ -1867,8 +1868,8 @@ static const char no_closure_warning[] = N_(
 "disabling bitmap writing, as some objects are not being packed"
 );
 
-static int add_object_entry(const struct object_id *oid, enum object_type type,
-			    const char *name, int exclude)
+static void add_object_entry(const struct object_id *oid, enum object_type type,
+			     const char *name, int exclude)
 {
 	struct packed_git *found_pack = NULL;
 	off_t found_offset = 0;
@@ -1876,7 +1877,7 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,
 	display_progress(progress_state, ++nr_seen);
 
 	if (have_duplicate_entry(oid, exclude))
-		return 0;
+		return;
 
 	if (!want_object_in_pack(oid, exclude, &found_pack, &found_offset)) {
 		/* The pack is missing an object, so it will not have closure */
@@ -1885,13 +1886,12 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,
 				warning(_(no_closure_warning));
 			write_bitmap_index = 0;
 		}
-		return 0;
+		return;
 	}
 
 	create_object_entry(oid, type, pack_name_hash_fn(name),
 			    exclude, name && no_try_delta(name),
 			    found_pack, found_offset);
-	return 1;
 }
 
 static int add_object_entry_from_bitmap(const struct object_id *oid,
@@ -1909,7 +1909,7 @@ static int add_object_entry_from_bitmap(const struct object_id *oid,
 		return 0;
 
 	create_object_entry(oid, type, name_hash, 0, 0, pack, offset);
-	return 1;
+	return 0;
 }
 
 struct pbase_tree_cache {
@@ -2463,7 +2463,7 @@ static void drop_reused_delta(struct object_entry *entry)
 
 	oi.sizep = &size;
 	oi.typep = &type;
-	if (packed_object_info(IN_PACK(entry), entry->in_pack_offset, &oi) < 0) {
+	if (packed_object_info(NULL, IN_PACK(entry), entry->in_pack_offset, &oi) < 0) {
 		/*
 		 * We failed to get the info from this pack for some reason;
 		 * fall back to odb_read_object_info, which may find another copy.
@@ -3820,7 +3820,7 @@ static int add_object_entry_from_pack(const struct object_id *oid,
 	ofs = nth_packed_object_offset(p, pos);
 
 	oi.typep = &type;
-	if (packed_object_info(p, ofs, &oi) < 0) {
+	if (packed_object_info(NULL, p, ofs, &oi) < 0) {
 		die(_("could not get type of object %s in pack %s"),
 		    oid_to_hex(oid), p->pack_name);
 	} else if (type == OBJ_COMMIT) {
@@ -4495,8 +4495,9 @@ static int add_object_in_unpacked_pack(const struct object_id *oid,
 				       void *data UNUSED)
 {
 	if (cruft) {
-		add_cruft_object_entry(oid, OBJ_NONE, oi->u.packed.pack,
-				       oi->u.packed.offset, NULL, *oi->mtimep);
+		add_cruft_object_entry(oid, OBJ_NONE, oi->source_infop->u.packed.pack,
+				       oi->source_infop->u.packed.offset, NULL,
+				       *oi->mtimep);
 	} else {
 		add_object_entry(oid, OBJ_NONE, "", 0);
 	}
@@ -4513,8 +4514,10 @@ static void add_objects_in_unpacked_packs(void)
 			 ODB_FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS |
 			 ODB_FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS,
 	};
+	struct odb_source_info source_info;
 	struct object_info oi = {
 		.mtimep = &mtime,
+		.source_infop = &source_info,
 	};
 
 	odb_prepare_alternates(to_pack.repo->objects);
@@ -4619,6 +4622,51 @@ static int loosened_object_can_be_discarded(const struct object_id *oid,
 	return 1;
 }
 
+static int force_object_loose(struct odb_source *source,
+			      const struct object_id *oid,
+			      const time_t *mtime)
+{
+	struct odb_source_files *files = odb_source_files_downcast(source);
+	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+	struct object_info oi = OBJECT_INFO_INIT;
+	struct object_id compat_oid, *compat_oid_p = NULL;
+	enum object_type type;
+	void *buf = NULL;
+	size_t len;
+	int ret;
+
+	for (struct odb_source *s = source->odb->sources; s; s = s->next) {
+		struct odb_source_files *files = odb_source_files_downcast(s);
+		if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
+			return 0;
+	}
+
+	oi.typep = &type;
+	oi.sizep = &len;
+	oi.contentp = &buf;
+	if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {
+		ret = error(_("cannot read object for %s"), oid_to_hex(oid));
+		goto out;
+	}
+
+	if (compat) {
+		if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {
+			ret = error(_("cannot map object %s to %s"),
+				    oid_to_hex(oid), compat->name);
+			goto out;
+		}
+
+		compat_oid_p = &compat_oid;
+	}
+
+	ret = odb_source_write_object(&files->loose->base, buf, len, type, oid,
+				      compat_oid_p, mtime, 0);
+
+out:
+	free(buf);
+	return ret;
+}
+
 static void loosen_unused_packed_objects(void)
 {
 	struct packed_git *p;
@@ -4639,7 +4687,7 @@ static void loosen_unused_packed_objects(void)
 			    !has_sha1_pack_kept_or_nonlocal(&oid) &&
 			    !loosened_object_can_be_discarded(&oid, p->mtime)) {
 				if (force_object_loose(the_repository->objects->sources,
-						       &oid, p->mtime))
+						       &oid, &p->mtime))
 					die(_("unable to force loose object"));
 				loosened_objects_nr++;
 			}
@@ -5036,10 +5084,14 @@ static int option_parse_cruft_expiration(const struct option *opt UNUSED,
 
 static int is_not_in_promisor_pack_obj(struct object *obj, void *data UNUSED)
 {
-	struct object_info info = OBJECT_INFO_INIT;
+	struct odb_source_info source_info;
+	struct object_info info = {
+		.source_infop = &source_info,
+	};
+
 	if (odb_read_object_info_extended(the_repository->objects, &obj->oid, &info, 0))
 		BUG("should_include_obj should only be called on existing objects");
-	return info.whence != OI_PACKED || !info.u.packed.pack->pack_promisor;
+	return source_info.source->type != ODB_SOURCE_PACKED || !source_info.u.packed.pack->pack_promisor;
 }
 
 static int is_not_in_promisor_pack(struct commit *commit, void *data) {
diff --git a/builtin/patch-id.c b/builtin/patch-id.c
index 2781598..22f36ec 100644
--- a/builtin/patch-id.c
+++ b/builtin/patch-id.c
@@ -73,7 +73,7 @@ static size_t get_one_patchid(struct object_id *next_oid, struct object_id *resu
 	char pre_oid_str[GIT_MAX_HEXSZ + 1], post_oid_str[GIT_MAX_HEXSZ + 1];
 	struct git_hash_ctx ctx;
 
-	the_hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, the_hash_algo);
 	oidclr(result, the_repository->hash_algo);
 
 	while (strbuf_getwholeline(line_buf, stdin, '\n') != EOF) {
@@ -173,6 +173,7 @@ static size_t get_one_patchid(struct object_id *next_oid, struct object_id *resu
 		oidclr(next_oid, the_repository->hash_algo);
 
 	flush_one_hunk(result, &ctx);
+	git_hash_discard(&ctx);
 
 	return patchlen;
 }
diff --git a/builtin/pull.c b/builtin/pull.c
index d49b091..db3ee0a 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -612,7 +612,7 @@ static const char *get_tracking_branch(const char *remote, const char *refspec)
 	const char *spec_src;
 	const char *merge_branch;
 
-	if (!refspec_item_init_fetch(&spec, refspec))
+	if (!refspec_item_init_fetch(&spec, refspec, the_hash_algo))
 		die(_("invalid refspec '%s'"), refspec);
 	spec_src = spec.src;
 	if (!*spec_src || !strcmp(spec_src, "HEAD"))
diff --git a/builtin/push.c b/builtin/push.c
index 6021b71..2377b5a 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -8,6 +8,7 @@
 #include "advice.h"
 #include "branch.h"
 #include "config.h"
+#include "dir.h"
 #include "environment.h"
 #include "gettext.h"
 #include "hex.h"
@@ -65,7 +66,7 @@ static enum transport_family family;
 
 static struct push_cas_option cas;
 
-static struct refspec rs = REFSPEC_INIT_PUSH;
+static struct refspec rs;
 
 static struct string_list push_options_config = STRING_LIST_INIT_DUP;
 
@@ -73,6 +74,7 @@ static void refspec_append_mapped(struct refspec *refspec, const char *ref,
 				  struct remote *remote, struct ref *matched)
 {
 	const char *branch_name;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (remote->push.nr) {
 		struct refspec_item query = {
@@ -88,7 +90,7 @@ static void refspec_append_mapped(struct refspec *refspec, const char *ref,
 		}
 	}
 
-	if (push_default == PUSH_DEFAULT_UPSTREAM &&
+	if (cfg->push_default == PUSH_DEFAULT_UPSTREAM &&
 	    skip_prefix(matched->name, "refs/heads/", &branch_name)) {
 		struct branch *branch = branch_get(branch_name);
 		if (branch->merge_nr == 1 && branch->merge[0]->src) {
@@ -160,7 +162,7 @@ static NORETURN void die_push_simple(struct branch *branch,
 	 * Don't show advice for people who explicitly set
 	 * push.default.
 	 */
-	if (push_default == PUSH_DEFAULT_UNSPECIFIED)
+	if (cfg->push_default == PUSH_DEFAULT_UNSPECIFIED)
 		advice_pushdefault_maybe = _("\n"
 				 "To choose either option permanently, "
 				 "see push.default in 'git help config'.\n");
@@ -231,8 +233,9 @@ static void setup_default_push_refspecs(int *flags, struct remote *remote)
 	struct branch *branch;
 	const char *dst;
 	int same_remote;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	switch (push_default) {
+	switch (cfg->push_default) {
 	case PUSH_DEFAULT_MATCHING:
 		refspec_append(&rs, ":");
 		return;
@@ -252,7 +255,7 @@ static void setup_default_push_refspecs(int *flags, struct remote *remote)
 	dst = branch->refname;
 	same_remote = !strcmp(remote->name, remote_for_branch(branch, NULL));
 
-	switch (push_default) {
+	switch (cfg->push_default) {
 	default:
 	case PUSH_DEFAULT_UNSPECIFIED:
 	case PUSH_DEFAULT_SIMPLE:
@@ -662,6 +665,29 @@ static int push_multiple(struct string_list *list,
 	return result;
 }
 
+static void die_if_repo_looks_like_ref(const char *repo)
+{
+	const char *slash = strchr(repo, '/');
+	struct strbuf name = STRBUF_INIT;
+	int code;
+
+	if (!slash || !slash[1] || file_exists(repo))
+		return;
+
+	strbuf_add(&name, repo, slash - repo);
+	if (!remote_is_configured(remote_get(name.buf), 0)) {
+		strbuf_release(&name);
+		return;
+	}
+
+	code = die_message(_("'%s' is not a valid push target"), repo);
+	advise_if_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF,
+			  _("Did you mean to use: git push %s %s?"),
+			  name.buf, slash + 1);
+	strbuf_release(&name);
+	exit(code);
+}
+
 int cmd_push(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -725,6 +751,8 @@ int cmd_push(int argc,
 		: &push_options_config);
 	set_push_cert_flags(&flags, push_cert);
 
+	refspec_init_push(&rs, the_hash_algo);
+
 	die_for_incompatible_opt4(deleterefs, "--delete",
 				  tags, "--tags",
 				  flags & TRANSPORT_PUSH_ALL, "--all/--branches",
@@ -744,6 +772,17 @@ int cmd_push(int argc,
 
 	if (repo) {
 		if (!add_remote_or_group(repo, &remote_group)) {
+			struct remote *r;
+
+			/*
+			 * Check the advice up front to avoid the remote
+			 * lookup when the hint is off. The helper still
+			 * calls advise_if_enabled() so the hint carries the
+			 * standard "disable this message" instructions.
+			 */
+			if (advice_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF))
+				die_if_repo_looks_like_ref(repo);
+
 			/*
 			 * Not a configured remote name or group name.
 			 * Try treating it as a direct URL or path, e.g.
@@ -753,7 +792,7 @@ int cmd_push(int argc,
 			 * from the URL so the loop below can handle it
 			 * identically to a named remote.
 			 */
-			struct remote *r = pushremote_get(repo);
+			r = pushremote_get(repo);
 			if (!r)
 				die(_("bad repository '%s'"), repo);
 			string_list_append(&remote_group, r->name);
@@ -820,7 +859,7 @@ int cmd_push(int argc,
 			}
 
 			refspec_clear(&rs);
-			rs = (struct refspec) REFSPEC_INIT_PUSH;
+			rs = (struct refspec) REFSPEC_INIT_PUSH(the_hash_algo);
 
 			if (tags)
 				refspec_append(&rs, "refs/tags/*");
diff --git a/builtin/rebase.c b/builtin/rebase.c
index fa4f5d9..10a3063 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -592,7 +592,7 @@ static int finish_rebase(struct rebase_options *opts)
 static int move_to_original_branch(struct rebase_options *opts)
 {
 	struct strbuf branch_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT;
-	struct reset_head_opts ropts = { 0 };
+	struct reset_working_tree_options ropts = { 0 };
 	int ret;
 
 	if (!opts->head_name)
@@ -607,10 +607,11 @@ static int move_to_original_branch(struct rebase_options *opts)
 	strbuf_addf(&head_reflog, "%s (finish): returning to %s",
 		    opts->reflog_action, opts->head_name);
 	ropts.branch = opts->head_name;
-	ropts.flags = RESET_HEAD_REFS_ONLY;
+	ropts.flags = RESET_WORKING_TREE_REFS_ONLY |
+		      RESET_WORKING_TREE_UPDATE_HEAD;
 	ropts.branch_msg = branch_reflog.buf;
 	ropts.head_msg = head_reflog.buf;
-	ret = reset_head(the_repository, &ropts);
+	ret = reset_working_tree(the_repository, &ropts);
 
 	strbuf_release(&branch_reflog);
 	strbuf_release(&head_reflog);
@@ -685,7 +686,7 @@ static int run_am(struct rebase_options *opts)
 
 	status = run_command(&format_patch);
 	if (status) {
-		struct reset_head_opts ropts = { 0 };
+		struct reset_working_tree_options ropts = { 0 };
 		unlink(rebased_patches);
 		free(rebased_patches);
 		child_process_clear(&am);
@@ -693,7 +694,8 @@ static int run_am(struct rebase_options *opts)
 		ropts.oid = &opts->orig_head->object.oid;
 		ropts.branch = opts->head_name;
 		ropts.default_reflog_action = opts->reflog_action;
-		reset_head(the_repository, &ropts);
+		ropts.flags = RESET_WORKING_TREE_UPDATE_HEAD;
+		reset_working_tree(the_repository, &ropts);
 		error(_("\ngit encountered an error while preparing the "
 			"patches to replay\n"
 			"these revisions:\n"
@@ -855,18 +857,19 @@ static int rebase_config(const char *var, const char *value,
 static int checkout_up_to_date(struct rebase_options *options)
 {
 	struct strbuf buf = STRBUF_INIT;
-	struct reset_head_opts ropts = { 0 };
+	struct reset_working_tree_options ropts = { 0 };
 	int ret = 0;
 
 	strbuf_addf(&buf, "%s: checkout %s",
 		    options->reflog_action, options->switch_to);
 	ropts.oid = &options->orig_head->object.oid;
 	ropts.branch = options->head_name;
-	ropts.flags = RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
+	ropts.flags = RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK |
+		      RESET_WORKING_TREE_UPDATE_HEAD;
 	if (!ropts.branch)
-		ropts.flags |=  RESET_HEAD_DETACH;
+		ropts.flags |=  RESET_WORKING_TREE_DETACH;
 	ropts.head_msg = buf.buf;
-	if (reset_head(the_repository, &ropts) < 0)
+	if (reset_working_tree(the_repository, &ropts) < 0)
 		ret = error(_("could not switch to %s"), options->switch_to);
 	strbuf_release(&buf);
 
@@ -1116,7 +1119,7 @@ int cmd_rebase(int argc,
 	int reschedule_failed_exec = -1;
 	int allow_preemptive_ff = 1;
 	int preserve_merges_selected = 0;
-	struct reset_head_opts ropts = { 0 };
+	struct reset_working_tree_options ropts = { 0 };
 	struct option builtin_rebase_options[] = {
 		OPT_STRING(0, "onto", &options.onto_name,
 			   N_("revision"),
@@ -1384,8 +1387,9 @@ int cmd_rebase(int argc,
 
 		rerere_clear(the_repository, &merge_rr);
 		string_list_clear(&merge_rr, 1);
-		ropts.flags = RESET_HEAD_HARD;
-		if (reset_head(the_repository, &ropts) < 0)
+		ropts.flags = RESET_WORKING_TREE_HARD |
+			      RESET_WORKING_TREE_UPDATE_HEAD;
+		if (reset_working_tree(the_repository, &ropts) < 0)
 			die(_("could not discard worktree changes"));
 		remove_branch_state(the_repository, 0);
 		if (read_basic_state(&options))
@@ -1409,8 +1413,9 @@ int cmd_rebase(int argc,
 		ropts.oid = &options.orig_head->object.oid;
 		ropts.head_msg = head_msg.buf;
 		ropts.branch = options.head_name;
-		ropts.flags = RESET_HEAD_HARD;
-		if (reset_head(the_repository, &ropts) < 0)
+		ropts.flags = RESET_WORKING_TREE_HARD |
+			      RESET_WORKING_TREE_UPDATE_HEAD;
+		if (reset_working_tree(the_repository, &ropts) < 0)
 			die(_("could not move back to %s"),
 			    oid_to_hex(&options.orig_head->object.oid));
 		strbuf_release(&head_msg);
@@ -1876,11 +1881,13 @@ int cmd_rebase(int argc,
 		    options.reflog_action, options.onto_name);
 	ropts.oid = &options.onto->object.oid;
 	ropts.orig_head = &options.orig_head->object.oid;
-	ropts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD |
-			RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
+	ropts.flags = RESET_WORKING_TREE_DETACH |
+		      RESET_WORKING_TREE_UPDATE_HEAD |
+		      RESET_WORKING_TREE_UPDATE_ORIG_HEAD |
+		      RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK;
 	ropts.head_msg = msg.buf;
 	ropts.default_reflog_action = options.reflog_action;
-	if (reset_head(the_repository, &ropts)) {
+	if (reset_working_tree(the_repository, &ropts)) {
 		ret = error(_("Could not detach HEAD"));
 		goto cleanup_autostash;
 	}
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 19eb6a1..86933d8 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -37,7 +37,6 @@
 #include "sigchain.h"
 #include "string-list.h"
 #include "strvec.h"
-#include "tmp-objdir.h"
 #include "trace.h"
 #include "trace2.h"
 #include "version.h"
@@ -112,8 +111,6 @@ static enum {
 } use_keepalive;
 static int keepalive_in_sec = 5;
 
-static struct tmp_objdir *tmp_objdir;
-
 static struct proc_receive_ref {
 	unsigned int want_add:1,
 		     want_delete:1,
@@ -615,7 +612,7 @@ static void hmac_hash(unsigned char *out,
 	/* RFC 2104 2. (1) */
 	memset(key, '\0', GIT_MAX_BLKSZ);
 	if (the_hash_algo->blksz < key_len) {
-		the_hash_algo->init_fn(&ctx);
+		git_hash_init(&ctx, the_hash_algo);
 		git_hash_update(&ctx, key_in, key_len);
 		git_hash_final(key, &ctx);
 	} else {
@@ -629,13 +626,13 @@ static void hmac_hash(unsigned char *out,
 	}
 
 	/* RFC 2104 2. (3) & (4) */
-	the_hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, the_hash_algo);
 	git_hash_update(&ctx, k_ipad, sizeof(k_ipad));
 	git_hash_update(&ctx, text, text_len);
 	git_hash_final(out, &ctx);
 
 	/* RFC 2104 2. (6) & (7) */
-	the_hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, the_hash_algo);
 	git_hash_update(&ctx, k_opad, sizeof(k_opad));
 	git_hash_update(&ctx, out, the_hash_algo->rawsz);
 	git_hash_final(out, &ctx);
@@ -926,6 +923,7 @@ static void receive_hook_feed_state_free(void *data)
 static int run_receive_hook(struct command *commands,
 			    const char *hook_name,
 			    int skip_broken,
+			    struct odb_transaction *transaction,
 			    const struct string_list *push_options)
 {
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
@@ -959,8 +957,8 @@ static int run_receive_hook(struct command *commands,
 		strvec_push(&opt.env, "GIT_PUSH_OPTION_COUNT");
 	}
 
-	if (tmp_objdir)
-		strvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));
+	if (transaction)
+		odb_transaction_env(transaction, &opt.env);
 
 	prepare_push_cert_sha1(&opt);
 
@@ -1363,7 +1361,6 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 		    !delayed_reachability_test(si, i))
 			oid_array_append(&extra, &si->shallow->oid[i]);
 
-	opt.env = tmp_objdir_env(tmp_objdir);
 	setup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra);
 	if (check_connected(command_singleton_iterator, cmd, &opt)) {
 		rollback_shallow_file(the_repository, &shallow_lock);
@@ -1503,7 +1500,7 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 	struct object_id *old_oid = &cmd->old_oid;
 	struct object_id *new_oid = &cmd->new_oid;
 	int do_update_worktree = 0;
-	struct worktree **worktrees = get_worktrees();
+	struct worktree **worktrees = get_worktrees(the_repository);
 	const struct worktree *worktree =
 		find_shared_symref(worktrees, "HEAD", name);
 
@@ -1790,24 +1787,30 @@ static const struct object_id *command_singleton_iterator(void *cb_data)
 }
 
 static void set_connectivity_errors(struct command *commands,
-				    struct shallow_info *si)
+				    struct shallow_info *si,
+				    struct odb_transaction *transaction)
 {
 	struct command *cmd;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
 		struct command *singleton = cmd;
 		struct check_connected_options opt = CHECK_CONNECTED_INIT;
+		struct strvec env = STRVEC_INIT;
 
 		if (shallow_update && si->shallow_ref[cmd->index])
 			/* to be checked in update_shallow_ref() */
 			continue;
 
-		opt.env = tmp_objdir_env(tmp_objdir);
+		odb_transaction_env(transaction, &env);
+		opt.env = env.v;
+
 		if (!check_connected(command_singleton_iterator, &singleton,
 				     &opt))
 			continue;
 
 		cmd->error_string = "missing necessary objects";
+
+		strvec_clear(&env);
 	}
 }
 
@@ -2028,6 +2031,7 @@ static void execute_commands_atomic(struct command *commands,
 static void execute_commands(struct command *commands,
 			     const char *unpacker_error,
 			     struct shallow_info *si,
+			     struct odb_transaction *transaction,
 			     const struct string_list *push_options)
 {
 	struct check_connected_options opt = CHECK_CONNECTED_INIT;
@@ -2044,6 +2048,8 @@ static void execute_commands(struct command *commands,
 	}
 
 	if (!skip_connectivity_check) {
+		struct strvec env = STRVEC_INIT;
+
 		if (use_sideband) {
 			memset(&muxer, 0, sizeof(muxer));
 			muxer.proc = copy_to_sideband;
@@ -2057,14 +2063,17 @@ static void execute_commands(struct command *commands,
 		data.si = si;
 		opt.err_fd = err_fd;
 		opt.progress = err_fd && !quiet;
-		opt.env = tmp_objdir_env(tmp_objdir);
+		odb_transaction_env(transaction, &env);
+		opt.env = env.v;
 		opt.exclude_hidden_refs_section = "receive";
 
 		if (check_connected(iterate_receive_command_list, &data, &opt))
-			set_connectivity_errors(commands, si);
+			set_connectivity_errors(commands, si, transaction);
 
 		if (use_sideband)
 			finish_async(&muxer);
+
+		strvec_clear(&env);
 	}
 
 	reject_updates_to_hidden(commands);
@@ -2085,7 +2094,7 @@ static void execute_commands(struct command *commands,
 		}
 	}
 
-	if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
+	if (run_receive_hook(commands, "pre-receive", 0, transaction, push_options)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
 				cmd->error_string = "pre-receive hook declined";
@@ -2106,14 +2115,13 @@ static void execute_commands(struct command *commands,
 	 * Now we'll start writing out refs, which means the objects need
 	 * to be in their final positions so that other processes can see them.
 	 */
-	if (tmp_objdir_migrate(tmp_objdir) < 0) {
+	if (odb_transaction_commit(transaction)) {
 		for (cmd = commands; cmd; cmd = cmd->next) {
 			if (!cmd->error_string)
 				cmd->error_string = "unable to migrate objects to permanent storage";
 		}
 		return;
 	}
-	tmp_objdir = NULL;
 
 	check_aliased_updates(commands);
 
@@ -2326,7 +2334,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)
 		     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));
 }
 
-static const char *unpack(int err_fd, struct shallow_info *si)
+static const char *unpack(int err_fd, struct shallow_info *si,
+			  struct odb_transaction *transaction)
 {
 	struct pack_header hdr;
 	const char *hdr_err;
@@ -2351,20 +2360,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)
 		strvec_push(&child.args, alt_shallow_file);
 	}
 
-	tmp_objdir = tmp_objdir_create(the_repository, "incoming");
-	if (!tmp_objdir) {
-		if (err_fd > 0)
-			close(err_fd);
-		return "unable to create temporary object directory";
-	}
-	strvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));
-
-	/*
-	 * Normally we just pass the tmp_objdir environment to the child
-	 * processes that do the heavy lifting, but we may need to see these
-	 * objects ourselves to set up shallow information.
-	 */
-	tmp_objdir_add_as_alternate(tmp_objdir);
+	odb_transaction_env(transaction, &child.env);
 
 	if (ntohl(hdr.hdr_entries) < unpack_limit) {
 		strvec_push(&child.args, "unpack-objects");
@@ -2431,13 +2427,14 @@ static const char *unpack(int err_fd, struct shallow_info *si)
 	return NULL;
 }
 
-static const char *unpack_with_sideband(struct shallow_info *si)
+static const char *unpack_with_sideband(struct shallow_info *si,
+					struct odb_transaction *transaction)
 {
 	struct async muxer;
 	const char *ret;
 
 	if (!use_sideband)
-		return unpack(0, si);
+		return unpack(0, si, transaction);
 
 	use_keepalive = KEEPALIVE_AFTER_NUL;
 	memset(&muxer, 0, sizeof(muxer));
@@ -2446,7 +2443,7 @@ static const char *unpack_with_sideband(struct shallow_info *si)
 	if (start_async(&muxer))
 		return NULL;
 
-	ret = unpack(muxer.in, si);
+	ret = unpack(muxer.in, si, transaction);
 
 	finish_async(&muxer);
 	return ret;
@@ -2623,6 +2620,7 @@ int cmd_receive_pack(int argc,
 	struct oid_array ref = OID_ARRAY_INIT;
 	struct shallow_info si;
 	struct packet_reader reader;
+	struct odb_transaction *transaction = NULL;
 
 	struct option options[] = {
 		OPT__QUIET(&quiet, N_("quiet")),
@@ -2707,11 +2705,14 @@ int cmd_receive_pack(int argc,
 		if (!si.nr_ours && !si.nr_theirs)
 			shallow_update = 0;
 		if (!delete_only(commands)) {
-			unpack_status = unpack_with_sideband(&si);
+			if (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))
+				unpack_status = "unable to start object transaction";
+			else
+				unpack_status = unpack_with_sideband(&si, transaction);
 			update_shallow_info(commands, &si, &ref);
 		}
 		use_keepalive = KEEPALIVE_ALWAYS;
-		execute_commands(commands, unpack_status, &si,
+		execute_commands(commands, unpack_status, &si, transaction,
 				 &push_options);
 		delete_tempfile(&pack_lockfile);
 		sigchain_push(SIGPIPE, SIG_IGN);
@@ -2720,7 +2721,7 @@ int cmd_receive_pack(int argc,
 		else if (report_status)
 			report(commands, unpack_status);
 		sigchain_pop(SIGPIPE);
-		run_receive_hook(commands, "post-receive", 1,
+		run_receive_hook(commands, "post-receive", 1, NULL,
 				 &push_options);
 		run_update_post_hook(commands);
 		free_commands(commands);
diff --git a/builtin/reflog.c b/builtin/reflog.c
index dcbfe89..1211c58 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -250,7 +250,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix,
 		struct string_list_item *item;
 		struct worktree **worktrees, **p;
 
-		worktrees = get_worktrees();
+		worktrees = get_worktrees(the_repository);
 		for (p = worktrees; *p; p++) {
 			if (single_worktree && !(*p)->is_current)
 				continue;
@@ -374,7 +374,7 @@ static int cmd_reflog_drop(int argc, const char **argv, const char *prefix,
 		struct string_list_item *item;
 		struct worktree **worktrees, **p;
 
-		worktrees = get_worktrees();
+		worktrees = get_worktrees(the_repository);
 		for (p = worktrees; *p; p++) {
 			if (single_worktree && !(*p)->is_current)
 				continue;
diff --git a/builtin/refs.c b/builtin/refs.c
index e3125bc..5cd21c2 100644
--- a/builtin/refs.c
+++ b/builtin/refs.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
 #include "builtin.h"
 #include "config.h"
 #include "fsck.h"
@@ -22,8 +21,20 @@
 #define REFS_OPTIMIZE_USAGE \
 	N_("git refs optimize " PACK_REFS_OPTS)
 
+#define REFS_CREATE_USAGE \
+	N_("git refs create [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value>")
+
+#define REFS_DELETE_USAGE \
+	N_("git refs delete [--message=<reason>] [--no-deref] <ref> [<old-value>]")
+
+#define REFS_UPDATE_USAGE \
+	N_("git refs update [--message=<reason>] [--no-deref] [--create-reflog] <ref> <new-value> [<old-value>]")
+
+#define REFS_RENAME_USAGE \
+	N_("git refs rename [--message=<reason>] <old-ref> <new-ref>")
+
 static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
-			    struct repository *repo UNUSED)
+			    struct repository *repo)
 {
 	const char * const migrate_usage[] = {
 		REFS_MIGRATE_USAGE,
@@ -59,13 +70,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix,
 		goto out;
 	}
 
-	if (the_repository->ref_storage_format == format) {
+	if (repo->ref_storage_format == format) {
 		err = error(_("repository already uses '%s' format"),
 			    ref_storage_format_to_name(format));
 		goto out;
 	}
 
-	if (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) {
+	if (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) {
 		err = error("%s", errbuf.buf);
 		goto out;
 	}
@@ -99,10 +110,10 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix,
 	if (argc)
 		usage(_("'git refs verify' takes no arguments"));
 
-	repo_config(the_repository, git_fsck_config, &fsck_refs_options);
-	prepare_repo_settings(the_repository);
+	repo_config(repo, git_fsck_config, &fsck_refs_options);
+	prepare_repo_settings(repo);
 
-	worktrees = get_worktrees_without_reading_head();
+	worktrees = get_worktrees_without_reading_head(repo);
 	for (size_t i = 0; worktrees[i]; i++)
 		ret |= refs_fsck(get_worktree_ref_store(worktrees[i]),
 				 &fsck_refs_options, worktrees[i]);
@@ -124,7 +135,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix,
 }
 
 static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
-			   struct repository *repo UNUSED)
+			   struct repository *repo)
 {
 	struct strbuf unused_referent = STRBUF_INIT;
 	struct object_id unused_oid;
@@ -145,7 +156,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix,
 		die(_("'git refs exists' requires a reference"));
 
 	ref = *argv++;
-	if (refs_read_raw_ref(get_main_ref_store(the_repository), ref,
+	if (refs_read_raw_ref(get_main_ref_store(repo), ref,
 			      &unused_oid, &unused_referent, &unused_type,
 			      &failure_errno)) {
 		if (failure_errno == ENOENT || failure_errno == EISDIR) {
@@ -176,6 +187,193 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix,
 	return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage);
 }
 
+static int cmd_refs_create(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	static char const * const refs_create_usage[] = {
+		REFS_CREATE_USAGE,
+		NULL
+	};
+	const char *message = NULL;
+	unsigned flags = 0;
+	struct option opts[] = {
+		OPT_STRING(0, "message", &message, N_("reason"),
+			   N_("reason of the update")),
+		OPT_BIT(0 ,"no-deref", &flags,
+			N_("update <refname> not the one it points to"),
+			REF_NO_DEREF),
+		OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+			REF_FORCE_CREATE_REFLOG),
+		OPT_END(),
+	};
+	struct object_id newoid;
+	const char *refname;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0);
+	if (argc != 2)
+		usage(_("create requires reference name and an object ID"));
+
+	if (message && !*message)
+		die(_("refusing to perform update with empty message"));
+
+	repo_config(repo, git_default_config, NULL);
+
+	refname = argv[0];
+	if (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+		die(_("invalid object ID: '%s'"), argv[1]);
+	if (is_null_oid(&newoid))
+		die(_("cannot create reference with null new object ID"));
+
+	ret = refs_update_ref(get_main_ref_store(repo), message, refname,
+			      &newoid, null_oid(repo->hash_algo), flags,
+			      UPDATE_REFS_MSG_ON_ERR);
+
+	if (ret < 0)
+		ret = 1;
+	return ret;
+}
+
+static int cmd_refs_delete(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	static char const * const refs_delete_usage[] = {
+		REFS_DELETE_USAGE,
+		NULL
+	};
+	const char *message = NULL;
+	unsigned flags = 0;
+	struct option opts[] = {
+		OPT_STRING(0, "message", &message, N_("reason"),
+			   N_("reason of the update")),
+		OPT_BIT(0 ,"no-deref", &flags,
+			N_("update <refname> not the one it points to"),
+			REF_NO_DEREF),
+		OPT_END(),
+	};
+	struct object_id oldoid;
+	const char *refname;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0);
+	if (argc < 1 || argc > 2)
+		usage(_("delete requires reference name and an optional old object ID"));
+
+	if (message && !*message)
+		die(_("refusing to perform update with empty message"));
+
+	repo_config(repo, git_default_config, NULL);
+
+	refname = argv[0];
+	if (argc == 2) {
+		if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK))
+			die(_("invalid old object ID: '%s'"), argv[1]);
+		if (is_null_oid(&oldoid))
+			die(_("cannot delete reference with null old object ID"));
+	}
+
+	ret = refs_delete_ref(get_main_ref_store(repo), message, refname,
+			      argc == 2 ? &oldoid : NULL, flags);
+
+	if (ret < 0)
+		ret = 1;
+	return ret;
+}
+
+static int cmd_refs_update(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	static char const * const refs_update_usage[] = {
+		REFS_UPDATE_USAGE,
+		NULL
+	};
+	const char *message = NULL;
+	unsigned flags = 0;
+	struct option opts[] = {
+		OPT_STRING(0, "message", &message, N_("reason"),
+			   N_("reason of the update")),
+		OPT_BIT(0 ,"no-deref", &flags,
+			N_("update <refname> not the one it points to"),
+			REF_NO_DEREF),
+		OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"),
+			REF_FORCE_CREATE_REFLOG),
+		OPT_END(),
+	};
+	struct object_id newoid, oldoid;
+	const char *refname;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0);
+	if (argc < 2 || argc > 3)
+		usage(_("update requires reference name, new value and an optional old value"));
+
+	if (message && !*message)
+		die(_("refusing to perform update with empty message"));
+
+	repo_config(repo, git_default_config, NULL);
+
+	refname = argv[0];
+	if (repo_get_oid_with_flags(repo, argv[1], &newoid,
+				    GET_OID_SKIP_AMBIGUITY_CHECK))
+		die(_("invalid new object ID: '%s'"), argv[1]);
+	if (argc == 3 &&
+	    repo_get_oid_with_flags(repo, argv[2], &oldoid,
+				    GET_OID_SKIP_AMBIGUITY_CHECK))
+		die(_("invalid old object ID: '%s'"), argv[2]);
+
+	ret = refs_update_ref(get_main_ref_store(repo), message, refname,
+			      &newoid, argc == 3 ? &oldoid : NULL, flags,
+			      UPDATE_REFS_MSG_ON_ERR);
+
+	if (ret < 0)
+		ret = 1;
+	return ret;
+}
+
+static int cmd_refs_rename(int argc, const char **argv, const char *prefix,
+			   struct repository *repo)
+{
+	static char const * const refs_rename_usage[] = {
+		REFS_RENAME_USAGE,
+		NULL
+	};
+	const char *message = NULL;
+	struct option opts[] = {
+		OPT_STRING(0, "message", &message, N_("reason"),
+			   N_("reason of the update")),
+		OPT_END(),
+	};
+	const char *oldref, *newref;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0);
+	if (argc != 2)
+		usage(_("rename requires old and new reference name"));
+	if (message && !*message)
+		die(_("refusing to perform update with empty message"));
+
+	repo_config(repo, git_default_config, NULL);
+
+	oldref = argv[0];
+	newref = argv[1];
+
+	if (check_refname_format(oldref, 0))
+		die(_("invalid ref format: '%s'"), oldref);
+	if (check_refname_format(newref, 0))
+		die(_("invalid ref format: '%s'"), newref);
+
+	if (!refs_ref_exists(get_main_ref_store(repo), oldref))
+		die(_("reference does not exist: '%s'"), oldref);
+	if (refs_ref_exists(get_main_ref_store(repo), newref))
+		die(_("reference already exists: '%s'"), newref);
+
+	ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message);
+
+	if (ret < 0)
+		ret = 1;
+	return ret;
+}
+
 int cmd_refs(int argc,
 	     const char **argv,
 	     const char *prefix,
@@ -187,6 +385,10 @@ int cmd_refs(int argc,
 		"git refs list " COMMON_USAGE_FOR_EACH_REF,
 		REFS_EXISTS_USAGE,
 		REFS_OPTIMIZE_USAGE,
+		REFS_CREATE_USAGE,
+		REFS_DELETE_USAGE,
+		REFS_UPDATE_USAGE,
+		REFS_RENAME_USAGE,
 		NULL,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
@@ -196,6 +398,10 @@ int cmd_refs(int argc,
 		OPT_SUBCOMMAND("list", &fn, cmd_refs_list),
 		OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists),
 		OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize),
+		OPT_SUBCOMMAND("create", &fn, cmd_refs_create),
+		OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete),
+		OPT_SUBCOMMAND("update", &fn, cmd_refs_update),
+		OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename),
 		OPT_END(),
 	};
 
diff --git a/builtin/repo.c b/builtin/repo.c
index 042d6de..84e012f 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -84,7 +84,7 @@ static int get_path_commondir_absolute(struct repository *repo, struct strbuf *b
 	if (!common_dir)
 		return error(_("unable to get common directory"));
 
-	format_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+	format_path(buf, common_dir, repo->prefix, PATH_FORMAT_CANONICAL);
 	return 0;
 }
 
@@ -95,7 +95,7 @@ static int get_path_commondir_relative(struct repository *repo, struct strbuf *b
 	if (!common_dir)
 		return error(_("unable to get common directory"));
 
-	format_path(buf, common_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+	format_path(buf, common_dir, repo->prefix, PATH_FORMAT_RELATIVE);
 	return 0;
 }
 
@@ -106,7 +106,7 @@ static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf)
 	if (!git_dir)
 		return error(_("unable to get git directory"));
 
-	format_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+	format_path(buf, git_dir, repo->prefix, PATH_FORMAT_CANONICAL);
 	return 0;
 }
 
@@ -117,7 +117,7 @@ static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
 	if (!git_dir)
 		return error(_("unable to get git directory"));
 
-	format_path(buf, git_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+	format_path(buf, git_dir, repo->prefix, PATH_FORMAT_RELATIVE);
 	return 0;
 }
 
diff --git a/builtin/rev-list.c b/builtin/rev-list.c
index 8f63003..02818b8 100644
--- a/builtin/rev-list.c
+++ b/builtin/rev-list.c
@@ -486,7 +486,7 @@ static int show_object_fast(
 	void *payload UNUSED)
 {
 	fprintf(stdout, "%s\n", oid_to_hex(oid));
-	return 1;
+	return 0;
 }
 
 static void print_disk_usage(off_t size)
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 5e04b0e..4369345 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -255,7 +255,7 @@ static int show_file(const char *arg, int output_prefix)
 	show_default();
 	if ((filter & (DO_NONFLAGS|DO_NOREV)) == (DO_NONFLAGS|DO_NOREV)) {
 		if (output_prefix) {
-			const char *prefix = startup_info->prefix;
+			const char *prefix = the_repository->prefix;
 			char *fname = prefix_filename(prefix, arg);
 			show(fname);
 			free(fname);
@@ -832,7 +832,8 @@ int cmd_rev_parse(int argc,
 				prefix = argv[++i];
 				if (!prefix)
 					die(_("--prefix requires an argument"));
-				startup_info->prefix = prefix;
+				FREE_AND_NULL(the_repository->prefix);
+				the_repository->prefix = xstrdup(prefix);
 				output_prefix = 1;
 				continue;
 			}
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 1412b49..d6cdbae 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -153,7 +153,7 @@ int cmd_send_pack(int argc,
 		  const char *prefix,
 		  struct repository *repo)
 {
-	struct refspec rs = REFSPEC_INIT_PUSH;
+	struct refspec rs;
 	const char *remote_name = NULL;
 	struct remote *remote = NULL;
 	const char *dest = NULL;
@@ -214,6 +214,9 @@ int cmd_send_pack(int argc,
 
 	repo_config(repo, send_pack_config, NULL);
 	argc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);
+
+	refspec_init_push(&rs, repo->hash_algo);
+
 	if (argc > 0) {
 		dest = argv[0];
 		refspec_appendn(&rs, argv + 1, argc - 1);
diff --git a/builtin/shortlog.c b/builtin/shortlog.c
index 6b2a0b9..4c78d2e 100644
--- a/builtin/shortlog.c
+++ b/builtin/shortlog.c
@@ -433,6 +433,8 @@ int cmd_shortlog(int argc,
 		case PARSE_OPT_UNKNOWN:
 			break;
 		case PARSE_OPT_HELP:
+			exit(0);
+		case PARSE_OPT_HELP_ERROR:
 		case PARSE_OPT_ERROR:
 		case PARSE_OPT_SUBCOMMAND:
 			exit(129);
diff --git a/builtin/show-branch.c b/builtin/show-branch.c
index f02831b..2435e8a 100644
--- a/builtin/show-branch.c
+++ b/builtin/show-branch.c
@@ -62,11 +62,10 @@ static const char *get_color_reset_code(void)
 
 static struct commit *interesting(struct prio_queue *queue)
 {
-	for (size_t i = 0; i < queue->nr; i++) {
-		struct commit *commit = queue->array[i].data;
-		if (commit->object.flags & UNINTERESTING)
-			continue;
-		return commit;
+	struct commit *commit;
+	prio_queue_for_each(queue, commit) {
+		if (!(commit->object.flags & UNINTERESTING))
+			return commit;
 	}
 	return NULL;
 }
@@ -228,17 +227,18 @@ static void join_revs(struct prio_queue *queue,
 {
 	int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
 	int all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+	struct commit *commit;
 
-	while (queue->nr) {
+	while ((commit = prio_queue_peek(queue))) {
 		struct commit_list *parents;
 		int still_interesting = !!interesting(queue);
-		struct commit *commit = prio_queue_peek(queue);
-		bool get_pending = true;
 		int flags = commit->object.flags & all_mask;
 
 		if (!still_interesting && extra <= 0)
 			break;
 
+		prio_queue_get(queue);
+
 		mark_seen(commit, seen_p);
 		if ((flags & all_revs) == all_revs)
 			flags |= UNINTERESTING;
@@ -254,14 +254,8 @@ static void join_revs(struct prio_queue *queue,
 			if (mark_seen(p, seen_p) && !still_interesting)
 				extra--;
 			p->object.flags |= flags;
-			if (get_pending)
-				prio_queue_replace(queue, p);
-			else
-				prio_queue_put(queue, p);
-			get_pending = false;
+			prio_queue_put(queue, p);
 		}
-		if (get_pending)
-			prio_queue_get(queue);
 	}
 
 	/*
diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index 0863d0f..cb4a037 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -341,7 +341,8 @@ static int write_patterns_and_update(struct repository *repo,
 	if (safe_create_leading_directories(repo, sparse_filename))
 		die(_("failed to create directory for sparse-checkout file"));
 
-	hold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR);
+	repo_hold_lock_file_for_update(repo, &lk, sparse_filename,
+				       LOCK_DIE_ON_ERROR);
 
 	result = update_working_directory(repo, pl);
 	if (result) {
diff --git a/builtin/stash.c b/builtin/stash.c
index c4809f2..72c5257 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1702,8 +1702,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
 	if (!include_untracked && ps->nr) {
 		char *ps_matched = xcalloc(ps->nr, 1);
 
-		/* TODO: audit for interaction with sparse-index. */
-		ensure_full_index(the_repository->index);
+		if (pathspec_needs_expanded_index(the_repository->index, ps))
+			ensure_full_index(the_repository->index);
 		for (size_t i = 0; i < the_repository->index->cache_nr; i++)
 			ce_path_match(the_repository->index, the_repository->index->cache[i], ps,
 				      ps_matched);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 1cc82a1..e7cd322 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -549,12 +549,13 @@ static void create_default_gitdir_config(const char *submodule_name)
 	}
 
 	/* Case 2.4: If all the above failed, try a hash of the name as a last resort */
-	header_len = snprintf(header, sizeof(header), "blob %zu", strlen(submodule_name));
-	the_hash_algo->init_fn(&ctx);
-	the_hash_algo->update_fn(&ctx, header, header_len);
-	the_hash_algo->update_fn(&ctx, "\0", 1);
-	the_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));
-	the_hash_algo->final_fn(raw_name_hash, &ctx);
+	header_len = snprintf(header, sizeof(header),
+			      "blob %"PRIuMAX, (uintmax_t)strlen(submodule_name));
+	git_hash_init(&ctx, the_hash_algo);
+	git_hash_update(&ctx, header, header_len);
+	git_hash_update(&ctx, "\0", 1);
+	git_hash_update(&ctx, submodule_name, strlen(submodule_name));
+	git_hash_final(raw_name_hash, &ctx);
 	hash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);
 	strbuf_reset(&gitdir_path);
 	repo_git_path_append(the_repository, &gitdir_path, "modules/%s", hex_name_hash);
@@ -2990,7 +2991,7 @@ static int module_update(int argc, const char **argv, const char *prefix,
 	struct option module_update_options[] = {
 		OPT__SUPER_PREFIX(&opt.super_prefix),
 		OPT__FORCE(&opt.force, N_("force checkout updates"), 0),
-		OPT_BOOL(0, "init", &opt.init,
+		OPT_BOOL('i', "init", &opt.init,
 			 N_("initialize uninitialized submodules before update")),
 		OPT_BOOL(0, "remote", &opt.remote,
 			 N_("use SHA-1 of submodule's remote tracking branch")),
@@ -3150,7 +3151,7 @@ static int push_check(int argc, const char **argv, const char *prefix UNUSED,
 	if (argc > 2) {
 		int i;
 		struct ref *local_refs = get_local_heads();
-		struct refspec refspec = REFSPEC_INIT_PUSH;
+		struct refspec refspec = REFSPEC_INIT_PUSH(the_hash_algo);
 
 		refspec_appendn(&refspec, argv + 2, argc - 2);
 
diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c
index f3849bb..4263edf 100644
--- a/builtin/unpack-objects.c
+++ b/builtin/unpack-objects.c
@@ -598,7 +598,7 @@ static void unpack_all(void)
 		progress = start_progress(the_repository,
 					  _("Unpacking objects"), nr_objects);
 	CALLOC_ARRAY(obj_list, nr_objects);
-	transaction = odb_transaction_begin(the_repository->objects);
+	odb_transaction_begin_or_die(the_repository->objects, &transaction, 0);
 	for (i = 0; i < nr_objects; i++) {
 		unpack_one(i);
 		display_progress(progress, i + 1);
@@ -670,10 +670,10 @@ int cmd_unpack_objects(int argc,
 		/* We don't take any non-flag arguments now.. Maybe some day */
 		usage(unpack_usage);
 	}
-	the_hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, the_hash_algo);
 	unpack_all();
 	git_hash_update(&ctx, buffer, offset);
-	the_hash_algo->init_fn(&tmp_ctx);
+	git_hash_init(&tmp_ctx, the_hash_algo);
 	git_hash_clone(&tmp_ctx, &ctx);
 	git_hash_final_oid(&oid, &tmp_ctx);
 	if (strict) {
diff --git a/builtin/update-index.c b/builtin/update-index.c
index 3d6646c..241abd4 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -294,7 +294,7 @@ static int add_one_path(const struct cache_entry *old, const char *path, int len
 	ce->ce_flags = create_ce_flags(0);
 	ce->ce_namelen = len;
 	fill_stat_cache_info(the_repository->index, ce, st);
-	ce->ce_mode = ce_mode_from_stat(old, st->st_mode);
+	ce->ce_mode = ce_mode_from_stat(the_repository, old, st->st_mode);
 
 	if (index_path(the_repository->index, &ce->oid, path, st,
 		       info_only ? 0 : INDEX_WRITE_OBJECT)) {
@@ -875,7 +875,7 @@ static enum parse_opt_result unresolve_callback(
 	const char *arg, int unset)
 {
 	int *has_errors = opt->value;
-	const char *prefix = startup_info->prefix;
+	const char *prefix = the_repository->prefix;
 
 	BUG_ON_OPT_NEG(unset);
 	BUG_ON_OPT_ARG(arg);
@@ -896,7 +896,7 @@ static enum parse_opt_result reupdate_callback(
 	const char *arg, int unset)
 {
 	int *has_errors = opt->value;
-	const char *prefix = startup_info->prefix;
+	const char *prefix = the_repository->prefix;
 
 	BUG_ON_OPT_NEG(unset);
 	BUG_ON_OPT_ARG(arg);
@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,
 	 * Allow the object layer to optimize adding multiple objects in
 	 * a batch.
 	 */
-	transaction = odb_transaction_begin(the_repository->objects);
+	odb_transaction_begin_or_die(the_repository->objects, &transaction, 0);
 	while (ctx.argc) {
 		if (parseopt_state != PARSE_OPT_DONE)
 			parseopt_state = parse_options_step(&ctx, options,
@@ -1133,6 +1133,8 @@ int cmd_update_index(int argc,
 			break;
 		switch (parseopt_state) {
 		case PARSE_OPT_HELP:
+			exit(0);
+		case PARSE_OPT_HELP_ERROR:
 		case PARSE_OPT_ERROR:
 			exit(129);
 		case PARSE_OPT_COMPLETE:
diff --git a/builtin/worktree.c b/builtin/worktree.c
index d21c43f..654d27c 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -226,7 +226,7 @@ static void prune_worktrees(void)
 	while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {
 		char *path;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path, expire))
+		if (should_prune_worktree(the_repository, d->d_name, &reason, &path, expire))
 			prune_worktree(d->d_name, reason.buf);
 		else if (path)
 			string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name);
@@ -349,7 +349,7 @@ static void copy_sparse_checkout(const char *worktree_git_dir)
 
 	if (file_exists(from_file)) {
 		if (safe_create_leading_directories(the_repository, to_file) ||
-			copy_file(to_file, from_file, 0666))
+			copy_file(the_repository, to_file, from_file, 0666))
 			error(_("failed to copy '%s' to '%s'; sparse-checkout may not work correctly"),
 				from_file, to_file);
 	}
@@ -368,7 +368,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 		int bare;
 
 		if (safe_create_leading_directories(the_repository, to_file) ||
-			copy_file(to_file, from_file, 0666)) {
+			copy_file(the_repository, to_file, from_file, 0666)) {
 			error(_("failed to copy worktree config from '%s' to '%s'"),
 				from_file, to_file);
 			goto worktree_copy_cleanup;
@@ -475,13 +475,13 @@ static int add_worktree(const char *path, const char *refname,
 	struct ref_store *wt_refs;
 	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	check_candidate_path(path, opts->force, worktrees, "add");
 	free_worktrees(worktrees);
 	worktrees = NULL;
 
 	/* is 'refname' a branch or commit? */
-	if (!opts->detach && !check_branch_ref(&symref, refname) &&
+	if (!opts->detach && !check_branch_ref(the_repository, &symref, refname) &&
 	    refs_ref_exists(get_main_ref_store(the_repository), symref.buf)) {
 		is_branch = 1;
 		if (!opts->force)
@@ -539,7 +539,8 @@ static int add_worktree(const char *path, const char *refname,
 
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
-	write_worktree_linking_files(sb_git.buf, sb.buf, opts->relative_paths);
+	write_worktree_linking_files(the_repository, sb_git.buf,
+				     sb.buf, opts->relative_paths);
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
 	write_file(sb.buf, "../..");
@@ -547,7 +548,7 @@ static int add_worktree(const char *path, const char *refname,
 	/*
 	 * Set up the ref store of the worktree and create the HEAD reference.
 	 */
-	wt = get_linked_worktree(name, 1);
+	wt = get_linked_worktree(the_repository, name, 1);
 	if (!wt) {
 		ret = error(_("could not find created worktree '%s'"), name);
 		goto done;
@@ -649,7 +650,7 @@ static void print_preparing_worktree_line(int detach,
 		fprintf_ln(stderr, _("Preparing worktree (new branch '%s')"), new_branch);
 	} else {
 		struct strbuf s = STRBUF_INIT;
-		if (!detach && !check_branch_ref(&s, branch) &&
+		if (!detach && !check_branch_ref(the_repository, &s, branch) &&
 		    refs_ref_exists(get_main_ref_store(the_repository), s.buf))
 			fprintf_ln(stderr, _("Preparing worktree (checking out '%s')"),
 				  branch);
@@ -771,7 +772,7 @@ static char *dwim_branch(const char *path, char **new_branch)
 	char *branchname = xstrndup(s, n);
 	struct strbuf ref = STRBUF_INIT;
 
-	branch_exists = !check_branch_ref(&ref, branchname) &&
+	branch_exists = !check_branch_ref(the_repository, &ref, branchname) &&
 			refs_ref_exists(get_main_ref_store(the_repository),
 					ref.buf);
 	strbuf_release(&ref);
@@ -868,7 +869,7 @@ static int add(int ac, const char **av, const char *prefix,
 		new_branch = new_branch_force;
 
 		if (!opts.force &&
-		    !check_branch_ref(&symref, new_branch) &&
+		    !check_branch_ref(the_repository, &symref, new_branch) &&
 		    refs_ref_exists(get_main_ref_store(the_repository), symref.buf))
 			die_if_checked_out(symref.buf, 0);
 		strbuf_release(&symref);
@@ -945,14 +946,17 @@ static int add(int ac, const char **av, const char *prefix,
 		strvec_push(&cp.args, branch);
 		if (opt_track)
 			strvec_push(&cp.args, opt_track);
-		if (run_command(&cp))
-			return -1;
+		if (run_command(&cp)) {
+			ret = -1;
+			goto cleanup;
+		}
 		branch = new_branch;
 	} else if (opt_track) {
 		die(_("--[no-]track can only be used if a new branch is created"));
 	}
 
 	ret = add_worktree(path, branch, &opts);
+cleanup:
 	free(path);
 	free(opt_track);
 	free(branch_to_free);
@@ -1103,7 +1107,7 @@ static int list(int ac, const char **av, const char *prefix,
 	else if (!line_terminator && !porcelain)
 		die(_("the option '%s' requires '%s'"), "-z", "--porcelain");
 	else {
-		struct worktree **worktrees = get_worktrees();
+		struct worktree **worktrees = get_worktrees(the_repository);
 		int path_maxwidth = 0, abbrev = DEFAULT_ABBREV, i;
 		struct worktree_display *display = NULL;
 
@@ -1146,7 +1150,7 @@ static int lock_worktree(int ac, const char **av, const char *prefix,
 	if (ac != 1)
 		usage_with_options(git_worktree_lock_usage, options);
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	wt = find_worktree(worktrees, prefix, av[0]);
 	if (!wt)
 		die(_("'%s' is not a working tree"), av[0]);
@@ -1183,7 +1187,7 @@ static int unlock_worktree(int ac, const char **av, const char *prefix,
 	if (ac != 1)
 		usage_with_options(git_worktree_unlock_usage, options);
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	wt = find_worktree(worktrees, prefix, av[0]);
 	if (!wt)
 		die(_("'%s' is not a working tree"), av[0]);
@@ -1269,7 +1273,7 @@ static int move_worktree(int ac, const char **av, const char *prefix,
 	strbuf_addstr(&dst, path);
 	free(path);
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	wt = find_worktree(worktrees, prefix, av[0]);
 	if (!wt)
 		die(_("'%s' is not a working tree"), av[0]);
@@ -1394,7 +1398,7 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
 	if (ac != 1)
 		usage_with_options(git_worktree_remove_usage, options);
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	wt = find_worktree(worktrees, prefix, av[0]);
 	if (!wt)
 		die(_("'%s' is not a working tree"), av[0]);
@@ -1456,8 +1460,9 @@ static int repair(int ac, const char **av, const char *prefix,
 	ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0);
 	p = ac > 0 ? av : self;
 	for (; *p; p++)
-		repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths);
-	repair_worktrees(report_repair, &rc, use_relative_paths);
+		repair_worktree_at_path(the_repository, *p, report_repair,
+					&rc, use_relative_paths);
+	repair_worktrees(the_repository, report_repair, &rc, use_relative_paths);
 	return rc;
 }
 
diff --git a/bundle-uri.c b/bundle-uri.c
index 3b2e347..2bb2eb9 100644
--- a/bundle-uri.c
+++ b/bundle-uri.c
@@ -378,7 +378,7 @@ static int download_https_uri_to_file(const char *file, const char *uri)
 	if (child_in)
 		fclose(child_in);
 	if (finish_command(&cp))
-		return 1;
+		result = 1;
 	if (child_out)
 		fclose(child_out);
 	return result;
@@ -396,7 +396,7 @@ static int copy_uri_to_file(const char *filename, const char *uri)
 		uri = out;
 
 	/* Copy as a file */
-	return copy_file(filename, uri, 0);
+	return copy_file(the_repository, filename, uri, 0);
 }
 
 static int unbundle_from_file(struct repository *r, const char *file)
@@ -946,8 +946,12 @@ static int config_to_packet_line(const char *key, const char *value,
 {
 	struct packet_reader *writer = data;
 
-	if (starts_with(key, "bundle."))
-		packet_write_fmt(writer->fd, "%s=%s", key, value);
+	if (starts_with(key, "bundle.")) {
+		if (value && *value)
+			packet_write_fmt(writer->fd, "%s=%s", key, value);
+		else
+			warning(_("config '%s' has no value"), key);
+	}
 
 	return 0;
 }
diff --git a/bundle.c b/bundle.c
index fd2db2c..b64716f 100644
--- a/bundle.c
+++ b/bundle.c
@@ -519,8 +519,8 @@ int create_bundle(struct repository *r, const char *path,
 	if (bundle_to_stdout)
 		bundle_fd = 1;
 	else
-		bundle_fd = hold_lock_file_for_update(&lock, path,
-						      LOCK_DIE_ON_ERROR);
+		bundle_fd = repo_hold_lock_file_for_update(r, &lock, path,
+							   LOCK_DIE_ON_ERROR);
 
 	if (version == -1)
 		version = min_version;
diff --git a/cache-tree.c b/cache-tree.c
index 9204eb3..d92f513 100644
--- a/cache-tree.c
+++ b/cache-tree.c
@@ -516,6 +516,7 @@ static int update_one(struct cache_tree *it,
 
 int cache_tree_update(struct index_state *istate, int flags)
 {
+	int inflight = !!the_repository->objects->transaction;
 	struct odb_transaction *transaction;
 	int skip, i;
 
@@ -532,10 +533,12 @@ int cache_tree_update(struct index_state *istate, int flags)
 
 	trace_performance_enter();
 	trace2_region_enter("cache_tree", "update", istate->repo);
-	transaction = odb_transaction_begin(the_repository->objects);
+	if (!inflight)
+		odb_transaction_begin_or_die(the_repository->objects, &transaction, 0);
 	i = update_one(istate->cache_tree, istate->cache, istate->cache_nr,
 		       "", 0, &skip, flags);
-	odb_transaction_commit(transaction);
+	if (!inflight)
+		odb_transaction_commit(transaction);
 	trace2_region_leave("cache_tree", "update", istate->repo);
 	trace_performance_leave("cache_tree_update");
 	if (i < 0)
diff --git a/ci/lib.sh b/ci/lib.sh
index b939110..6c52154 100755
--- a/ci/lib.sh
+++ b/ci/lib.sh
@@ -215,6 +215,7 @@
 	test macos != "$CI_OS_NAME" || CI_OS_NAME=osx
 	CI_REPO_SLUG="$GITHUB_REPOSITORY"
 	CI_JOB_ID="$GITHUB_RUN_ID"
+	CI_EVENT="$GITHUB_EVENT_NAME"
 	CC="${CC_PACKAGE:-${CC:-gcc}}"
 	DONT_SKIP_TAGS=t
 	handle_failed_tests () {
@@ -239,6 +240,13 @@
 	CI_BRANCH="$CI_COMMIT_REF_NAME"
 	CI_COMMIT="$CI_COMMIT_SHA"
 
+	case "$CI_PIPELINE_SOURCE" in
+	merge_request_event)
+		CI_EVENT=pull_request;;
+	*)
+		CI_EVENT="$CI_PIPELINE_SOURCE";;
+	esac
+
 	case "$OS,$CI_JOB_IMAGE" in
 	Windows_NT,*)
 		CI_OS_NAME=windows
@@ -319,9 +327,9 @@
 # enable "expensive" tests for PR events.
 # In order to catch bugs introduced at integration time by mismerges,
 # enable the long tests for pushes to the integration branches as well.
-case "$GITHUB_EVENT_NAME,$CI_BRANCH" in
+case "$CI_EVENT,$CI_BRANCH" in
 pull_request,*|push,*next*|push,*master*|push,*main*|push,*maint*)
-	export GIT_TEST_LONG=YesPlease
+	export GIT_TEST_LONG=${GIT_TEST_LONG:-true}
 	;;
 esac
 
diff --git a/ci/run-build-and-minimal-fuzzers.sh b/ci/run-build-and-minimal-fuzzers.sh
index e7b9795..37b24b0 100755
--- a/ci/run-build-and-minimal-fuzzers.sh
+++ b/ci/run-build-and-minimal-fuzzers.sh
@@ -21,6 +21,7 @@
 pack-headers
 pack-idx
 parse-attr-line
+reftable
 url-decode-mem
 "
 
diff --git a/combine-diff.c b/combine-diff.c
index fb72174..717d537 100644
--- a/combine-diff.c
+++ b/combine-diff.c
@@ -1080,7 +1080,7 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent,
 			/* if symlinks don't work, assume symlink if all parents
 			 * are symlinks
 			 */
-			is_file = has_symlinks;
+			is_file = repo_has_symlinks(rev->repo);
 			for (i = 0; !is_file && i < num_parent; i++)
 				is_file = !S_ISLNK(elem->parent[i].mode);
 			if (!is_file)
diff --git a/commit-graph.c b/commit-graph.c
index c6d9c5c..49e8f63 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -1538,7 +1538,7 @@ static int add_packed_commits(const struct object_id *oid,
 	struct object_info oi = OBJECT_INFO_INIT;
 
 	oi.typep = &type;
-	if (packed_object_info(pack, offset, &oi) < 0)
+	if (packed_object_info(NULL, pack, offset, &oi) < 0)
 		die(_("unable to get type of object %s"), oid_to_hex(oid));
 
 	return add_packed_commits_oi(oid, &oi, data);
@@ -1653,6 +1653,7 @@ static void compute_reachable_generation_numbers(
 {
 	int i;
 	struct commit_list *list = NULL;
+	intmax_t steps = 0;
 
 	for (i = 0; i < info->commits->nr; i++) {
 		struct commit *c = info->commits->items[i];
@@ -1671,6 +1672,7 @@ static void compute_reachable_generation_numbers(
 			int all_parents_computed = 1;
 			timestamp_t max_gen = 0;
 
+			steps++;
 			for (parent = current->parents; parent; parent = parent->next) {
 				repo_parse_commit(info->r, parent->item);
 				gen = info->get_generation(parent->item, info->data);
@@ -1694,6 +1696,9 @@ static void compute_reachable_generation_numbers(
 			}
 		}
 	}
+
+	trace2_data_intmax("commit-graph", info->r,
+			   "generation-dfs-steps", steps);
 }
 
 static timestamp_t get_topo_level(struct commit *c, void *data)
@@ -2122,8 +2127,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
 	if (ctx->split) {
 		char *lock_name = get_commit_graph_chain_filename(ctx->odb_source);
 
-		hold_lock_file_for_update_mode(&lk, lock_name,
-					       LOCK_DIE_ON_ERROR, 0444);
+		repo_hold_lock_file_for_update_mode(ctx->r, &lk, lock_name,
+						    LOCK_DIE_ON_ERROR, 0444);
 		free(lock_name);
 
 		graph_layer = mks_tempfile_m(ctx->graph_name, 0444);
@@ -2141,8 +2146,9 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
 		f = hashfd(ctx->r->hash_algo,
 			   get_tempfile_fd(graph_layer), get_tempfile_path(graph_layer));
 	} else {
-		hold_lock_file_for_update_mode(&lk, ctx->graph_name,
-					       LOCK_DIE_ON_ERROR, 0444);
+		repo_hold_lock_file_for_update_mode(ctx->r, &lk,
+						    ctx->graph_name,
+						    LOCK_DIE_ON_ERROR, 0444);
 		f = hashfd(ctx->r->hash_algo,
 			   get_lock_file_fd(&lk), get_lock_file_path(&lk));
 	}
@@ -2605,7 +2611,7 @@ int write_commit_graph(struct odb_source *source,
 
 	g = prepare_commit_graph(ctx.r);
 	for (struct commit_graph *chain = g; chain; chain = chain->base_graph)
-		g->topo_levels = &topo_levels;
+		chain->topo_levels = &topo_levels;
 
 	if (flags & COMMIT_GRAPH_WRITE_BLOOM_FILTERS)
 		ctx.changed_paths = 1;
diff --git a/commit-reach.c b/commit-reach.c
index 5df471a..b53c6b1 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -108,11 +108,14 @@ static int paint_down_to_common(struct repository *r,
 		{ compare_commits_by_gen_then_commit_date }
 	};
 	int i;
+	int gen_ordered = 1;
 	timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
 	struct commit_list **tail = result;
 
-	if (!min_generation && !corrected_commit_dates_enabled(r))
+	if (!min_generation && !corrected_commit_dates_enabled(r)) {
 		queue.pq.compare = compare_commits_by_commit_date;
+		gen_ordered = 0;
+	}
 
 	one->object.flags |= PARENT1;
 	if (!n) {
@@ -147,11 +150,12 @@ static int paint_down_to_common(struct repository *r,
 				commit->object.flags |= RESULT;
 				tail = commit_list_append(commit, tail);
 				/*
-				 * The queue is generation-ordered; no
-				 * remaining common ancestor can be a
+				 * When the queue is generation-ordered,
+				 * no remaining common ancestor can be a
 				 * descendant of this one.
 				 */
 				if (!(mb_flags & MERGE_BASE_FIND_ALL) &&
+				    gen_ordered &&
 				    generation < GENERATION_NUMBER_INFINITY)
 					break;
 			}
@@ -757,7 +761,8 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
 
 /*
  * Test whether the candidate is contained in the list.
- * Do not recurse to find out, though, but return -1 if inconclusive.
+ * Do not recurse to find out, though, but return CONTAINS_UNKNOWN if
+ * inconclusive.
  */
 static enum contains_result contains_test(struct commit *candidate,
 					  const struct commit_list *want,
@@ -814,6 +819,7 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
 	if (result != CONTAINS_UNKNOWN)
 		return result;
 
+	*contains_cache_at(cache, candidate) = CONTAINS_IN_PROGRESS;
 	push_to_contains_stack(candidate, &contains_stack);
 	while (contains_stack.nr) {
 		struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1];
@@ -825,8 +831,8 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
 			contains_stack.nr--;
 		}
 		/*
-		 * If we just popped the stack, parents->item has been marked,
-		 * therefore contains_test will return a meaningful yes/no.
+		 * A parent may have just been popped and marked, or may still
+		 * be active when replacement refs create a cycle.
 		 */
 		else switch (contains_test(parents->item, want, cache, cutoff)) {
 		case CONTAINS_YES:
@@ -836,7 +842,11 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
 		case CONTAINS_NO:
 			entry->parents = parents->next;
 			break;
+		case CONTAINS_IN_PROGRESS:
+			die(_("commit ancestry contains a cycle"));
 		case CONTAINS_UNKNOWN:
+			*contains_cache_at(cache, parents->item) =
+				CONTAINS_IN_PROGRESS;
 			push_to_contains_stack(parents->item, &contains_stack);
 			break;
 		}
@@ -848,9 +858,16 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
 int commit_contains(struct ref_filter *filter, struct commit *commit,
 		    struct commit_list *list, struct contains_cache *cache)
 {
-	if (filter->with_commit_tag_algo)
+	int result;
+
+	if (filter->with_commit_tag_algo ||
+	    generation_numbers_enabled(the_repository))
 		return contains_tag_algo(commit, list, cache) == CONTAINS_YES;
-	return repo_is_descendant_of(the_repository, commit, list);
+
+	result = repo_is_descendant_of(the_repository, commit, list);
+	if (result < 0)
+		die(_("failed to check reachability"));
+	return result;
 }
 
 int can_all_from_reach_with_flag(struct object_array *from,
@@ -1121,6 +1138,7 @@ void ahead_behind(struct repository *r,
 	struct nonstale_queue queue = {
 		{ .compare = compare_commits_by_gen_then_commit_date }
 	};
+	void *entry;
 	size_t width = DIV_ROUND_UP(commits_nr, BITS_IN_EWORD);
 
 	if (!commits_nr || !counts_nr)
@@ -1186,8 +1204,8 @@ void ahead_behind(struct repository *r,
 
 	/* STALE is used here, PARENT2 is used by insert_no_dup(). */
 	repo_clear_commit_marks(r, PARENT2 | STALE);
-	for (size_t i = 0; i < queue.pq.nr; i++)
-		free_bit_array(queue.pq.array[i].data);
+	prio_queue_for_each(&queue.pq, entry)
+		free_bit_array(entry);
 	clear_bit_arrays(&bit_arrays);
 	clear_nonstale_queue(&queue);
 }
@@ -1320,7 +1338,7 @@ int get_branch_base_for_tip(struct repository *r,
 			    size_t bases_nr)
 {
 	int best_index = -1;
-	struct commit *branch_point = NULL;
+	struct commit *c, *branch_point = NULL;
 	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
 	int found_missing_gen = 0;
 
@@ -1373,8 +1391,7 @@ int get_branch_base_for_tip(struct repository *r,
 		prio_queue_put(&queue, c);
 	}
 
-	while (queue.nr) {
-		struct commit *c = prio_queue_get(&queue);
+	while ((c = prio_queue_get(&queue))) {
 		int best_for_c = get_best(c);
 		int best_for_p, positive;
 		struct commit *parent;
diff --git a/commit-reach.h b/commit-reach.h
index 3f3a563..f908d30 100644
--- a/commit-reach.h
+++ b/commit-reach.h
@@ -73,7 +73,8 @@ int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid);
 enum contains_result {
 	CONTAINS_UNKNOWN = 0,
 	CONTAINS_NO,
-	CONTAINS_YES
+	CONTAINS_YES,
+	CONTAINS_IN_PROGRESS
 };
 
 define_commit_slab(contains_cache, enum contains_result);
diff --git a/commit.c b/commit.c
index 5c4a431..ad26f0b 100644
--- a/commit.c
+++ b/commit.c
@@ -782,24 +782,17 @@ void commit_list_sort_by_date(struct commit_list **list)
 struct commit *pop_most_recent_commit(struct prio_queue *queue,
 				      unsigned int mark)
 {
-	struct commit *ret = prio_queue_peek(queue);
-	int get_pending = 1;
+	struct commit *ret = prio_queue_get(queue);
 	struct commit_list *parents = ret->parents;
 
 	while (parents) {
 		struct commit *commit = parents->item;
 		if (!repo_parse_commit(the_repository, commit) && !(commit->object.flags & mark)) {
 			commit->object.flags |= mark;
-			if (get_pending)
-				prio_queue_replace(queue, commit);
-			else
-				prio_queue_put(queue, commit);
-			get_pending = 0;
+			prio_queue_put(queue, commit);
 		}
 		parents = parents->next;
 	}
-	if (get_pending)
-		prio_queue_get(queue);
 	return ret;
 }
 
diff --git a/common-init.c b/common-init.c
index 5cc73f0..d26c9c1 100644
--- a/common-init.c
+++ b/common-init.c
@@ -5,7 +5,10 @@
 #include "exec-cmd.h"
 #include "gettext.h"
 #include "attr.h"
+#include "odb.h"
+#include "parse.h"
 #include "repository.h"
+#include "replace-object.h"
 #include "setup.h"
 #include "strbuf.h"
 #include "trace2.h"
@@ -31,6 +34,22 @@ static void restore_sigpipe_to_default(void)
 	signal(SIGPIPE, SIG_DFL);
 }
 
+static void setup_environment(void)
+{
+	char *git_replace_ref_base;
+	const char *replace_ref_base;
+
+	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
+		disable_replace_refs();
+	replace_ref_base = getenv(GIT_REPLACE_REF_BASE_ENVIRONMENT);
+	git_replace_ref_base = xstrdup(replace_ref_base ? replace_ref_base
+							  : "refs/replace/");
+	update_ref_namespace(NAMESPACE_REPLACE, git_replace_ref_base);
+
+	if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0))
+		fetch_if_missing = 0;
+}
+
 void init_git(const char **argv)
 {
 	struct strbuf tmp = STRBUF_INIT;
@@ -51,6 +70,7 @@ void init_git(const char **argv)
 	git_setup_gettext();
 
 	initialize_repository(the_repository);
+	setup_environment();
 
 	attr_start();
 
diff --git a/compat/mingw.c b/compat/mingw.c
index 5c049c1..e0fbd2c 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -7,6 +7,7 @@
 #include "config.h"
 #include "dir.h"
 #include "environment.h"
+#include "repository.h"
 #include "gettext.h"
 #include "run-command.h"
 #include "strbuf.h"
@@ -1044,7 +1045,7 @@ int mingw_chdir(const char *dirname)
 	if (xutftowcs_path(wdirname, dirname) < 0)
 		return -1;
 
-	if (has_symlinks) {
+	if (repo_has_symlinks(the_repository)) {
 		HANDLE hnd = CreateFileW(wdirname, 0,
 				FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
 				OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
@@ -2269,10 +2270,8 @@ int mingw_kill(pid_t pid, int sig)
 			}
 			ret = terminate_process_tree(h, 128 + sig);
 		}
-		if (ret) {
+		if (ret)
 			errno = err_win_to_posix(GetLastError());
-			CloseHandle(h);
-		}
 		return ret;
 	} else if (pid > 0 && sig == 0) {
 		HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
@@ -2916,7 +2915,7 @@ int symlink(const char *target, const char *link)
 	int len;
 
 	/* fail if symlinks are disabled or API is not supported (WinXP) */
-	if (!has_symlinks) {
+	if (!repo_has_symlinks(the_repository)) {
 		errno = ENOSYS;
 		return -1;
 	}
@@ -3186,15 +3185,23 @@ static void setup_windows_environment(void)
 		if (!tmp && (tmp = getenv("USERPROFILE")))
 			setenv("HOME", tmp, 1);
 	}
+}
 
+int mingw_platform_has_symlinks(void)
+{
+	static int has_symlinks = -1;
 	/*
 	 * Change 'core.symlinks' default to false, unless native symlinks are
 	 * enabled in MSys2 (via 'MSYS=winsymlinks:nativestrict'). Thus we can
 	 * run the test suite (which doesn't obey config files) with or without
 	 * symlink support.
 	 */
-	if (!(tmp = getenv("MSYS")) || !strstr(tmp, "winsymlinks:nativestrict"))
-		has_symlinks = 0;
+	if (has_symlinks < 0) {
+		const char *tmp = getenv("MSYS");
+		has_symlinks = (tmp && strstr(tmp, "winsymlinks:nativestrict")) ? 1 : 0;
+	}
+
+	return has_symlinks;
 }
 
 static void get_current_user_sid(PSID *sid, HANDLE *linked_token)
diff --git a/compat/mingw.h b/compat/mingw.h
index 444daed..df02aeb 100644
--- a/compat/mingw.h
+++ b/compat/mingw.h
@@ -208,6 +208,9 @@ void open_in_gdb(void);
  */
 int err_win_to_posix(DWORD winerr);
 
+int mingw_platform_has_symlinks(void);
+#define platform_has_symlinks() mingw_platform_has_symlinks()
+
 #ifndef NO_UNIX_SOCKETS
 int mingw_have_unix_sockets(void);
 #undef have_unix_sockets
diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c
index 1711794..8077f62 100644
--- a/compat/precompose_utf8.c
+++ b/compat/precompose_utf8.c
@@ -19,6 +19,11 @@ typedef char *iconv_ibp;
 static const char *repo_encoding = "UTF-8";
 static const char *path_encoding = "UTF-8-MAC";
 
+static size_t dirent_prec_psx_size(size_t max_name_len)
+{
+	return st_add(offsetof(dirent_prec_psx, d_name), max_name_len);
+}
+
 static size_t has_non_ascii(const char *s, size_t maxlen, size_t *strlen_c)
 {
 	const uint8_t *ptr = (const uint8_t *)s;
@@ -114,8 +119,8 @@ const char *precompose_argv_prefix(int argc, const char **argv, const char *pref
 PREC_DIR *precompose_utf8_opendir(const char *dirname)
 {
 	PREC_DIR *prec_dir = xmalloc(sizeof(PREC_DIR));
-	prec_dir->dirent_nfc = xmalloc(sizeof(dirent_prec_psx));
-	prec_dir->dirent_nfc->max_name_len = sizeof(prec_dir->dirent_nfc->d_name);
+	prec_dir->dirent_nfc = xmalloc(dirent_prec_psx_size(NAME_MAX + 1));
+	prec_dir->dirent_nfc->max_name_len = NAME_MAX + 1;
 
 	prec_dir->dirp = opendir(dirname);
 	if (!prec_dir->dirp) {
@@ -145,8 +150,7 @@ struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir)
 		int ret_errno = errno;
 
 		if (new_maxlen > prec_dir->dirent_nfc->max_name_len) {
-			size_t new_len = sizeof(dirent_prec_psx) + new_maxlen -
-				sizeof(prec_dir->dirent_nfc->d_name);
+			size_t new_len = dirent_prec_psx_size(new_maxlen);
 
 			prec_dir->dirent_nfc = xrealloc(prec_dir->dirent_nfc, new_len);
 			prec_dir->dirent_nfc->max_name_len = new_maxlen;
diff --git a/compat/precompose_utf8.h b/compat/precompose_utf8.h
index fea06cf..c7c3cc2 100644
--- a/compat/precompose_utf8.h
+++ b/compat/precompose_utf8.h
@@ -14,11 +14,12 @@ typedef struct dirent_prec_psx {
 
 	/*
 	 * See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/dirent.h.html
-	 * NAME_MAX + 1 should be enough, but some systems have
-	 * NAME_MAX=255 and strlen(d_name) may return 508 or 510
-	 * Solution: allocate more when needed, see precompose_utf8_readdir()
+	 * Start with room for NAME_MAX + 1 bytes, but keep d_name as a
+	 * flexible array. Some systems have NAME_MAX=255 while strlen(d_name)
+	 * from readdir() may return 508 or 510 bytes. Grow the allocation as
+	 * needed in precompose_utf8_readdir().
 	 */
-	char   d_name[NAME_MAX+1];
+	char   d_name[FLEX_ARRAY];
 } dirent_prec_psx;
 
 
diff --git a/compat/win32/exit-process.h b/compat/win32/exit-process.h
index d539898..2600416 100644
--- a/compat/win32/exit-process.h
+++ b/compat/win32/exit-process.h
@@ -159,6 +159,7 @@ static int exit_process(HANDLE process, int exit_code)
 		return terminate_process_tree(process, exit_code);
 	}
 
+	CloseHandle(process);
 	return 0;
 }
 
diff --git a/compat/win32/path-utils.c b/compat/win32/path-utils.c
index 966ef77..f779f36 100644
--- a/compat/win32/path-utils.c
+++ b/compat/win32/path-utils.c
@@ -2,6 +2,7 @@
 
 #include "../../git-compat-util.h"
 #include "../../environment.h"
+#include "../../repository.h"
 
 int win32_has_dos_drive_prefix(const char *path)
 {
@@ -75,7 +76,7 @@ int win32_fspathncmp(const char *a, const char *b, size_t count)
 		} else if (is_dir_sep(*b))
 			return +1;
 
-		diff = ignore_case ?
+		diff = repo_ignore_case(the_repository) ?
 			(unsigned char)tolower(*a) - (int)(unsigned char)tolower(*b) :
 			(unsigned char)*a - (int)(unsigned char)*b;
 		if (diff)
diff --git a/config.c b/config.c
index 6a0de86..1bdd702 100644
--- a/config.c
+++ b/config.c
@@ -235,23 +235,20 @@ static int prepare_include_condition_pattern(const struct key_value_info *kvi,
 	return 0;
 }
 
-static int include_by_gitdir(const struct key_value_info *kvi,
-			     const struct config_options *opts,
-			     const char *cond, size_t cond_len, int icase)
+static int include_by_path(const struct key_value_info *kvi,
+			   const char *path,
+			   const char *cond, size_t cond_len, int icase)
 {
 	struct strbuf text = STRBUF_INIT;
 	struct strbuf pattern = STRBUF_INIT;
 	size_t prefix;
 	int ret = 0;
-	const char *git_dir;
 	int already_tried_absolute = 0;
 
-	if (opts->git_dir)
-		git_dir = opts->git_dir;
-	else
+	if (!path)
 		goto done;
 
-	strbuf_realpath(&text, git_dir, 1);
+	strbuf_realpath(&text, path, 1);
 	strbuf_add(&pattern, cond, cond_len);
 	ret = prepare_include_condition_pattern(kvi, &pattern, &prefix);
 	if (ret < 0)
@@ -284,7 +281,7 @@ static int include_by_gitdir(const struct key_value_info *kvi,
 		 * which'll do the right thing
 		 */
 		strbuf_reset(&text);
-		strbuf_add_absolute_path(&text, git_dir);
+		strbuf_add_absolute_path(&text, path);
 		already_tried_absolute = 1;
 		goto again;
 	}
@@ -400,9 +397,15 @@ static int include_condition_is_true(const struct key_value_info *kvi,
 	const struct config_options *opts = inc->opts;
 
 	if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
-		return include_by_gitdir(kvi, opts, cond, cond_len, 0);
+		return include_by_path(kvi, opts->git_dir, cond, cond_len, 0);
 	else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
-		return include_by_gitdir(kvi, opts, cond, cond_len, 1);
+		return include_by_path(kvi, opts->git_dir, cond, cond_len, 1);
+	else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len))
+		return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+				       cond, cond_len, 0);
+	else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len))
+		return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL,
+				       cond, cond_len, 1);
 	else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
 		return include_by_branch(inc, cond, cond_len);
 	else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
@@ -2951,6 +2954,24 @@ char *git_config_prepare_comment_string(const char *comment)
 	return prepared;
 }
 
+/*
+ * How long to retry acquiring config.lock when another process holds
+ * it. Default matches core.packedRefsTimeout; override via
+ * core.configLockTimeout.
+ */
+static long config_lock_timeout_ms(struct repository *r)
+{
+	static int configured;
+	static int timeout_ms = 1000;
+
+	if (!configured) {
+		repo_config_get_int(r, "core.configlocktimeout", &timeout_ms);
+		configured = 1;
+	}
+
+	return timeout_ms;
+}
+
 static void validate_comment_string(const char *comment)
 {
 	size_t leading_blanks;
@@ -3034,7 +3055,8 @@ int repo_config_set_multivar_in_file_gently(struct repository *r,
 	 * The lock serves a purpose in addition to locking: the new
 	 * contents of .git/config will be written into it.
 	 */
-	fd = hold_lock_file_for_update(&lock, config_filename, 0);
+	fd = repo_hold_lock_file_for_update_timeout(r, &lock, config_filename, 0,
+						    config_lock_timeout_ms(r));
 	if (fd < 0) {
 		error_errno(_("could not lock config file %s"), config_filename);
 		ret = CONFIG_NO_LOCK;
@@ -3379,7 +3401,9 @@ static int repo_config_copy_or_rename_section_in_file(
 	if (!config_filename)
 		config_filename = filename_buf = repo_git_path(r, "config");
 
-	out_fd = hold_lock_file_for_update(&lock, config_filename, 0);
+	out_fd = repo_hold_lock_file_for_update_timeout(r, &lock,
+							config_filename, 0,
+							config_lock_timeout_ms(r));
 	if (out_fd < 0) {
 		ret = error(_("could not lock config file %s"), config_filename);
 		goto out;
diff --git a/connect.c b/connect.c
index 47e39d2..feb0420 100644
--- a/connect.c
+++ b/connect.c
@@ -517,7 +517,7 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)
 int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,
 			  struct bundle_list *bundles, int stateless_rpc)
 {
-	int line_nr = 1;
+	int line_nr = 1, err = 0;
 
 	/* Assert bundle-uri support */
 	ensure_server_supports_v2("bundle-uri");
@@ -536,10 +536,19 @@ int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,
 		const char *line = reader->line;
 		line_nr++;
 
+		/*
+		 * Do not parse if an error was encountered, but
+		 * continue draining the response so no stale data
+		 * is left in the reader for subsequent protocol
+		 * exchanges.
+		 */
+		if (err)
+			continue;
+
 		if (!bundle_uri_parse_line(bundles, line))
 			continue;
 
-		return error(_("error on bundle-uri response line %d: %s"),
+		err = error(_("error on bundle-uri response line %d: %s"),
 			     line_nr, line);
 	}
 
@@ -554,7 +563,7 @@ int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,
 	check_stateless_delimiter(stateless_rpc, reader,
 				  _("expected response end packet after ref listing"));
 
-	return 0;
+	return err;
 }
 
 struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
@@ -700,6 +709,40 @@ int server_supports(const char *feature)
 	return !!server_feature_value(feature, NULL);
 }
 
+void write_command_and_capabilities(struct strbuf *req_buf, const char *command,
+				    const struct string_list *server_options)
+{
+	const char *hash_name;
+	int advertise_sid = 0;
+
+	repo_config_get_bool(the_repository, "transfer.advertisesid", &advertise_sid);
+
+	ensure_server_supports_v2(command);
+	packet_buf_write(req_buf, "command=%s", command);
+	if (server_supports_v2("agent"))
+		packet_buf_write(req_buf, "agent=%s", git_user_agent_sanitized());
+	if (advertise_sid && server_supports_v2("session-id"))
+		packet_buf_write(req_buf, "session-id=%s", trace2_session_id());
+	if (server_options && server_options->nr) {
+		ensure_server_supports_v2("server-option");
+		for (size_t i = 0; i < server_options->nr; i++)
+			packet_buf_write(req_buf, "server-option=%s",
+					 server_options->items[i].string);
+	}
+
+	if (server_feature_v2("object-format", &hash_name)) {
+		const unsigned int hash_algo = hash_algo_by_name(hash_name);
+		if (hash_algo_by_ptr(the_hash_algo) != hash_algo)
+			die(_("mismatched algorithms: client %s; server %s"),
+			    the_hash_algo->name, hash_name);
+		packet_buf_write(req_buf, "object-format=%s", the_hash_algo->name);
+	} else if (hash_algo_by_ptr(the_hash_algo) != GIT_HASH_SHA1_LEGACY) {
+		die(_("the server does not support algorithm '%s'"),
+		    the_hash_algo->name);
+	}
+	packet_buf_delim(req_buf);
+}
+
 static const char *url_scheme_name(enum url_scheme scheme)
 {
 	switch (scheme) {
diff --git a/connect.h b/connect.h
index aa482a3..957e5fe 100644
--- a/connect.h
+++ b/connect.h
@@ -34,4 +34,12 @@ void check_stateless_delimiter(int stateless_rpc,
 			       struct packet_reader *reader,
 			       const char *error);
 
+struct string_list;
+/*
+ * Write a protocol v2 command request, along with the capability
+ * advertisements, into req_buf.
+ */
+void write_command_and_capabilities(struct strbuf *req_buf, const char *command,
+				    const struct string_list *server_options);
+
 #endif
diff --git a/contrib/Makefile b/contrib/Makefile
index 787cd07..1203c726 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -1,10 +1,22 @@
+include ../config.mak.uname
+-include ../config.mak.autogen
+-include ../config.mak
+
+
+ifeq ($(uname_S),Darwin)
+OS_CONTRIB += credential/osxkeychain
+endif
+
 all::
+	$(foreach dir,$(OS_CONTRIB),$(MAKE) -C $(dir) $@;)
 
 test::
 	$(MAKE) -C diff-highlight $@
 	$(MAKE) -C subtree $@
+	$(foreach dir,$(OS_CONTRIB),$(MAKE) -C $(dir) $@;)
 
 clean::
 	$(MAKE) -C contacts $@
 	$(MAKE) -C diff-highlight $@
 	$(MAKE) -C subtree $@
+	$(foreach dir,$(OS_CONTRIB),$(MAKE) -C $(dir) $@;)
diff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile
index 219b0d7..d9fba07 100644
--- a/contrib/credential/osxkeychain/Makefile
+++ b/contrib/credential/osxkeychain/Makefile
@@ -10,4 +10,6 @@
 clean:
 	$(MAKE) -C ../../.. clean-git-credential-osxkeychain
 
-.PHONY: all git-credential-osxkeychain install clean
+test: git-credential-osxkeychain
+
+.PHONY: all git-credential-osxkeychain install clean test
diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c
index 73c2b9b..22eb27c 100644
--- a/contrib/credential/wincred/git-credential-wincred.c
+++ b/contrib/credential/wincred/git-credential-wincred.c
@@ -121,10 +121,10 @@ static int match_part_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
 
 static int match_cred_password(const CREDENTIALW *cred) {
 	int ret;
-	WCHAR *cred_password = xmalloc(cred->CredentialBlobSize);
-	wcsncpy_s(cred_password, cred->CredentialBlobSize,
-		(LPCWSTR)cred->CredentialBlob,
-		cred->CredentialBlobSize / sizeof(WCHAR));
+	size_t wlen = cred->CredentialBlobSize / sizeof(WCHAR);
+	WCHAR *cred_password = xmalloc((wlen + 1) * sizeof(WCHAR));
+	wcsncpy_s(cred_password, wlen + 1,
+		(LPCWSTR)cred->CredentialBlob, wlen);
 	ret = !wcscmp(cred_password, password);
 	free(cred_password);
 	return ret;
@@ -208,8 +208,8 @@ static void store_credential(void)
 
 	if (oauth_refresh_token) {
 		wlen = _scwprintf(L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token);
-		secret = xmalloc(sizeof(WCHAR) * wlen);
-		_snwprintf_s(secret, sizeof(WCHAR) * wlen, wlen, L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token);
+		secret = xmalloc((wlen + 1) * sizeof(WCHAR));
+		_snwprintf_s(secret, wlen + 1, wlen, L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token);
 	} else {
 		secret = _wcsdup(password);
 	}
diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh
index 4194687..c10f283 100755
--- a/contrib/subtree/t/t7900-subtree.sh
+++ b/contrib/subtree/t/t7900-subtree.sh
@@ -99,7 +99,7 @@
 }
 
 test_expect_success 'shows short help text for -h' '
-	test_expect_code 129 git subtree -h >out 2>err &&
+	git subtree -h >out 2>err &&
 	test_must_be_empty err &&
 	grep -e "^ *or: git subtree pull" out &&
 	grep -F -e "--[no-]annotate" out
diff --git a/copy.c b/copy.c
index b668209..6074132 100644
--- a/copy.c
+++ b/copy.c
@@ -1,5 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
-
 #include "git-compat-util.h"
 #include "copy.h"
 #include "path.h"
@@ -35,7 +33,8 @@ static int copy_times(const char *dst, const char *src)
 	return 0;
 }
 
-int copy_file(const char *dst, const char *src, int mode)
+int copy_file(struct repository *repo,
+	      const char *dst, const char *src, int mode)
 {
 	int fdi, fdo, status;
 
@@ -59,15 +58,16 @@ int copy_file(const char *dst, const char *src, int mode)
 	if (close(fdo) != 0)
 		return error_errno("%s: close error", dst);
 
-	if (!status && adjust_shared_perm(the_repository, dst))
+	if (!status && adjust_shared_perm(repo, dst))
 		return -1;
 
 	return status;
 }
 
-int copy_file_with_time(const char *dst, const char *src, int mode)
+int copy_file_with_time(struct repository *repo,
+			const char *dst, const char *src, int mode)
 {
-	int status = copy_file(dst, src, mode);
+	int status = copy_file(repo, dst, src, mode);
 	if (!status)
 		return copy_times(dst, src);
 	return status;
diff --git a/copy.h b/copy.h
index 2af77cb..1059b11 100644
--- a/copy.h
+++ b/copy.h
@@ -1,10 +1,14 @@
 #ifndef COPY_H
 #define COPY_H
 
+struct repository;
+
 #define COPY_READ_ERROR (-2)
 #define COPY_WRITE_ERROR (-3)
 int copy_fd(int ifd, int ofd);
-int copy_file(const char *dst, const char *src, int mode);
-int copy_file_with_time(const char *dst, const char *src, int mode);
+int copy_file(struct repository *repo,
+	      const char *dst, const char *src, int mode);
+int copy_file_with_time(struct repository *repo,
+			const char *dst, const char *src, int mode);
 
 #endif /* COPY_H */
diff --git a/csum-file.c b/csum-file.c
index d7a682c..fe18ee1 100644
--- a/csum-file.c
+++ b/csum-file.c
@@ -57,6 +57,7 @@ void hashflush(struct hashfile *f)
 
 void free_hashfile(struct hashfile *f)
 {
+	git_hash_discard(&f->ctx);
 	free(f->buffer);
 	free(f->check_buffer);
 	free(f);
@@ -101,15 +102,6 @@ int finalize_hashfile(struct hashfile *f, unsigned char *result,
 	return fd;
 }
 
-void discard_hashfile(struct hashfile *f)
-{
-	if (0 <= f->check_fd)
-		close(f->check_fd);
-	if (0 <= f->fd)
-		close(f->fd);
-	free_hashfile(f);
-}
-
 void hashwrite(struct hashfile *f, const void *buf, uint32_t count)
 {
 	while (count) {
@@ -176,7 +168,7 @@ struct hashfile *hashfd_ext(const struct git_hash_algo *algop,
 	f->skip_hash = 0;
 
 	f->algop = unsafe_hash_algo(algop);
-	f->algop->init_fn(&f->ctx);
+	git_hash_init(&f->ctx, f->algop);
 
 	f->buffer_len = opts->buffer_len ? opts->buffer_len : DEFAULT_IO_BUFFER_SIZE;
 	f->buffer = xmalloc(f->buffer_len);
@@ -201,7 +193,7 @@ void hashfile_checkpoint_init(struct hashfile *f,
 			      struct hashfile_checkpoint *checkpoint)
 {
 	memset(checkpoint, 0, sizeof(*checkpoint));
-	f->algop->init_fn(&checkpoint->ctx);
+	git_hash_init(&checkpoint->ctx, f->algop);
 }
 
 void hashfile_checkpoint(struct hashfile *f, struct hashfile_checkpoint *checkpoint)
@@ -224,6 +216,11 @@ int hashfile_truncate(struct hashfile *f, struct hashfile_checkpoint *checkpoint
 	return 0;
 }
 
+void hashfile_checkpoint_release(struct hashfile_checkpoint *checkpoint)
+{
+	git_hash_discard(&checkpoint->ctx);
+}
+
 void crc32_begin(struct hashfile *f)
 {
 	f->crc32 = crc32(0, NULL, 0);
@@ -248,7 +245,7 @@ int hashfile_checksum_valid(const struct git_hash_algo *algop,
 	if (total_len < algop->rawsz)
 		return 0; /* say "too short"? */
 
-	algop->init_fn(&ctx);
+	git_hash_init(&ctx, algop);
 	git_hash_update(&ctx, data, data_len);
 	git_hash_final(got, &ctx);
 
diff --git a/csum-file.h b/csum-file.h
index a270738..6ed74d1 100644
--- a/csum-file.h
+++ b/csum-file.h
@@ -39,6 +39,7 @@ struct hashfile_checkpoint {
 void hashfile_checkpoint_init(struct hashfile *, struct hashfile_checkpoint *);
 void hashfile_checkpoint(struct hashfile *, struct hashfile_checkpoint *);
 int hashfile_truncate(struct hashfile *, struct hashfile_checkpoint *);
+void hashfile_checkpoint_release(struct hashfile_checkpoint *);
 
 /* finalize_hashfile flags */
 #define CSUM_CLOSE		1
@@ -74,7 +75,6 @@ void free_hashfile(struct hashfile *f);
  * Finalize the hashfile by flushing data to disk and free'ing it.
  */
 int finalize_hashfile(struct hashfile *, unsigned char *, enum fsync_component, unsigned int);
-void discard_hashfile(struct hashfile *);
 void hashwrite(struct hashfile *, const void *, uint32_t);
 void hashflush(struct hashfile *f);
 void crc32_begin(struct hashfile *);
diff --git a/diff-lib.c b/diff-lib.c
index ae91027..ac4e310 100644
--- a/diff-lib.c
+++ b/diff-lib.c
@@ -160,7 +160,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option)
 
 			changed = check_removed(ce, &st);
 			if (!changed)
-				wt_mode = ce_mode_from_stat(ce, st.st_mode);
+				wt_mode = ce_mode_from_stat(revs->repo, ce, st.st_mode);
 			else {
 				if (changed < 0) {
 					perror(ce->name);
@@ -193,7 +193,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option)
 					num_compare_stages++;
 					oidcpy(&dpath->parent[stage - 2].oid,
 					       &nce->oid);
-					dpath->parent[stage-2].mode = ce_mode_from_stat(nce, mode);
+					dpath->parent[stage-2].mode = ce_mode_from_stat(revs->repo, nce, mode);
 					dpath->parent[stage-2].status =
 						DIFF_STATUS_MODIFIED;
 				}
@@ -262,7 +262,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option)
 				continue;
 			} else if (revs->diffopt.ita_invisible_in_index &&
 				   ce_intent_to_add(ce)) {
-				newmode = ce_mode_from_stat(ce, st.st_mode);
+				newmode = ce_mode_from_stat(revs->repo, ce, st.st_mode);
 				diff_addremove(&revs->diffopt, '+', newmode,
 					       null_oid(the_hash_algo), 0, ce->name, 0);
 				continue;
@@ -270,7 +270,7 @@ void run_diff_files(struct rev_info *revs, unsigned int option)
 
 			changed = match_stat_with_submodule(&revs->diffopt, ce, &st,
 							    ce_option, &dirty_submodule);
-			newmode = ce_mode_from_stat(ce, st.st_mode);
+			newmode = ce_mode_from_stat(revs->repo, ce, st.st_mode);
 		}
 
 		if (!changed && !dirty_submodule) {
@@ -338,7 +338,7 @@ static int get_stat_data(const struct cache_entry *ce,
 		changed = match_stat_with_submodule(diffopt, ce, &st,
 						    0, dirty_submodule);
 		if (changed) {
-			mode = ce_mode_from_stat(ce, st.st_mode);
+			mode = ce_mode_from_stat(diffopt->repo, ce, st.st_mode);
 			oid = null_oid(the_hash_algo);
 		}
 	}
@@ -467,7 +467,7 @@ static void do_oneway_diff(struct unpack_trees_options *o,
 	if (cached && idx && ce_stage(idx)) {
 		struct diff_filepair *pair;
 		pair = diff_unmerge(&revs->diffopt, idx->name);
-		if (tree)
+		if (pair && tree)
 			fill_filespec(pair->one, &tree->oid, 1,
 				      tree->ce_mode);
 		return;
@@ -530,6 +530,16 @@ static int oneway_diff(const struct cache_entry * const *src,
 	if (tree == o->df_conflict_entry)
 		tree = NULL;
 
+	/*
+	 * We should only see a NULL idx when the entry was present in the tree
+	 * but deleted in the idx. In which case it should be impossible
+	 * that a NULL tree was passed in (there would have been no entry at
+	 * all) or that we got a df conflict above (you need a directory and a
+	 * file to get such a conflict, which implies both sides are present).
+	 */
+	if (!idx && !tree)
+		BUG("oneway_diff with neither idx nor tree");
+
 	if (ce_path_match(revs->diffopt.repo->index,
 			  idx ? idx : tree,
 			  &revs->prune_data, NULL)) {
diff --git a/diff.c b/diff.c
index 2a9d0d8..589c196 100644
--- a/diff.c
+++ b/diff.c
@@ -6855,7 +6855,7 @@ void flush_one_hunk(struct object_id *result, struct git_hash_ctx *ctx)
 	int i;
 
 	git_hash_final(hash, ctx);
-	the_hash_algo->init_fn(ctx);
+	git_hash_init(ctx, the_hash_algo);
 	/* 20-byte sum, with carry */
 	for (i = 0; i < the_hash_algo->rawsz; ++i) {
 		carry += result->hash[i] + hash[i];
@@ -6899,7 +6899,7 @@ static int diff_get_patch_id(struct diff_options *options, struct object_id *oid
 	struct git_hash_ctx ctx;
 	struct patch_id_t data;
 
-	the_hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, the_hash_algo);
 	memset(&data, 0, sizeof(struct patch_id_t));
 	data.ctx = &ctx;
 	oidclr(oid, the_repository->hash_algo);
@@ -6987,6 +6987,7 @@ static int diff_get_patch_id(struct diff_options *options, struct object_id *oid
 		flush_one_hunk(oid, &ctx);
 	}
 
+	git_hash_discard(&ctx);
 	return 0;
 }
 
diff --git a/diffcore-break.c b/diffcore-break.c
index 17b5ad1..b5bcc95 100644
--- a/diffcore-break.c
+++ b/diffcore-break.c
@@ -289,6 +289,8 @@ void diffcore_merge_broken(void)
 			 */
 			for (j = i + 1; j < q->nr; j++) {
 				struct diff_filepair *pp = q->queue[j];
+				if (!pp)
+					continue;
 				if (pp->broken_pair &&
 				    !strcmp(pp->one->path, pp->two->path) &&
 				    !strcmp(p->one->path, pp->two->path)) {
diff --git a/dir.c b/dir.c
index 3243009..95d8a1c 100644
--- a/dir.c
+++ b/dir.c
@@ -126,7 +126,7 @@ int count_slashes(const char *s)
 
 int git_fspathcmp(const char *a, const char *b)
 {
-	return ignore_case ? strcasecmp(a, b) : strcmp(a, b);
+	return repo_ignore_case(the_repository) ? strcasecmp(a, b) : strcmp(a, b);
 }
 
 int fspatheq(const char *a, const char *b)
@@ -136,7 +136,7 @@ int fspatheq(const char *a, const char *b)
 
 int git_fspathncmp(const char *a, const char *b, size_t count)
 {
-	return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count);
+	return repo_ignore_case(the_repository) ? strncasecmp(a, b, count) : strncmp(a, b, count);
 }
 
 int paths_collide(const char *a, const char *b)
@@ -153,7 +153,7 @@ int paths_collide(const char *a, const char *b)
 
 unsigned int fspathhash(const char *str)
 {
-	return ignore_case ? strihash(str) : strhash(str);
+	return repo_ignore_case(the_repository) ? strihash(str) : strhash(str);
 }
 
 int git_fnmatch(const struct pathspec_item *item,
@@ -202,7 +202,7 @@ static int fnmatch_icase_mem(const char *pattern, int patternlen,
 		use_str = str_buf.buf;
 	}
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		flags |= WM_CASEFOLD;
 	match_status = wildmatch(use_pat, use_str, flags);
 
@@ -1851,7 +1851,7 @@ static struct dir_entry *dir_add_name(struct dir_struct *dir,
 				      struct index_state *istate,
 				      const char *pathname, int len)
 {
-	if (index_file_exists(istate, pathname, len, ignore_case))
+	if (index_file_exists(istate, pathname, len, repo_ignore_case(the_repository)))
 		return NULL;
 
 	ALLOC_GROW(dir->entries, dir->nr+1, dir->internal.alloc);
@@ -1888,7 +1888,7 @@ static enum exist_status directory_exists_in_index_icase(struct index_state *ist
 	if (index_dir_exists(istate, dirname, len))
 		return index_directory;
 
-	ce = index_file_exists(istate, dirname, len, ignore_case);
+	ce = index_file_exists(istate, dirname, len, repo_ignore_case(the_repository));
 	if (ce && S_ISGITLINK(ce->ce_mode))
 		return index_gitdir;
 
@@ -1907,7 +1907,7 @@ static enum exist_status directory_exists_in_index(struct index_state *istate,
 {
 	int pos;
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		return directory_exists_in_index_icase(istate, dirname, len);
 
 	pos = index_name_pos(istate, dirname, len);
@@ -2447,7 +2447,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
 
 	/* Always exclude indexed files */
 	has_path_in_index = !!index_file_exists(istate, path->buf, path->len,
-						ignore_case);
+						repo_ignore_case(the_repository));
 	if (dtype != DT_DIR && has_path_in_index)
 		return path_none;
 
@@ -3201,7 +3201,7 @@ static int cmp_icase(char a, char b)
 {
 	if (a == b)
 		return 0;
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		return toupper(a) - toupper(b);
 	return a - b;
 }
@@ -3481,11 +3481,11 @@ static GIT_PATH_FUNC(git_path_info_exclude, "info/exclude")
 
 void setup_standard_excludes(struct dir_struct *dir)
 {
+	const char *excludes_file = repo_excludes_file(the_repository);
+
 	dir->exclude_per_dir = ".gitignore";
 
 	/* core.excludesfile defaulting to $XDG_CONFIG_HOME/git/ignore */
-	if (!excludes_file)
-		excludes_file = xdg_config_home("ignore");
 	if (excludes_file && !access_or_warn(excludes_file, R_OK, 0))
 		add_patterns_from_file_1(dir, excludes_file,
 					 dir->untracked ? &dir->internal.ss_excludes_file : NULL);
@@ -3792,13 +3792,18 @@ static int read_one_dir(struct untracked_cache_dir **untracked_,
 		ALLOC_ARRAY(ud.untracked, ud.untracked_nr);
 
 	ud.dirs_alloc = ud.dirs_nr = decode_varint(&data);
-	if (data > end)
+	if (data > end) {
+		free(ud.untracked);
 		return -1;
+	}
 	ALLOC_ARRAY(ud.dirs, ud.dirs_nr);
 
 	eos = memchr(data, '\0', end - data);
-	if (!eos || eos == end)
+	if (!eos || eos == end) {
+		free(ud.untracked);
+		free(ud.dirs);
 		return -1;
+	}
 
 	*untracked_ = untracked = xmalloc(st_add3(sizeof(*untracked), eos - data, 1));
 	memcpy(untracked, &ud, sizeof(ud));
diff --git a/editor.c b/editor.c
index fd174e6a..0d1cb87 100644
--- a/editor.c
+++ b/editor.c
@@ -29,8 +29,8 @@ const char *git_editor(void)
 	const char *editor = getenv("GIT_EDITOR");
 	int terminal_is_dumb = is_terminal_dumb();
 
-	if (!editor && editor_program)
-		editor = editor_program;
+	if (!editor)
+		editor = repo_config_values(the_repository)->editor_program;
 	if (!editor && !terminal_is_dumb)
 		editor = getenv("VISUAL");
 	if (!editor)
diff --git a/entry.c b/entry.c
index 6b79884..1c4f0f4 100644
--- a/entry.c
+++ b/entry.c
@@ -319,7 +319,8 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca
 		 * We can't make a real symlink; write out a regular file entry
 		 * with the symlink destination as its contents.
 		 */
-		if (!has_symlinks || to_tempfile)
+		if (!repo_has_symlinks(state->istate && state->istate->repo ?
+				       state->istate->repo : the_repository) || to_tempfile)
 			goto write_file_entry;
 
 		ret = symlink(new_blob, path);
diff --git a/environment.c b/environment.c
index 8f0c1c4..76ee65e 100644
--- a/environment.c
+++ b/environment.c
@@ -41,32 +41,21 @@
 static int pack_compression_seen;
 static int zlib_compression_seen;
 
-int trust_executable_bit = 1;
-int has_symlinks = 1;
 int minimum_abbrev = 4, default_abbrev = -1;
-int ignore_case;
 int assume_unchanged;
 char *git_commit_encoding;
 char *git_log_output_encoding;
-char *apply_default_whitespace;
-char *apply_default_ignorewhitespace;
 int fsync_object_files = -1;
 int use_fsync = -1;
 enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT;
 enum fsync_component fsync_components = FSYNC_COMPONENTS_DEFAULT;
-char *editor_program;
-char *askpass_program;
-char *excludes_file;
 enum auto_crlf auto_crlf = AUTO_CRLF_FALSE;
 enum eol core_eol = EOL_UNSET;
 int global_conv_flags_eol = CONV_EOL_RNDTRP_WARN;
 char *check_roundtrip_encoding;
-enum rebase_setup_type autorebase = AUTOREBASE_NEVER;
-enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
 #ifndef OBJECT_CREATION_MODE
 #define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS
 #endif
-enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
 int grafts_keep_true_parents;
 unsigned long pack_size_limit_cfg;
 
@@ -142,6 +131,37 @@ int repo_protect_hfs(struct repository *repo)
 		PROTECT_HFS_DEFAULT;
 }
 
+int repo_ignore_case(struct repository *repo)
+{
+	return (repo && repo->initialized) ?
+		repo_config_values(repo)->ignore_case :
+		0;
+}
+
+int repo_trust_executable_bit(struct repository *repo)
+{
+	return repo->initialized
+		? repo_config_values(repo)->trust_executable_bit
+		: 1;
+}
+
+int repo_has_symlinks(struct repository *repo)
+{
+	return repo->initialized
+		? repo_config_values(repo)->has_symlinks
+		: platform_has_symlinks();
+}
+
+const char *repo_excludes_file(struct repository *repo)
+{
+	struct repo_config_values *cfg = repo_config_values(repo);
+
+	if (!cfg->excludes_file)
+		cfg->excludes_file = xdg_config_home("ignore");
+
+	return cfg->excludes_file;
+}
+
 int have_git_dir(void)
 {
 	return startup_info->have_repository
@@ -305,7 +325,7 @@ int git_default_core_config(const char *var, const char *value,
 
 	/* This needs a better name */
 	if (!strcmp(var, "core.filemode")) {
-		trust_executable_bit = git_config_bool(var, value);
+		cfg->trust_executable_bit = git_config_bool(var, value);
 		return 0;
 	}
 	if (!strcmp(var, "core.trustctime")) {
@@ -330,12 +350,13 @@ int git_default_core_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "core.symlinks")) {
-		has_symlinks = git_config_bool(var, value);
+		struct repo_config_values *cfg = repo_config_values(the_repository);
+		cfg->has_symlinks = git_config_bool(var, value);
 		return 0;
 	}
 
 	if (!strcmp(var, "core.ignorecase")) {
-		ignore_case = git_config_bool(var, value);
+		cfg->ignore_case = git_config_bool(var, value);
 		return 0;
 	}
 
@@ -436,8 +457,8 @@ int git_default_core_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "core.editor")) {
-		FREE_AND_NULL(editor_program);
-		return git_config_string(&editor_program, var, value);
+		FREE_AND_NULL(cfg->editor_program);
+		return git_config_string(&cfg->editor_program, var, value);
 	}
 
 	if (!strcmp(var, "core.commentchar") ||
@@ -464,13 +485,13 @@ int git_default_core_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "core.askpass")) {
-		FREE_AND_NULL(askpass_program);
-		return git_config_string(&askpass_program, var, value);
+		FREE_AND_NULL(cfg->askpass_program);
+		return git_config_string(&cfg->askpass_program, var, value);
 	}
 
 	if (!strcmp(var, "core.excludesfile")) {
-		FREE_AND_NULL(excludes_file);
-		return git_config_pathname(&excludes_file, var, value);
+		FREE_AND_NULL(cfg->excludes_file);
+		return git_config_pathname(&cfg->excludes_file, var, value);
 	}
 
 	if (!strcmp(var, "core.whitespace")) {
@@ -517,9 +538,9 @@ int git_default_core_config(const char *var, const char *value,
 		if (!value)
 			return config_error_nonbool(var);
 		if (!strcmp(value, "rename"))
-			object_creation_mode = OBJECT_CREATION_USES_RENAMES;
+			cfg->object_creation_mode = OBJECT_CREATION_USES_RENAMES;
 		else if (!strcmp(value, "link"))
-			object_creation_mode = OBJECT_CREATION_USES_HARDLINKS;
+			cfg->object_creation_mode = OBJECT_CREATION_USES_HARDLINKS;
 		else
 			die(_("invalid mode for object creation: %s"), value);
 		return 0;
@@ -605,13 +626,13 @@ static int git_default_branch_config(const char *var, const char *value)
 		if (!value)
 			return config_error_nonbool(var);
 		else if (!strcmp(value, "never"))
-			autorebase = AUTOREBASE_NEVER;
+			cfg->autorebase = AUTOREBASE_NEVER;
 		else if (!strcmp(value, "local"))
-			autorebase = AUTOREBASE_LOCAL;
+			cfg->autorebase = AUTOREBASE_LOCAL;
 		else if (!strcmp(value, "remote"))
-			autorebase = AUTOREBASE_REMOTE;
+			cfg->autorebase = AUTOREBASE_REMOTE;
 		else if (!strcmp(value, "always"))
-			autorebase = AUTOREBASE_ALWAYS;
+			cfg->autorebase = AUTOREBASE_ALWAYS;
 		else
 			return error(_("malformed value for %s"), var);
 		return 0;
@@ -623,21 +644,23 @@ static int git_default_branch_config(const char *var, const char *value)
 
 static int git_default_push_config(const char *var, const char *value)
 {
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+
 	if (!strcmp(var, "push.default")) {
 		if (!value)
 			return config_error_nonbool(var);
 		else if (!strcmp(value, "nothing"))
-			push_default = PUSH_DEFAULT_NOTHING;
+			cfg->push_default = PUSH_DEFAULT_NOTHING;
 		else if (!strcmp(value, "matching"))
-			push_default = PUSH_DEFAULT_MATCHING;
+			cfg->push_default = PUSH_DEFAULT_MATCHING;
 		else if (!strcmp(value, "simple"))
-			push_default = PUSH_DEFAULT_SIMPLE;
+			cfg->push_default = PUSH_DEFAULT_SIMPLE;
 		else if (!strcmp(value, "upstream"))
-			push_default = PUSH_DEFAULT_UPSTREAM;
+			cfg->push_default = PUSH_DEFAULT_UPSTREAM;
 		else if (!strcmp(value, "tracking")) /* deprecated */
-			push_default = PUSH_DEFAULT_UPSTREAM;
+			cfg->push_default = PUSH_DEFAULT_UPSTREAM;
 		else if (!strcmp(value, "current"))
-			push_default = PUSH_DEFAULT_CURRENT;
+			cfg->push_default = PUSH_DEFAULT_CURRENT;
 		else {
 			error(_("malformed value for %s: %s"), var, value);
 			return error(_("must be one of nothing, matching, simple, "
@@ -723,9 +746,21 @@ int git_default_config(const char *var, const char *value,
 void repo_config_values_init(struct repo_config_values *cfg)
 {
 	cfg->attributes_file = NULL;
+	cfg->excludes_file = NULL;
+	cfg->editor_program = NULL;
+	cfg->pager_program = NULL;
+	cfg->askpass_program = NULL;
+	cfg->apply_default_whitespace = NULL;
+	cfg->apply_default_ignorewhitespace = NULL;
+	cfg->push_default = PUSH_DEFAULT_UNSPECIFIED;
+	cfg->autorebase = AUTOREBASE_NEVER;
+	cfg->object_creation_mode = OBJECT_CREATION_MODE;
 	cfg->apply_sparse_checkout = 0;
 	cfg->protect_hfs = PROTECT_HFS_DEFAULT;
 	cfg->protect_ntfs = PROTECT_NTFS_DEFAULT;
+	cfg->ignore_case = 0;
+	cfg->trust_executable_bit = 1;
+	cfg->has_symlinks = platform_has_symlinks();
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
 	cfg->trust_ctime = 1;
 	cfg->check_stat = 1;
@@ -736,3 +771,14 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->sparse_expect_files_outside_of_patterns = 0;
 	cfg->warn_on_object_refname_ambiguity = 1;
 }
+
+void repo_config_values_clear(struct repo_config_values *cfg)
+{
+	FREE_AND_NULL(cfg->attributes_file);
+	FREE_AND_NULL(cfg->excludes_file);
+	FREE_AND_NULL(cfg->editor_program);
+	FREE_AND_NULL(cfg->pager_program);
+	FREE_AND_NULL(cfg->askpass_program);
+	FREE_AND_NULL(cfg->apply_default_whitespace);
+	FREE_AND_NULL(cfg->apply_default_ignorewhitespace);
+}
diff --git a/environment.h b/environment.h
index 8aaedcf..e7ec5b0 100644
--- a/environment.h
+++ b/environment.h
@@ -87,9 +87,45 @@ extern const char * const local_repo_env[];
 struct strvec;
 
 struct repository;
+
+/*
+ * NEEDSWORK: It would be better if these definitions could be moved to
+ * other more specific files, but care is needed to avoid circular
+ * inclusion issues.
+ */
+enum push_default_type {
+	PUSH_DEFAULT_NOTHING = 0,
+	PUSH_DEFAULT_MATCHING,
+	PUSH_DEFAULT_SIMPLE,
+	PUSH_DEFAULT_UPSTREAM,
+	PUSH_DEFAULT_CURRENT,
+	PUSH_DEFAULT_UNSPECIFIED
+};
+
+enum rebase_setup_type {
+	AUTOREBASE_NEVER = 0,
+	AUTOREBASE_LOCAL,
+	AUTOREBASE_REMOTE,
+	AUTOREBASE_ALWAYS
+};
+
+enum object_creation_mode {
+	OBJECT_CREATION_USES_HARDLINKS = 0,
+	OBJECT_CREATION_USES_RENAMES = 1
+};
+
 struct repo_config_values {
 	/* section "core" config values */
 	char *attributes_file;
+	char *excludes_file;
+	char *editor_program;
+	char *pager_program;
+	char *askpass_program;
+	char *apply_default_whitespace;
+	char *apply_default_ignorewhitespace;
+	enum push_default_type push_default;
+	enum rebase_setup_type autorebase;
+	enum object_creation_mode object_creation_mode;
 	int apply_sparse_checkout;
 	int trust_ctime;
 	int check_stat;
@@ -100,6 +136,9 @@ struct repo_config_values {
 	int warn_on_object_refname_ambiguity;
 	int protect_hfs;
 	int protect_ntfs;
+	int ignore_case;
+	int trust_executable_bit;
+	int has_symlinks;
 
 	/* section "sparse" config values */
 	int sparse_expect_files_outside_of_patterns;
@@ -143,11 +182,33 @@ int git_default_core_config(const char *var, const char *value,
 int repo_protect_hfs(struct repository *repo);
 int repo_protect_ntfs(struct repository *repo);
 
+/*
+ * Getter for the `ignore_case` field of `struct repo_config_values`.
+ * It checks `repo->initialized` to prevent calling repo_config_values()`
+ * before the repository setup is fully complete or in non-git environments.
+ */
+int repo_ignore_case(struct repository *repo);
+
+int repo_trust_executable_bit(struct repository *repo);
+
+int repo_has_symlinks(struct repository *repo);
+
+const char *repo_excludes_file(struct repository *repo);
+
 void repo_config_values_init(struct repo_config_values *cfg);
 
 int is_bare_repository(struct repository *repo);
 
 /*
+ * Frees memory allocated for dynamically loaded configuration values
+ * inside `repo_config_values`.
+ *
+ * As dynamically allocated variables are migrated into this struct,
+ * their FREE_AND_NULL() calls should be appended here.
+ */
+void repo_config_values_clear(struct repo_config_values *cfg);
+
+/*
  * TODO: All the below state either explicitly or implicitly relies on
  * `the_repository`. We should eventually get rid of these and make the
  * dependency on a repository explicit:
@@ -170,39 +231,10 @@ int is_bare_repository(struct repository *repo);
 int have_git_dir(void);
 
 /* Environment bits from configuration mechanism */
-extern int trust_executable_bit;
-extern int has_symlinks;
 extern int minimum_abbrev, default_abbrev;
-extern int ignore_case;
 extern int assume_unchanged;
-extern char *apply_default_whitespace;
-extern char *apply_default_ignorewhitespace;
 extern unsigned long pack_size_limit_cfg;
 
-enum rebase_setup_type {
-	AUTOREBASE_NEVER = 0,
-	AUTOREBASE_LOCAL,
-	AUTOREBASE_REMOTE,
-	AUTOREBASE_ALWAYS
-};
-extern enum rebase_setup_type autorebase;
-
-enum push_default_type {
-	PUSH_DEFAULT_NOTHING = 0,
-	PUSH_DEFAULT_MATCHING,
-	PUSH_DEFAULT_SIMPLE,
-	PUSH_DEFAULT_UPSTREAM,
-	PUSH_DEFAULT_CURRENT,
-	PUSH_DEFAULT_UNSPECIFIED
-};
-extern enum push_default_type push_default;
-
-enum object_creation_mode {
-	OBJECT_CREATION_USES_HARDLINKS = 0,
-	OBJECT_CREATION_USES_RENAMES = 1
-};
-extern enum object_creation_mode object_creation_mode;
-
 extern int grafts_keep_true_parents;
 
 const char *get_log_output_encoding(void);
@@ -211,10 +243,6 @@ const char *get_commit_output_encoding(void);
 extern char *git_commit_encoding;
 extern char *git_log_output_encoding;
 
-extern char *editor_program;
-extern char *askpass_program;
-extern char *excludes_file;
-
 /*
  * The character that begins a commented line in user-editable file
  * that is subject to stripspace.
diff --git a/fetch-object-info.c b/fetch-object-info.c
new file mode 100644
index 0000000..ba7e179
--- /dev/null
+++ b/fetch-object-info.c
@@ -0,0 +1,156 @@
+#include "git-compat-util.h"
+#include "gettext.h"
+#include "hex.h"
+#include "pkt-line.h"
+#include "connect.h"
+#include "oid-array.h"
+#include "odb.h"
+#include "fetch-object-info.h"
+#include "string-list.h"
+
+/* Sends object-info command and its arguments into the request buffer. */
+static void send_object_info_request(const int fd_out, struct object_info_args *args)
+{
+	struct strbuf req_buf = STRBUF_INIT;
+
+	write_command_and_capabilities(&req_buf, "object-info", args->server_options);
+
+	if (unsorted_string_list_has_string(args->object_info_options, "size"))
+		packet_buf_write(&req_buf, "size");
+	else if (args->object_info_options->nr)
+		BUG("only size should be in object_info_options");
+
+	if (args->oids)
+		for (size_t i = 0; i < args->oids->nr; i++)
+			packet_buf_write(&req_buf, "oid %s", oid_to_hex(&args->oids->oid[i]));
+
+	packet_buf_flush(&req_buf);
+	if (write_in_full(fd_out, req_buf.buf, req_buf.len) < 0)
+		die_errno(_("unable to write request to remote"));
+
+	strbuf_release(&req_buf);
+}
+
+static int parse_object_size(const char *s, size_t *res)
+{
+	uintmax_t uim;
+
+	if (!s[0] || s[strspn(s, "0123456789")])
+		return -1;
+	errno = 0;
+	uim = strtoumax(s, NULL, 10);
+	if (errno || uim > SIZE_MAX)
+		return -1;
+	*res = uim;
+	return 0;
+}
+
+int fetch_object_info(const enum protocol_version version, struct object_info_args *args,
+		      struct packet_reader *reader, struct object_info *object_info_data,
+		      const int stateless_rpc, const int fd_out)
+{
+	int size_index = -1;
+
+	switch (version) {
+	case protocol_v2:
+		if (!server_supports_v2("object-info"))
+			die(_("object-info capability is not enabled on the server"));
+		/*
+		 * When removing an element from the list it gets swapped by the
+		 * last element, iterate backwards to prevent elements skipping
+		 * evaluation.
+		 *
+		 * object_info_options->nr can be safely casted without overflow
+		 * because the number of options is a small known number (the
+		 * supported placeholders which currently are size and type).
+		 */
+		for (int i = (int)args->object_info_options->nr - 1; i >= 0; i--)
+			if (!server_supports_feature("object-info",
+						     args->object_info_options->items[i].string, 0))
+				unsorted_string_list_delete_item(args->object_info_options, i, 0);
+
+		/*
+		 * Even if no options are left, we still send the oid so we get
+		 * at least an existence check.
+		 */
+		send_object_info_request(fd_out, args);
+		break;
+	case protocol_v1:
+	case protocol_v0:
+		die(_("object-info requires protocol v2"));
+	case protocol_unknown_version:
+		BUG("unknown protocol version");
+	}
+
+	for (size_t i = 0; i < args->object_info_options->nr; i++) {
+		if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
+			check_stateless_delimiter(stateless_rpc, reader,
+						  "stateless delimiter expected");
+			return -1;
+		}
+
+		if (!unsorted_string_list_has_string(args->object_info_options, reader->line))
+			return -1;
+
+		if (!strcmp(reader->line, "size")) {
+			/*
+			 * i is the number of supported options which currently
+			 * is only size. No risk of overflow.
+			 */
+			size_index = (int)i;
+			for (size_t j = 0; j < args->oids->nr; j++)
+				object_info_data[j].sizep =
+					xcalloc(1, sizeof(*object_info_data[j].sizep));
+		} else {
+			BUG("only size is supported");
+		}
+	}
+
+	for (size_t i = 0;
+	     packet_reader_read(reader) == PACKET_READ_NORMAL &&
+	     i < args->oids->nr;
+	     i++) {
+		struct string_list object_info_values = STRING_LIST_INIT_DUP;
+
+		string_list_split(&object_info_values, reader->line, " ", -1);
+
+		if (strcmp(object_info_values.items[0].string,
+			   oid_to_hex(&args->oids->oid[i])))
+			die(_("object-info: expected OID: %s, got %s"),
+			    oid_to_hex(&args->oids->oid[i]),
+			    object_info_values.items[0].string);
+
+		/*
+		 * If the response is two elements but the second one is an
+		 * empty string, that means that the OID is unrecognized by the
+		 * server.
+		 */
+		if (object_info_values.nr >= 2 &&
+		    !strcmp(object_info_values.items[1].string, "")) {
+			object_info_data[i].unrecognized = 1;
+			string_list_clear(&object_info_values, 0);
+			continue;
+		}
+
+		/*
+		 * Because we filter the options to be only the supported by
+		 * the server we expect the server to answer with the same
+		 * number of attributes requested.
+		 */
+		if (args->object_info_options->nr + 1 != object_info_values.nr)
+			die("object-info: unexpected number of attributes: %s",
+			    reader->line);
+
+		if (size_index >= 0 &&
+		    parse_object_size(object_info_values.items[size_index + 1].string,
+				      object_info_data[i].sizep))
+			die("object-info: ref %s has invalid size %s",
+			    object_info_values.items[0].string,
+			    object_info_values.items[size_index + 1].string);
+
+		string_list_clear(&object_info_values, 0);
+	}
+	check_stateless_delimiter(stateless_rpc, reader, "stateless delimiter expected");
+
+	return 0;
+}
diff --git a/fetch-object-info.h b/fetch-object-info.h
new file mode 100644
index 0000000..269cebb
--- /dev/null
+++ b/fetch-object-info.h
@@ -0,0 +1,25 @@
+#ifndef FETCH_OBJECT_INFO_H
+#define FETCH_OBJECT_INFO_H
+
+#include "pkt-line.h"
+#include "protocol.h"
+
+struct object_info_args {
+	struct string_list *object_info_options;
+	const struct string_list *server_options;
+	struct oid_array *oids;
+};
+
+struct object_info;
+/*
+ * Sends git-cat-file object-info command into the request buf and read the
+ * results from packets.
+ *
+ * Modifies args->object_info_options, on return it contains only the supported
+ * options by the server.
+ */
+int fetch_object_info(enum protocol_version version, struct object_info_args *args,
+		      struct packet_reader *reader, struct object_info *object_info_data,
+		      int stateless_rpc, int fd_out);
+
+#endif /* FETCH_OBJECT_INFO_H */
diff --git a/fetch-pack.c b/fetch-pack.c
index 120e01f..922a9b2 100644
--- a/fetch-pack.c
+++ b/fetch-pack.c
@@ -49,7 +49,6 @@ static int fetch_fsck_objects = -1;
 static int transfer_fsck_objects = -1;
 static int agent_supported;
 static int server_supports_filtering;
-static int advertise_sid;
 static struct shallow_lock shallow_lock;
 static const char *alternate_shallow_file;
 static struct strbuf fsck_msg_types = STRBUF_INIT;
@@ -363,6 +362,9 @@ static int find_common(struct fetch_negotiator *negotiator,
 	size_t state_len = 0;
 	struct packet_reader reader;
 	struct oidset negotiation_include_oids = OIDSET_INIT;
+	int advertise_sid = 0;
+
+	repo_config_get_bool(the_repository, "transfer.advertisesid", &advertise_sid);
 
 	if (args->stateless_rpc && multi_ack == 1)
 		die(_("the option '%s' requires '%s'"), "--stateless-rpc", "multi_ack_detailed");
@@ -414,7 +416,7 @@ static int find_common(struct fetch_negotiator *negotiator,
 			if (deepen_not_ok)      strbuf_addstr(&c, " deepen-not");
 			if (agent_supported)    strbuf_addf(&c, " agent=%s",
 							    git_user_agent_sanitized());
-			if (advertise_sid)
+			if (advertise_sid && server_supports("session-id"))
 				strbuf_addf(&c, " session-id=%s", trace2_session_id());
 			if (args->filter_options.choice)
 				strbuf_addstr(&c, " filter");
@@ -662,8 +664,8 @@ static int mark_complete_oid(const struct reference *ref, void *cb_data UNUSED)
 static void mark_recent_complete_commits(struct fetch_pack_args *args,
 					 timestamp_t cutoff)
 {
-	while (complete.nr) {
-		struct commit *item = prio_queue_peek(&complete);
+	struct commit *item;
+	while ((item = prio_queue_peek(&complete))) {
 		if (item->date < cutoff)
 			break;
 		print_verbose(args, _("Marking %s as complete"),
@@ -1160,9 +1162,6 @@ static struct ref *do_fetch_pack(struct fetch_pack_args *args,
 				      (int)agent_len, agent_feature);
 	}
 
-	if (!server_supports("session-id"))
-		advertise_sid = 0;
-
 	if (server_supports("shallow"))
 		print_verbose(args, _("Server supports %s"), "shallow");
 	else if (args->depth > 0 || is_repository_shallow(r))
@@ -1376,38 +1375,6 @@ static int add_haves(struct fetch_negotiator *negotiator,
 	return haves_added;
 }
 
-static void write_fetch_command_and_capabilities(struct strbuf *req_buf,
-						 const struct string_list *server_options)
-{
-	const char *hash_name;
-
-	ensure_server_supports_v2("fetch");
-	packet_buf_write(req_buf, "command=fetch");
-	if (server_supports_v2("agent"))
-		packet_buf_write(req_buf, "agent=%s", git_user_agent_sanitized());
-	if (advertise_sid && server_supports_v2("session-id"))
-		packet_buf_write(req_buf, "session-id=%s", trace2_session_id());
-	if (server_options && server_options->nr) {
-		int i;
-		ensure_server_supports_v2("server-option");
-		for (i = 0; i < server_options->nr; i++)
-			packet_buf_write(req_buf, "server-option=%s",
-					 server_options->items[i].string);
-	}
-
-	if (server_feature_v2("object-format", &hash_name)) {
-		int hash_algo = hash_algo_by_name(hash_name);
-		if (hash_algo_by_ptr(the_hash_algo) != hash_algo)
-			die(_("mismatched algorithms: client %s; server %s"),
-			    the_hash_algo->name, hash_name);
-		packet_buf_write(req_buf, "object-format=%s", the_hash_algo->name);
-	} else if (hash_algo_by_ptr(the_hash_algo) != GIT_HASH_SHA1_LEGACY) {
-		die(_("the server does not support algorithm '%s'"),
-		    the_hash_algo->name);
-	}
-	packet_buf_delim(req_buf);
-}
-
 static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out,
 			      struct fetch_pack_args *args,
 			      const struct ref *wants, struct oidset *common,
@@ -1419,7 +1386,7 @@ static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out,
 	int done_sent = 0;
 	struct strbuf req_buf = STRBUF_INIT;
 
-	write_fetch_command_and_capabilities(&req_buf, args->server_options);
+	write_command_and_capabilities(&req_buf, "fetch", args->server_options);
 
 	if (args->use_thin_pack)
 		packet_buf_write(&req_buf, "thin-pack");
@@ -1768,18 +1735,18 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
 		reader.me = "fetch-pack";
 	}
 
+	/* v2 supports these by default */
+	allow_unadvertised_object_request |= ALLOW_REACHABLE_SHA1;
+	use_sideband = 2;
+	if (args->depth > 0 || args->deepen_since || args->deepen_not)
+		args->deepen = 1;
+
 	while (state != FETCH_DONE) {
 		switch (state) {
 		case FETCH_CHECK_LOCAL:
 			sort_ref_list(&ref, ref_compare_name);
 			QSORT(sought, nr_sought, cmp_ref_by_name);
 
-			/* v2 supports these by default */
-			allow_unadvertised_object_request |= ALLOW_REACHABLE_SHA1;
-			use_sideband = 2;
-			if (args->depth > 0 || args->deepen_since || args->deepen_not)
-				args->deepen = 1;
-
 			/* Filter 'ref' by 'sought' and those that aren't local */
 			mark_complete_and_common_ref(negotiator, args, &ref);
 			filter_refs(args, &ref, sought, nr_sought);
@@ -1999,7 +1966,6 @@ static void fetch_pack_config(void)
 	repo_config_get_bool(the_repository, "repack.usedeltabaseoffset", &prefer_ofs_delta);
 	repo_config_get_bool(the_repository, "fetch.fsckobjects", &fetch_fsck_objects);
 	repo_config_get_bool(the_repository, "transfer.fsckobjects", &transfer_fsck_objects);
-	repo_config_get_bool(the_repository, "transfer.advertisesid", &advertise_sid);
 	if (!uri_protocols.nr) {
 		char *str;
 
@@ -2287,7 +2253,7 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips,
 					   the_repository, "%d",
 					   negotiation_round);
 		strbuf_reset(&req_buf);
-		write_fetch_command_and_capabilities(&req_buf, server_options);
+		write_command_and_capabilities(&req_buf, "fetch", server_options);
 
 		packet_buf_write(&req_buf, "wait-for-done");
 
diff --git a/fsmonitor.c b/fsmonitor.c
index d07dc18..1077675 100644
--- a/fsmonitor.c
+++ b/fsmonitor.c
@@ -453,7 +453,7 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
 	 * case-insensitive file system, try again using the name-hash
 	 * and dir-name-hash.
 	 */
-	if (!nr_in_cone && ignore_case) {
+	if (!nr_in_cone && repo_ignore_case(the_repository)) {
 		nr_in_cone = handle_using_name_hash_icase(istate, name);
 		if (!nr_in_cone)
 			nr_in_cone = handle_using_dir_name_hash_icase(
diff --git a/git-compat-util.h b/git-compat-util.h
index 8809776..a0f901c 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -245,6 +245,10 @@ static inline int git_is_dir_sep(int c)
 #define is_dir_sep git_is_dir_sep
 #endif
 
+#ifndef platform_has_symlinks
+#define platform_has_symlinks() 1
+#endif
+
 #ifndef offset_1st_component
 static inline int git_offset_1st_component(const char *path)
 {
diff --git a/git-gui/Makefile b/git-gui/Makefile
index d33204e..2e1711a 100644
--- a/git-gui/Makefile
+++ b/git-gui/Makefile
@@ -69,8 +69,7 @@
 	QUIET          = @
 	QUIET_GEN      = $(QUIET)echo '   ' GEN '$@' &&
 	QUIET_INDEX    = $(QUIET)echo '   ' INDEX $(dir $@) &&
-	QUIET_MSGFMT0  = $(QUIET)printf '    MSGFMT %12s ' $@ && v=`
-	QUIET_MSGFMT1  = 2>&1` && echo "$$v" | sed -e 's/fuzzy translations/fuzzy/' | sed -e 's/ messages*//g'
+	QUIET_MSGFMT   = $(QUIET)echo '   ' MSGFMT '$@' &&
 
 	INSTALL_D0 = dir=
 	INSTALL_D1 = && echo ' ' DEST $$dir && $(INSTALL) -d -m 755 "$$dir"
@@ -155,7 +154,7 @@
 update-po:: $(PO_TEMPLATE)
 	$(foreach p, $(ALL_POFILES), echo Updating $p ; msgmerge -U $p $(PO_TEMPLATE) ; )
 $(ALL_MSGFILES): %.msg : %.po
-	$(QUIET_MSGFMT0)$(MSGFMT) --statistics --tcl -l $(basename $(notdir $<)) -d $(dir $@) $< $(QUIET_MSGFMT1)
+	$(QUIET_MSGFMT)$(MSGFMT) --tcl -l $(basename $(notdir $<)) -d $(dir $@) $<
 
 lib/tclIndex: $(ALL_LIBFILES) generate-tclindex.sh GIT-GUI-BUILD-OPTIONS
 	$(QUIET_INDEX)$(SHELL_PATH) generate-tclindex.sh . ./GIT-GUI-BUILD-OPTIONS $(ALL_LIBFILES)
diff --git a/git-gui/lib/option.tcl b/git-gui/lib/option.tcl
index 487d706..43f79ff 100644
--- a/git-gui/lib/option.tcl
+++ b/git-gui/lib/option.tcl
@@ -155,7 +155,7 @@
 		{i-0..300 gui.blamehistoryctx {mc "Blame History Context Radius (days)"}}
 		{i-1..99 gui.diffcontext {mc "Number of Diff Context Lines"}}
 		{t gui.diffopts {mc "Additional Diff Parameters"}}
-		{i-0..99 gui.commitmsgwidth {mc "Commit Message Text Width"}}
+		{i-0..9999 gui.commitmsgwidth {mc "Commit Message Text Width"}}
 		{t gui.newbranchtemplate {mc "New Branch Name Template"}}
 		{c gui.encoding {mc "Default File Contents Encoding"}}
 		{b gui.warndetachedcommit {mc "Warn before committing to a detached head"}}
diff --git a/git-gui/po/bg.po b/git-gui/po/bg.po
index ff5adb2..472863d 100644
--- a/git-gui/po/bg.po
+++ b/git-gui/po/bg.po
@@ -8,8 +8,8 @@
 msgstr ""
 "Project-Id-Version: git-gui master\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-07-22 17:37+0200\n"
-"PO-Revision-Date: 2026-01-19 10:12+0100\n"
+"POT-Creation-Date: 2026-06-02 19:13+0200\n"
+"PO-Revision-Date: 2026-06-14 22:57+0200\n"
 "Last-Translator: Alexander Shopov <ash@kambanaria.org>\n"
 "Language-Team: Bulgarian <dict@fsa-bg.org>\n"
 "Language: bg\n"
@@ -43,18 +43,21 @@
 msgid "git returned:"
 msgstr "git върна:"
 
+msgid "Invalid configuration:"
+msgstr "Неправилна настройка:"
+
+msgid "Unusable repo/worktree:"
+msgstr "Неизползваемо хранилище/раб. дърво:"
+
 msgid "Git directory not found:"
 msgstr "Директорията на Git не е открита:"
 
-msgid "Cannot move to top of working directory:"
-msgstr "Не може да се премине към родителската директория."
+msgid "No working directory"
+msgstr "Работната директория липсва"
 
 msgid "Cannot use bare repository:"
 msgstr "Голо хранилище не може да се използва:"
 
-msgid "No working directory"
-msgstr "Работната директория липсва"
-
 msgid "Refreshing file status..."
 msgstr "Обновяване на състоянието на файла…"
 
@@ -305,12 +308,21 @@
 msgid "Usage"
 msgstr "Употреба"
 
-msgid "Error"
-msgstr "Грешка"
+#, tcl-format
+msgid "fatal: no such file '%s' in worktree"
+msgstr "фатална грешка: в работното дърво липсва файл „%s“"
 
 #, tcl-format
-msgid "fatal: cannot stat path %s: No such file or directory"
-msgstr "ФАТАЛНА ГРЕШКА: пътят „%s“ липсва: такъв файл или директория няма"
+msgid "fatal: '%s' is not a valid rev'"
+msgstr "фатална грешка: „%s“ не е версия"
+
+#, tcl-format
+msgid "'%s' is not a directory in rev '%s'"
+msgstr "„%s“ не е директория във версия „%s“"
+
+#, tcl-format
+msgid "'%s' is not a filename in rev '%s'"
+msgstr "„%s“ не е име на файл във версия „%s“"
 
 msgid "Current Branch:"
 msgstr "Текущ клон:"
@@ -461,6 +473,9 @@
 msgid "Reading %s..."
 msgstr "Чете се „%s“…"
 
+msgid "Error"
+msgstr "Грешка"
+
 msgid "Loading copy/move tracking annotations..."
 msgstr "Зареждане на анотациите за проследяване на копирането/преместването…"
 
diff --git a/git.c b/git.c
index 387eabe..e5f1811 100644
--- a/git.c
+++ b/git.c
@@ -306,7 +306,7 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
 		} else if (!strcmp(cmd, "--shallow-file")) {
 			(*argv)++;
 			(*argc)--;
-			set_alternate_shallow_file(the_repository, (*argv)[0], 1);
+			setenv(GIT_SHALLOW_FILE_ENVIRONMENT, (*argv)[0], 1);
 			if (envchanged)
 				*envchanged = 1;
 		} else if (!strcmp(cmd, "-C")) {
diff --git a/gitk-git/Makefile b/gitk-git/Makefile
index 41116d8..dd87f50 100644
--- a/gitk-git/Makefile
+++ b/gitk-git/Makefile
@@ -43,9 +43,12 @@
 ALL_POFILES = $(wildcard po/*.po)
 ALL_MSGFILES = $(subst .po,.msg,$(ALL_POFILES))
 
+ifneq ($(findstring s,$(firstword -$(MAKEFLAGS))),s)
 ifndef V
 	QUIET          = @
 	QUIET_GEN      = $(QUIET)echo '   ' GEN $@ &&
+	QUIET_MSGFMT   = $(QUIET)echo '   ' MSGFMT $@ &&
+endif
 endif
 
 all:: gitk-wish $(ALL_MSGFILES)
@@ -75,8 +78,7 @@
 	echo; \
 	echo "	git config filter.gettext-no-location.clean \"msgcat --no-location -\""
 $(ALL_MSGFILES): %.msg : %.po
-	@echo Generating catalog $@
-	$(MSGFMT) --statistics --tcl -l $(basename $(notdir $<)) -d $(dir $@) $<
+	$(QUIET_MSGFMT)$(MSGFMT) --tcl -l $(basename $(notdir $<)) -d $(dir $@) $<
 
 .PHONY: all install uninstall clean update-po
 .PHONY: FORCE
diff --git a/gitk-git/po/bg.po b/gitk-git/po/bg.po
index e7e2f87..3521fdb 100644
--- a/gitk-git/po/bg.po
+++ b/gitk-git/po/bg.po
@@ -1,15 +1,15 @@
 # Bulgarian translation of gitk po-file.
-# Copyright (C) 2014, 2015, 2019, 2020, 2024, 2025 Alexander Shopov <ash@kambanaria.org>.
+# Copyright (C) 2014, 2015, 2019, 2020, 2024, 2025, 2026 Alexander Shopov <ash@kambanaria.org>.
 # This file is distributed under the same license as the git package.
-# Alexander Shopov <ash@kambanaria.org>, 2014, 2015, 2019, 2020, 2024, 2025.
+# Alexander Shopov <ash@kambanaria.org>, 2014, 2015, 2019, 2020, 2024, 2025, 2026.
 #
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: Gitk master\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-07-22 18:34+0200\n"
-"PO-Revision-Date: 2025-07-28 13:38+0200\n"
+"POT-Creation-Date: 2026-04-18 17:41+0200\n"
+"PO-Revision-Date: 2026-06-14 22:45+0200\n"
 "Last-Translator: Alexander Shopov <ash@kambanaria.org>\n"
 "Language-Team: Bulgarian <dict@fsa-bg.org>\n"
 "Language: bg\n"
@@ -183,10 +183,10 @@
 msgstr "Нова версия"
 
 msgid "Lines of context"
-msgstr "Контекст в редове"
+msgstr "Редове контекст"
 
 msgid "Ignore space change"
-msgstr "Празните знаци без значение"
+msgstr "Празни знаци без значение"
 
 msgid "Line diff"
 msgstr "Поредови разлики"
@@ -954,6 +954,9 @@
 msgid "Hide remote refs"
 msgstr "Скриване на отдалечените указатели"
 
+msgid "Refs to hide (space-separated globs)"
+msgstr "Указатели за скриване (шаблони разделени с интервал):"
+
 msgid "Copy commit ID to clipboard"
 msgstr "Копиране на контролната сума към буфера за обмен"
 
@@ -999,15 +1002,24 @@
 msgid "Web browser"
 msgstr "Уеб браузър"
 
+msgid "Themes - change requires restart"
+msgstr "Теми — за прилагане трябва да рестартирате програмата"
+
+msgid "Theme to use after restart"
+msgstr "Тема за прилагане след рестартиране на програмата"
+
+msgid "Theme definition file"
+msgstr "Файл с тема"
+
+msgid "The theme definition file may affect all themes."
+msgstr "Файлът с тема може да повлияе на всички теми."
+
+msgid "Apply theme"
+msgstr "Прилагане на тема"
+
 msgid "Colors: press to choose"
 msgstr "Цветове: избира се с натискане"
 
-msgid "Interface"
-msgstr "Интерфейс"
-
-msgid "interface"
-msgstr "интерфейс"
-
 msgid "Background"
 msgstr "Фон"
 
@@ -1059,6 +1071,12 @@
 msgid "Select bg"
 msgstr "Избор на фон"
 
+msgid "Link"
+msgstr "Връзка"
+
+msgid "link"
+msgstr "връзка"
+
 msgid "Fonts: press to choose"
 msgstr "Шрифтове: избира се с натискане"
 
@@ -1083,6 +1101,9 @@
 msgid "Fonts"
 msgstr "Шрифтове"
 
+msgid "Gitk: select theme definition"
+msgstr "Gitk: избор на тема"
+
 #, tcl-format
 msgid "Gitk: choose color for %s"
 msgstr "Gitk: избор на цвят на „%s“"
diff --git a/gitk-git/po/es.po b/gitk-git/po/es.po
index 25260c6..881738e 100644
--- a/gitk-git/po/es.po
+++ b/gitk-git/po/es.po
@@ -8,30 +8,30 @@
 msgstr ""
 "Project-Id-Version: Gitk\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-05-17 14:32+1000\n"
-"PO-Revision-Date: 2008-03-25 11:20+0100\n"
-"Last-Translator: Santiago Gala <santiago.gala@gmail.com>\n"
+"POT-Creation-Date: 2026-04-18 18:45+0200\n"
+"PO-Revision-Date: 2026-04-19 20:18+0200\n"
+"Last-Translator: Luis B <basuradeluis@gmail.com>\n"
 "Language-Team: Spanish\n"
-"Language: \n"
+"Language: es\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.4.2\n"
 
 msgid "Couldn't get list of unmerged files:"
 msgstr "Imposible obtener la lista de archivos pendientes de fusión:"
 
 msgid "Color words"
-msgstr ""
+msgstr "Colorear palabras"
 
 msgid "Markup words"
-msgstr ""
+msgstr "Marcar cambios por palabras"
 
-#, fuzzy
 msgid "Error parsing revisions:"
-msgstr "Error al leer las diferencias de fusión:"
+msgstr "Error procesando revisiones:"
 
 msgid "Error executing --argscmd command:"
-msgstr ""
+msgstr "Error ejecutando comando --argscmd:"
 
 msgid "No files selected: --merge specified but no files are unmerged."
 msgstr ""
@@ -45,9 +45,8 @@
 "No hay archivos seleccionados: se seleccionó la opción --merge pero los "
 "archivos especificados no necesitan fusión."
 
-#, fuzzy
 msgid "Error executing git log:"
-msgstr "Error al crear la etiqueta:"
+msgstr "Error ejecutando git log:"
 
 msgid "Reading"
 msgstr "Leyendo"
@@ -74,19 +73,19 @@
 msgstr "Cancelar"
 
 msgid "&Update"
-msgstr "Actualizar"
+msgstr "Act&ualizar"
 
 msgid "&Reload"
-msgstr ""
+msgstr "&Recargar"
 
 msgid "Reread re&ferences"
-msgstr "Releer referencias"
+msgstr "Releer re&ferencias"
 
 msgid "&List references"
-msgstr "Lista de referencias"
+msgstr "Mostrar &lista de referencias"
 
 msgid "Start git &gui"
-msgstr ""
+msgstr "Ejecutar 'git &gui'"
 
 msgid "&Quit"
 msgstr "Salir"
@@ -124,11 +123,11 @@
 msgid "&Help"
 msgstr "Ayuda"
 
-msgid "SHA1 ID:"
-msgstr "SHA1 ID:"
+msgid "Commit ID:"
+msgstr "Commit ID:"
 
 msgid "Row"
-msgstr ""
+msgstr "Fila"
 
 msgid "Find"
 msgstr "Buscar"
@@ -146,7 +145,7 @@
 msgstr "que añade/elimina cadena:"
 
 msgid "changing lines matching:"
-msgstr ""
+msgstr "que cambia líneas que coinciden con:"
 
 msgid "Exact"
 msgstr "Exacto"
@@ -170,7 +169,7 @@
 msgstr "Autor"
 
 msgid "Committer"
-msgstr ""
+msgstr "Comiteador"
 
 msgid "Search"
 msgstr "Buscar"
@@ -188,10 +187,10 @@
 msgstr "Líneas de contexto"
 
 msgid "Ignore space change"
-msgstr "Ignora cambios de espaciado"
+msgstr "Ignorar cambios de espaciado"
 
 msgid "Line diff"
-msgstr ""
+msgstr "Diferencia de líneas"
 
 msgid "Patch"
 msgstr "Parche"
@@ -199,6 +198,9 @@
 msgid "Tree"
 msgstr "Árbol"
 
+msgid "Unknown windowing system, cannot bind mouse"
+msgstr "Sistema de ventanas desconocido, no se puede asociar el ratón"
+
 msgid "Diff this -> selected"
 msgstr "Diferencia de esta -> seleccionada"
 
@@ -206,11 +208,14 @@
 msgstr "Diferencia de seleccionada -> esta"
 
 msgid "Make patch"
-msgstr "Crear patch"
+msgstr "Crear parche"
 
 msgid "Create tag"
 msgstr "Crear etiqueta"
 
+msgid "Copy commit reference"
+msgstr "Copiar referencia de la revisión"
+
 msgid "Write commit to file"
 msgstr "Escribir revisiones a archivo"
 
@@ -223,39 +228,38 @@
 msgid "Reset HEAD branch to here"
 msgstr "Traer la rama HEAD aquí"
 
-#, fuzzy
 msgid "Mark this commit"
-msgstr "Añadir esta revisión a la rama actual (cherry-pick)"
+msgstr "Marcar esta revisión"
 
 msgid "Return to mark"
-msgstr ""
+msgstr "Volver a revisión marcada"
 
 msgid "Find descendant of this and mark"
-msgstr ""
+msgstr "Encontrar primer descendiente de esta y revisión marcada"
 
 msgid "Compare with marked commit"
-msgstr ""
+msgstr "Comparar con revisión marcada"
 
-#, fuzzy
 msgid "Diff this -> marked commit"
-msgstr "Diferencia de esta -> seleccionada"
+msgstr "Diferencia de esta -> revisión marcada"
 
-#, fuzzy
 msgid "Diff marked commit -> this"
-msgstr "Diferencia de seleccionada -> esta"
+msgstr "Diferencia de revisión marcada -> esta"
 
-#, fuzzy
 msgid "Revert this commit"
-msgstr "Añadir esta revisión a la rama actual (cherry-pick)"
+msgstr "Revertir este commit"
 
 msgid "Check out this branch"
 msgstr "Cambiar a esta rama"
 
+msgid "Rename this branch"
+msgstr "Renombrar esta rama"
+
 msgid "Remove this branch"
 msgstr "Eliminar esta rama"
 
 msgid "Copy branch name"
-msgstr ""
+msgstr "Copiar nombre de rama"
 
 msgid "Highlight this too"
 msgstr "Seleccionar también"
@@ -264,21 +268,23 @@
 msgstr "Seleccionar sólo"
 
 msgid "External diff"
-msgstr ""
+msgstr "Comparador de diferencias externo"
 
 msgid "Blame parent commit"
-msgstr ""
+msgstr "Ejecutar git blame en el commit padre"
 
 msgid "Copy path"
-msgstr ""
+msgstr "Copiar ruta"
 
 msgid "Show origin of this line"
-msgstr ""
+msgstr "Mostrar origen de esta línea"
 
 msgid "Run git gui blame on this line"
-msgstr ""
+msgstr "Ejecutar git gui blame en esta línea"
 
-#, fuzzy
+msgid "About gitk"
+msgstr "Acerca de gitk"
+
 msgid ""
 "\n"
 "Gitk - a commit viewer for git\n"
@@ -290,7 +296,7 @@
 "\n"
 "Gitk - un visualizador de revisiones para git\n"
 "\n"
-"Copyright \\u00a9 2005-2016 Paul Mackerras\n"
+"Copyright © 2005-2016 Paul Mackerras\n"
 "\n"
 "Uso y redistribución permitidos según los términos de la Licencia Pública "
 "General de GNU (GNU GPL)"
@@ -308,9 +314,9 @@
 msgid "<%s-Q>\t\tQuit"
 msgstr "<%s-Q>\t\tSalir"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "<%s-W>\t\tClose window"
-msgstr "<%s-F>\t\tBuscar"
+msgstr "<%s-W>\t\tCerrar ventana"
 
 msgid "<Home>\t\tMove to first commit"
 msgstr "<Home>\t\tIr a la primera revisión"
@@ -318,24 +324,21 @@
 msgid "<End>\t\tMove to last commit"
 msgstr "<End>\t\tIr a la última revisión"
 
-#, fuzzy
 msgid "<Up>, p, k\tMove up one commit"
-msgstr "<Up>, p, i\tSubir una revisión"
+msgstr "<Up>, p, k\tSubir una revisión"
 
-#, fuzzy
 msgid "<Down>, n, j\tMove down one commit"
-msgstr "<Down>, n, k\tBajar una revisión"
+msgstr "<Down>, n, j\tBajar una revisión"
 
-#, fuzzy
 msgid "<Left>, z, h\tGo back in history list"
-msgstr "<Left>, z, j\tRetroceder en la historia"
+msgstr "<Left>, z, h\tRetroceder en la historia"
 
 msgid "<Right>, x, l\tGo forward in history list"
 msgstr "<Right>, x, l\tAvanzar en la historia"
 
 #, tcl-format
 msgid "<%s-n>\tGo to n-th parent of current commit in history list"
-msgstr ""
+msgstr "<%s-n>\tIr al padre número N del commit actual"
 
 msgid "<PageUp>\tMove up one page in commit list"
 msgstr "<PageUp>\tSubir una página en la lista de revisiones"
@@ -399,12 +402,11 @@
 msgid "<Return>\tMove to next find hit"
 msgstr "<Return>\tBuscar el siguiente"
 
-#, fuzzy
 msgid "g\t\tGo to commit"
-msgstr "<End>\t\tIr a la última revisión"
+msgstr "g\t\tIr a la revisión"
 
 msgid "/\t\tFocus the search box"
-msgstr ""
+msgstr "/\t\tFoco en el cuadro de búsqueda"
 
 msgid "?\t\tMove to previous find hit"
 msgstr "?\t\tBuscar el anterior"
@@ -439,51 +441,54 @@
 msgid "<F5>\t\tUpdate"
 msgstr "<F5>\t\tActualizar"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error creating temporary directory %s:"
-msgstr "Error en la creación del parche:"
+msgstr "Error creando directorio temporal %s:"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error getting \"%s\" from %s:"
-msgstr "Error al leer las diferencias de fusión:"
+msgstr "Error obteniendo \"%s\" de %s:"
 
-#, fuzzy
 msgid "command failed:"
-msgstr "Línea de comandos"
+msgstr "el comando falló:"
 
-#, fuzzy
 msgid "No such commit"
-msgstr "No se han guardado cambios"
+msgstr "No existe el commit"
 
 msgid "git gui blame: command failed:"
-msgstr ""
+msgstr "git gui blame: error de ejecución:"
 
 #, tcl-format
 msgid "Couldn't read merge head: %s"
-msgstr ""
+msgstr "No se pudo leer merge head: %s"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error reading index: %s"
-msgstr "Error al crear la etiqueta:"
+msgstr "Error leyendo índice:%s"
 
 #, tcl-format
 msgid "Couldn't start git blame: %s"
-msgstr ""
+msgstr "No se pudo iniciar git blame: %s"
 
 msgid "Searching"
 msgstr "Buscando"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error running git blame: %s"
-msgstr "Error al crear la etiqueta:"
+msgstr "Error al ejecutar git blame: %s"
 
 #, tcl-format
 msgid "That line comes from commit %s,  which is not in this view"
-msgstr ""
+msgstr "Esa línea proviene del commit %s, que no está en esta vista"
 
-#, fuzzy
 msgid "External diff viewer failed:"
-msgstr "f\t\tDesplazar la vista de diferencias al archivo siguiente"
+msgstr "Visor de diferencias externo falló:"
+
+msgid "All files"
+msgstr "Todos los archivos"
+
+msgid "View"
+msgstr "Vista"
 
 msgid "Gitk view definition"
 msgstr "Definición de vistas de Gitk"
@@ -492,96 +497,93 @@
 msgstr "Recordar esta vista"
 
 msgid "References (space separated list):"
-msgstr ""
+msgstr "Referencias (lista separada por espacios):"
 
 msgid "Branches & tags:"
-msgstr ""
+msgstr "Ramas y etiquetas:"
 
-#, fuzzy
 msgid "All refs"
-msgstr "Todos los archivos"
+msgstr "Todas las referencias"
 
 msgid "All (local) branches"
-msgstr ""
+msgstr "Todas las ramas (locales)"
 
 msgid "All tags"
-msgstr ""
+msgstr "Todas las etiquetas"
 
 msgid "All remote-tracking branches"
-msgstr ""
+msgstr "Todas las ramas remotas"
 
 msgid "Commit Info (regular expressions):"
-msgstr ""
+msgstr "Información del commit (expresiones regulares)"
 
-#, fuzzy
 msgid "Author:"
-msgstr "Autor"
+msgstr "Autor:"
 
-#, fuzzy
 msgid "Committer:"
-msgstr "revisión"
+msgstr "Comiteador:"
 
 msgid "Commit Message:"
-msgstr ""
+msgstr "Mensaje del commit:"
 
 msgid "Matches all Commit Info criteria"
-msgstr ""
+msgstr "Cumple todos los criterios de 'Información del commit'"
 
 msgid "Matches no Commit Info criteria"
-msgstr ""
+msgstr "Cumple ningún criterio de 'Información del commit'"
 
 msgid "Changes to Files:"
-msgstr ""
+msgstr "Cambios en ficheros:"
 
 msgid "Fixed String"
-msgstr ""
+msgstr "Texto exacto"
 
 msgid "Regular Expression"
-msgstr ""
+msgstr "Expresión regular"
 
-#, fuzzy
 msgid "Search string:"
-msgstr "Buscando"
+msgstr "Buscar cadena:"
 
+# Notice git log since and until format
 msgid ""
 "Commit Dates (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, 2009 "
 "15:27:38\"):"
 msgstr ""
+"Fechas del commit (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, "
+"2009 15:27:38\"):"
 
 msgid "Since:"
-msgstr ""
+msgstr "Desde:"
 
 msgid "Until:"
-msgstr ""
+msgstr "Hasta:"
 
 msgid "Limit and/or skip a number of revisions (positive integer):"
-msgstr ""
+msgstr "Limitar y/o saltar las siguientes revisiones (número positivo)"
 
 msgid "Number to show:"
-msgstr ""
+msgstr "Limitar a n revisiones:"
 
 msgid "Number to skip:"
-msgstr ""
+msgstr "Saltar n revisiones:"
 
 msgid "Miscellaneous options:"
-msgstr ""
+msgstr "Opciones varias:"
 
 msgid "Strictly sort by date"
-msgstr ""
+msgstr "Ordenar estrictamente por fecha"
 
 msgid "Mark branch sides"
-msgstr ""
+msgstr "Marcar lados de ramas"
 
-#, fuzzy
 msgid "Limit to first parent"
-msgstr "Limitar las diferencias a las rutas seleccionadas"
+msgstr "Limitar al primer padre"
 
 msgid "Simple history"
-msgstr ""
+msgstr "Historia simple"
 
-#, fuzzy
 msgid "Additional arguments to git log:"
-msgstr "Revisiones a incluir (argumentos a git log):"
+msgstr "Argumentos extra a git log:"
 
 msgid "Enter files and directories to include, one per line:"
 msgstr "Introducir archivos y directorios a incluir, uno por línea:"
@@ -590,17 +592,16 @@
 msgstr "Comando que genera más revisiones a incluir:"
 
 msgid "Gitk: edit view"
-msgstr ""
+msgstr "Gitk: editar vista"
 
 msgid "-- criteria for selecting revisions"
-msgstr ""
+msgstr "-- criterios para filtrar revisiones"
 
-#, fuzzy
 msgid "View Name"
-msgstr "Vista"
+msgstr "Nombre de la vista"
 
 msgid "Apply (F5)"
-msgstr ""
+msgstr "Aplicar (F5)"
 
 msgid "Error in commit selection arguments:"
 msgstr "Error en los argumentos de selección de las revisiones:"
@@ -626,11 +627,14 @@
 msgid "Local uncommitted changes, not checked in to index"
 msgstr "Cambios locales sin añadir al índice"
 
+msgid "Error starting web browser:"
+msgstr "Error al arrancar navegador web:"
+
 msgid "and many more"
-msgstr ""
+msgstr "y muchos más"
 
 msgid "many"
-msgstr ""
+msgstr "muchos"
 
 msgid "Tags:"
 msgstr "Etiquetas:"
@@ -650,28 +654,28 @@
 msgid "Precedes"
 msgstr "Precede-a"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error getting diffs: %s"
-msgstr "Error al leer las diferencias de fusión:"
+msgstr "Error al leer las diferencias: %s"
 
 msgid "Goto:"
 msgstr "Ir a:"
 
 #, tcl-format
-msgid "Short SHA1 id %s is ambiguous"
-msgstr "La id SHA1 abreviada %s es ambigua"
-
-#, fuzzy, tcl-format
-msgid "Revision %s is not known"
-msgstr "La id SHA1 %s es desconocida"
+msgid "Short commit ID %s is ambiguous"
+msgstr "El id SHA1 abreviado %s es ambiguo"
 
 #, tcl-format
-msgid "SHA1 id %s is not known"
-msgstr "La id SHA1 %s es desconocida"
+msgid "Revision %s is not known"
+msgstr "Revisión %s es desconocida"
+
+#, tcl-format
+msgid "Commit ID %s is not known"
+msgstr "El id del commit %s es desconocido"
 
 #, tcl-format
 msgid "Revision %s is not in the current view"
-msgstr ""
+msgstr "La revisión %s no está en la vista actual"
 
 msgid "Date"
 msgstr "Fecha"
@@ -681,51 +685,55 @@
 
 #, tcl-format
 msgid "Reset %s branch to here"
-msgstr "Poner la rama %s en esta revisión"
+msgstr "Resetear la rama %s en esta revisión"
 
 msgid "Detached head: can't reset"
-msgstr ""
+msgstr "En estado 'detached head': no se puede resetear la rama"
 
 msgid "Skipping merge commit "
-msgstr ""
+msgstr "Omitiendo el commit de merge "
 
-#, fuzzy
 msgid "Error getting patch ID for "
-msgstr "Error en la creación del parche:"
+msgstr "Error obteniendo ID del parche para "
 
 msgid " - stopping\n"
-msgstr ""
+msgstr " - deteniendo\n"
 
-#, fuzzy
 msgid "Commit "
-msgstr "revisión"
+msgstr "Revisión "
 
 msgid ""
 " is the same patch as\n"
 "       "
 msgstr ""
+" es el mismo parche que\n"
+"       "
 
 msgid ""
 " differs from\n"
 "       "
 msgstr ""
+" difiere de\n"
+"       "
 
 msgid ""
 "Diff of commits:\n"
 "\n"
 msgstr ""
+"Diferencia de commits:\n"
+"\n"
 
 #, tcl-format
 msgid " has %s children - stopping\n"
-msgstr ""
+msgstr " tiene %s hijos - parando\n"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error writing commit to file: %s"
-msgstr "Error al escribir revisión:"
+msgstr "Error al escribir revisión al fichero: %s"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Error diffing commits: %s"
-msgstr "Error al escribir revisión:"
+msgstr "Error comparando revisiones: %s"
 
 msgid "Top"
 msgstr "Origen"
@@ -764,11 +772,10 @@
 msgstr "Nombre de etiqueta:"
 
 msgid "Tag message is optional"
-msgstr ""
+msgstr "El mensaje de la etiqueta es opcional"
 
-#, fuzzy
 msgid "Tag message:"
-msgstr "Nombre de etiqueta:"
+msgstr "Mensaje de la etiqueta:"
 
 msgid "Create"
 msgstr "Crear"
@@ -792,15 +799,28 @@
 msgid "Error writing commit:"
 msgstr "Error al escribir revisión:"
 
+msgid "Create branch"
+msgstr "Crear nueva rama"
+
+#, tcl-format
+msgid "Rename branch %s"
+msgstr "Renombrar rama '%s'"
+
+msgid "Rename"
+msgstr "Renombrar"
+
 msgid "Name:"
 msgstr "Nombre:"
 
 msgid "Please specify a name for the new branch"
 msgstr "Especifique un nombre para la nueva rama"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Branch '%s' already exists. Overwrite?"
-msgstr "La etiqueta \"%s\" ya existe"
+msgstr "La rama '%s' ya existe. ¿Sobreescribir?"
+
+msgid "Please specify a new name for the branch"
+msgstr "Especifique un nuevo nombre para la rama"
 
 #, tcl-format
 msgid "Commit %s is already included in branch %s -- really re-apply it?"
@@ -814,33 +834,42 @@
 "Cherry-pick failed because of local changes to file '%s'.\n"
 "Please commit, reset or stash your changes and try again."
 msgstr ""
+"El comando cherry-pick falló por los cambios locales al fichero '%s'.\n"
+"Por favor, comiteé, reseteé o use stash con sus cambios y pruebe de nuevo"
 
 msgid ""
 "Cherry-pick failed because of merge conflict.\n"
 "Do you wish to run git citool to resolve it?"
 msgstr ""
+"El comando cherry-pick falló debido a un conflicto de fusión.\n"
+"¿Desea ejecutar git citool para resolverlo?"
 
 msgid "No changes committed"
 msgstr "No se han guardado cambios"
 
-#, fuzzy, tcl-format
+#, tcl-format
 msgid "Commit %s is not included in branch %s -- really revert it?"
-msgstr "La revisión %s ya está incluida en la rama %s -- ¿Volver a aplicarla?"
+msgstr ""
+"La revisión %s no está incluida en la rama %s -- ¿seguro que quiere "
+"revertirla?"
 
-#, fuzzy
 msgid "Reverting"
-msgstr "Reponiendo"
+msgstr "Revirtiendo"
 
 #, tcl-format
 msgid ""
 "Revert failed because of local changes to the following files:%s Please "
 "commit, reset or stash  your changes and try again."
 msgstr ""
+"El comando revert falló por los cambios locales a estos ficheros:%s Por "
+"favor, comitéelos, reseteé o use stash y pruebe otra vez"
 
 msgid ""
 "Revert failed because of merge conflict.\n"
 " Do you wish to run git citool to resolve it?"
 msgstr ""
+"Revert falló por un conflicto de fusión.\n"
+"¿Quiere ejecutar git citool para resolverlo?"
 
 msgid "Confirm reset"
 msgstr "Confirmar git reset"
@@ -868,6 +897,10 @@
 msgid "Resetting"
 msgstr "Reponiendo"
 
+#, tcl-format
+msgid "A local branch named %s exists already"
+msgstr "Ya existe una rama local llamada '%s'"
+
 msgid "Checking out"
 msgstr "Creando copia de trabajo"
 
@@ -889,6 +922,9 @@
 msgid "Filter"
 msgstr "Filtro"
 
+msgid "Sort refs by type"
+msgstr "Ordenar referencias por tipo"
+
 msgid ""
 "Error reading commit topology information; branch and preceding/following "
 "tag information will be incomplete."
@@ -902,15 +938,6 @@
 msgid "Id"
 msgstr "Id"
 
-msgid "Gitk font chooser"
-msgstr "Selector de tipografías gitk"
-
-msgid "B"
-msgstr "B"
-
-msgid "I"
-msgstr "I"
-
 msgid "Commit list display options"
 msgstr "Opciones de visualización de la lista de revisiones"
 
@@ -924,12 +951,23 @@
 msgid "Show local changes"
 msgstr "Mostrar cambios locales"
 
-#, fuzzy
-msgid "Auto-select SHA1 (length)"
-msgstr "Seleccionar automáticamente SHA1 hash"
-
 msgid "Hide remote refs"
-msgstr ""
+msgstr "Esconder referencias remotas"
+
+msgid "Refs to hide (space-separated globs)"
+msgstr "Referencias a esconder (globs separados por espacios)"
+
+msgid "Copy commit ID to clipboard"
+msgstr "Copiar id de la revisión al portapapeles"
+
+msgid "Copy commit ID to X11 selection"
+msgstr "Copiar id de la revisión a selección X11"
+
+msgid "Length of commit ID to copy"
+msgstr "Longitud del ID de la revisión al copiar"
+
+msgid "Wheel scrolling multiplier"
+msgstr "Multiplicador desplazamiento rueda del ratón"
 
 msgid "Diff display options"
 msgstr "Opciones de visualización de diferencias"
@@ -937,92 +975,108 @@
 msgid "Tab spacing"
 msgstr "Espaciado de tabulador"
 
-#, fuzzy
+msgid "Wrap comment text"
+msgstr "Ajustar texto de comentarios"
+
+msgid "Wrap other text"
+msgstr "Ajustar otros textos"
+
 msgid "Display nearby tags/heads"
-msgstr "Mostrar etiquetas cercanas"
+msgstr "Mostrar etiquetas/referencias cercanas"
 
 msgid "Maximum # tags/heads to show"
-msgstr ""
+msgstr "Máximo número de etiquetas/"
 
 msgid "Limit diffs to listed paths"
 msgstr "Limitar las diferencias a las rutas seleccionadas"
 
 msgid "Support per-file encodings"
-msgstr ""
+msgstr "Soporte para codificaciones por archivo"
 
 msgid "External diff tool"
-msgstr ""
+msgstr "Herramienta de comparación externa"
 
 msgid "Choose..."
-msgstr ""
+msgstr "Escoger..."
 
-#, fuzzy
-msgid "General options"
-msgstr "Generar parche"
+msgid "Web browser"
+msgstr "Navegador web"
 
-msgid "Use themed widgets"
-msgstr ""
+msgid "Themes - change requires restart"
+msgstr "Temas - los cambios requieren reinicio"
 
-msgid "(change requires restart)"
-msgstr ""
+msgid "Theme to use after restart"
+msgstr "Tema a usar tras reiniciar"
 
-msgid "(currently unavailable)"
-msgstr ""
+msgid "Theme definition file"
+msgstr "Fichero de definición del tema"
+
+msgid "The theme definition file may affect all themes."
+msgstr "El fichero de definición del tema puede afectar a todos los temas."
+
+msgid "Apply theme"
+msgstr "Aplicar tema"
 
 msgid "Colors: press to choose"
 msgstr "Colores: pulse para seleccionar"
 
-msgid "Interface"
-msgstr ""
-
-#, fuzzy
-msgid "interface"
-msgstr "Tipografía para interfaz de usuario"
-
 msgid "Background"
 msgstr "Fondo"
 
-#, fuzzy
 msgid "background"
-msgstr "Fondo"
+msgstr "fondo"
 
 msgid "Foreground"
 msgstr "Primer plano"
 
-#, fuzzy
 msgid "foreground"
-msgstr "Primer plano"
+msgstr "primer plano"
 
 msgid "Diff: old lines"
-msgstr "Diff: líneas viejas"
+msgstr "Diff: líneas antiguas"
 
-#, fuzzy
 msgid "diff old lines"
-msgstr "Diff: líneas viejas"
+msgstr "diff líneas antiguas"
+
+msgid "Diff: old lines bg"
+msgstr "Diff: fondo de líneas antiguas"
+
+msgid "diff old lines bg"
+msgstr "diff fondo de líneas antiguas"
 
 msgid "Diff: new lines"
 msgstr "Diff: líneas nuevas"
 
-#, fuzzy
 msgid "diff new lines"
-msgstr "Diff: líneas nuevas"
+msgstr "diff líneas nuevas"
+
+msgid "Diff: new lines bg"
+msgstr "Diff: fondo de líneas nuevas"
+
+msgid "diff new lines bg"
+msgstr "diff fondo de líneas nuevas"
 
 msgid "Diff: hunk header"
 msgstr "Diff: cabecera de fragmento"
 
-#, fuzzy
 msgid "diff hunk header"
-msgstr "Diff: cabecera de fragmento"
+msgstr "diff cabecera de fragmento"
 
 msgid "Marked line bg"
-msgstr ""
+msgstr "Fondo de línea marcada"
 
 msgid "marked line background"
-msgstr ""
+msgstr "fondo de linea marcada"
 
 msgid "Select bg"
 msgstr "Color de fondo de la selección"
 
+msgid "Link"
+msgstr "Enlace"
+
+msgid "link"
+msgstr "enlace"
+
 msgid "Fonts: press to choose"
 msgstr "Tipografías: pulse para elegir"
 
@@ -1038,27 +1092,22 @@
 msgid "Gitk preferences"
 msgstr "Preferencias de gitk"
 
-#, fuzzy
 msgid "General"
-msgstr "Generar"
+msgstr "General"
 
 msgid "Colors"
-msgstr ""
+msgstr "Colores"
 
 msgid "Fonts"
-msgstr ""
+msgstr "Fuentes"
+
+msgid "Gitk: select theme definition"
+msgstr "Gitk: escoger definición de tema"
 
 #, tcl-format
 msgid "Gitk: choose color for %s"
 msgstr "Gitk: elegir color para %s"
 
-msgid ""
-"Sorry, gitk cannot run with this version of Tcl/Tk.\n"
-" Gitk requires at least Tcl/Tk 8.4."
-msgstr ""
-"Esta versión de Tcl/Tk es demasiado antigua.\n"
-" Gitk requiere Tcl/Tk versión 8.4 o superior."
-
 msgid "Cannot find a git repository here."
 msgstr "No hay un repositorio git aquí."
 
@@ -1070,6 +1119,37 @@
 msgid "Bad arguments to gitk:"
 msgstr "Argumentos incorrectos a Gitk:"
 
+#~ msgid "SHA1 ID:"
+#~ msgstr "SHA1 ID:"
+
+#~ msgid "Gitk font chooser"
+#~ msgstr "Selector de tipografías gitk"
+
+#~ msgid "B"
+#~ msgstr "B"
+
+#~ msgid "I"
+#~ msgstr "I"
+
+#, fuzzy
+#~ msgid "Auto-select SHA1 (length)"
+#~ msgstr "Seleccionar automáticamente SHA1 hash"
+
+#, fuzzy
+#~ msgid "General options"
+#~ msgstr "Generar parche"
+
+#, fuzzy
+#~ msgid "interface"
+#~ msgstr "Tipografía para interfaz de usuario"
+
+#~ msgid ""
+#~ "Sorry, gitk cannot run with this version of Tcl/Tk.\n"
+#~ " Gitk requires at least Tcl/Tk 8.4."
+#~ msgstr ""
+#~ "Esta versión de Tcl/Tk es demasiado antigua.\n"
+#~ " Gitk requiere Tcl/Tk versión 8.4 o superior."
+
 #~ msgid "SHA1 ID: "
 #~ msgstr "SHA1 ID: "
 
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index fde8045..8c2d9b8 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -2339,12 +2339,14 @@
 		$line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
 		                 esc_path($to->{'file'}));
 	}
-	# match single <mode>
-	if ($line =~ m/\s(\d{6})$/) {
-		$line .= '<span class="info"> (' .
-		         file_type_long($1) .
-		         ')</span>';
+
+	# Temporarily remove a trailing <mode> so an index line ends with its
+	# object IDs and can be shortened below.
+	my $mode;
+	if ($line =~ s/\s(\d{6})$//) {
+		$mode = $1;
 	}
+
 	# match <hash>
 	if ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", ",") |
 	    $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", ",")) {
@@ -2388,6 +2390,12 @@
 		my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
 		$line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
 	}
+	if (defined $mode) {
+		$line .= " $mode" .
+		         '<span class="info"> (' .
+		         file_type_long($mode) .
+		         ')</span>';
+	}
 
 	return $line . "<br/>\n";
 }
diff --git a/gpg-interface.c b/gpg-interface.c
index dafd537..95abf1e 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -990,21 +990,18 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,
 	return ret;
 }
 
-/*
- * Strip CR from the line endings, in case we are on Windows.
- * NEEDSWORK: make it trim only CRs before LFs and rename
- */
-static void remove_cr_after(struct strbuf *buffer, size_t offset)
+/* Strip CR before LF from the line endings, in case we are on Windows. */
+static void strip_cr_before_lf(struct strbuf *buffer, size_t offset)
 {
 	size_t i, j;
 
 	for (i = j = offset; i < buffer->len; i++) {
-		if (buffer->buf[i] != '\r') {
-			if (i != j)
-				buffer->buf[j] = buffer->buf[i];
-			j++;
-		}
+		if (buffer->buf[i] == '\r' &&
+		    i + 1 < buffer->len && buffer->buf[i + 1] == '\n')
+			continue;
+		buffer->buf[j++] = buffer->buf[i];
 	}
+
 	strbuf_setlen(buffer, j);
 }
 
@@ -1049,8 +1046,8 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 	}
 	strbuf_release(&gpg_status);
 
-	/* Strip CR from the line endings, in case we are on Windows. */
-	remove_cr_after(signature, bottom);
+	/* Strip CR before LF from the line endings, in case we are on Windows. */
+	strip_cr_before_lf(signature, bottom);
 
 	return 0;
 }
@@ -1136,8 +1133,8 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
 			ssh_signature_filename.buf);
 		goto out;
 	}
-	/* Strip CR from the line endings, in case we are on Windows. */
-	remove_cr_after(signature, bottom);
+	/* Strip CR before LF from the line endings, in case we are on Windows. */
+	strip_cr_before_lf(signature, bottom);
 
 out:
 	if (key_file)
diff --git a/graph.c b/graph.c
index 8422826..28bef1b 100644
--- a/graph.c
+++ b/graph.c
@@ -60,12 +60,23 @@ struct column {
 	 * index into column_colors.
 	 */
 	unsigned short color;
+	/*
+	 * Marks if a commit is a non-first parent of a merge. These columns are
+	 * already visually connected to the merge commit and do not need
+	 * indentation.
+	 *
+	 * The first parent is the one that inherits the column and it can need
+	 * indentation if turns out to be a visual root and there's still
+	 * commits to render.
+	 */
+	unsigned int is_merge_parent:1;
 };
 
 enum graph_state {
 	GRAPH_PADDING,
 	GRAPH_SKIP,
 	GRAPH_PRE_COMMIT,
+	GRAPH_PRE_ROOT,
 	GRAPH_COMMIT,
 	GRAPH_POST_MERGE,
 	GRAPH_COLLAPSING
@@ -315,6 +326,59 @@ struct git_graph {
 	 * diff_output_prefix_callback().
 	 */
 	struct strbuf prefix_buf;
+
+	/*
+	 * Lookahead buffer: up to 2 pre-fetched commits that will be shown.
+	 * Populated by get_revision() so graph_peek_next_visible() can use
+	 * actual walk results instead of peeking at rev_info internals.
+	 */
+	struct commit *lookahead[2];
+	int lookahead_nr;
+
+	/*
+	 * If a commit is a visual root, we need to indent it to prevent
+	 * unrelated commits from being vertically adjacent to it.
+	 */
+	unsigned int is_visual_root:1;
+
+	/*
+	 * Indentation increases for each visual root adjacent to another visual
+	 * root, making visual root commits indentation cascade.
+	 */
+	unsigned int visual_root_depth;
+
+	/*
+	 * When a visual root is adjacent to other visual roots, the first one
+	 * can avoid indentation and the rest cascades, increasing the indentation
+	 * for each one.
+	 */
+	unsigned int visual_root_cascade:1;
+
+	/*
+	 * Set when the current commit was already present in graph->columns
+	 * before being processed.
+	 */
+	unsigned int commit_in_columns:1;
+};
+
+struct graph_lookahead_flags {
+
+	/*
+	 * Set when there will be a commit after the current one that will be
+	 * rendered.
+	 */
+	unsigned int is_next_visible:1;
+
+	/*
+	 * Set when the next visible commit is candidate to be a visual root.
+	 */
+	unsigned int is_next_visual_root:1;
+
+	/*
+	 * Set when the next visible commit will be rendered under the current
+	 * commit.
+	 */
+	unsigned int next_has_column:1;
 };
 
 static inline int graph_needs_truncation(struct git_graph *graph, int lane)
@@ -353,13 +417,13 @@ void graph_setup_line_prefix(struct diff_options *diffopt)
 		diffopt->output_prefix = diff_output_prefix_callback;
 }
 
-struct git_graph *graph_init(struct rev_info *opt)
+static void graph_read_config(struct rev_info *revs)
 {
-	struct git_graph *graph = xmalloc(sizeof(struct git_graph));
+	int val;
 
 	if (!column_colors) {
 		char *string;
-		if (repo_config_get_string(opt->repo, "log.graphcolors", &string)) {
+		if (repo_config_get_string(revs->repo, "log.graphcolors", &string)) {
 			/* not configured -- use default */
 			graph_set_column_colors(column_colors_ansi,
 						column_colors_ansi_max);
@@ -374,6 +438,16 @@ struct git_graph *graph_init(struct rev_info *opt)
 		}
 	}
 
+	if (!repo_config_get_bool(revs->repo, "log.graphIndent", &val))
+		revs->no_graph_indent = !val;
+}
+
+struct git_graph *graph_init(struct rev_info *opt)
+{
+	struct git_graph *graph = xmalloc(sizeof(struct git_graph));
+
+	graph_read_config(opt);
+
 	graph->commit = NULL;
 	graph->revs = opt;
 	graph->num_parents = 0;
@@ -388,6 +462,11 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
+	graph->lookahead[0] = NULL;
+	graph->lookahead[1] = NULL;
+	graph->lookahead_nr = 0;
+	graph->visual_root_depth = 0;
+	graph->visual_root_cascade = 0;
 	/*
 	 * Start the column color at the maximum value, since we'll
 	 * always increment it for the first commit we output.
@@ -457,6 +536,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)
 static int graph_is_interesting(struct git_graph *graph, struct commit *commit)
 {
 	/*
+	 * Commits in the lookahead buffer have been pre-fetched by
+	 * get_revision() and will be shown in the future. They already have
+	 * the SHOWN flag set when they were pre-fetched but the graph still
+	 * needs to treat them as interesting parents.
+	 */
+	for (int i = 0; i < graph->lookahead_nr; i++)
+		if (graph->lookahead[i] == commit)
+			return 1;
+	/*
 	 * If revs->boundary is set, commits whose children have
 	 * been shown are always interesting, even if they have the
 	 * UNINTERESTING or TREESAME flags set.
@@ -561,6 +649,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 					  struct commit *commit,
 					  int idx)
 {
+	/*
+	 * Get the initial merge_layout before it's modified to know if this
+	 * is a merge.
+	 */
+	int initial_merge_layout = graph->merge_layout;
 	int i = graph_find_new_column_by_commit(graph, commit);
 	int mapping_idx;
 
@@ -572,6 +665,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		i = graph->num_new_columns++;
 		graph->new_columns[i].commit = commit;
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
+		graph->new_columns[i].is_merge_parent = 0;
 	}
 
 	if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
@@ -610,6 +704,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 	}
 
 	graph->mapping[mapping_idx] = i;
+
+	/*
+	 * Mark non-first parents of a merge.
+	 */
+	if (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)
+		graph->new_columns[i].is_merge_parent = 1;
 }
 
 static void graph_update_columns(struct git_graph *graph)
@@ -701,10 +801,20 @@ static void graph_update_columns(struct git_graph *graph)
 			if (graph->num_parents == 0)
 				graph->width += 2;
 		} else {
+			int j;
 			graph_insert_into_new_columns(graph, col_commit, -1);
+			/*
+			 * This column is not the current commit, but we need to
+			 * propagate the flag until the commit is processed.
+			 */
+			j = graph_find_new_column_by_commit(graph, col_commit);
+			if (j >= 0 && graph->columns[i].is_merge_parent)
+				graph->new_columns[j].is_merge_parent = 1;
 		}
 	}
 
+	graph->commit_in_columns = is_commit_in_columns;
+
 	/*
 	 * If graph_max_lanes is set, cap the width
 	 */
@@ -763,9 +873,145 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)
 	       graph->expansion_row < graph_num_expansion_rows(graph);
 }
 
+struct commit *graph_pop_lookahead(struct git_graph *graph)
+{
+	struct commit *c;
+
+	if (!graph->lookahead_nr)
+		return NULL;
+
+	c = graph->lookahead[0];
+	if (!c)
+		BUG("lookahead buffer has %d entries but the first one is NULL",
+		    graph->lookahead_nr);
+
+	graph->lookahead[0] = graph->lookahead[1];
+	graph->lookahead[1] = NULL;
+	graph->lookahead_nr--;
+	return c;
+}
+
+int graph_get_lookahead_room(struct git_graph *graph)
+{
+	return (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr;
+}
+
+void graph_push_lookahead(struct git_graph *graph, struct commit *c)
+{
+	if (!graph_get_lookahead_room(graph))
+		BUG("pushing into lookahead buffer when it is already full");
+
+	graph->lookahead[graph->lookahead_nr++] = c;
+}
+
+/*
+ * A commit can be a visual root when:
+ *
+ * - It has no parents.
+ *
+ * - It has parents but they are all filtered out and
+ *   commit->parents arrives NULL.
+ *
+ * - Its parents are uninteresting.
+ *
+ * - It is not a boundary commit. Boundary commits also have no visible
+ *   parents, but they are not selected as visual roots because they cannot
+ *   cause the ambiguity of being vertically adjacent because:
+ *
+ *   1. A boundary only appears because an included commit is its child.
+ *      Children are always above, and the renderer draws an edge down to
+ *      the boundary from that child. Rather than starting a column like a
+ *      visual root would do, it inherits its child column.
+ *
+ *   2. Included commits cannot appear below a boundary. Boundaries are
+ *      ancestors of the exclusion point; if an included commit were an
+ *      ancestor of the boundary it would be excluded and not rendered.
+ *      Boundaries therefore always sink to the bottom.
+ */
+static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)
+{
+	struct commit_list *p;
+
+	if (c->object.flags & BOUNDARY)
+		return 0;
+	for (p = c->parents; p; p = p->next)
+		if (graph_is_interesting(graph, p->item))
+			return 0;
+	return 1;
+}
+
+static int graph_is_visual_root(struct git_graph *graph,
+				struct graph_lookahead_flags *flags)
+{
+	/*
+	 * This must be only called for the current commit as graph contains
+	 * the state for the current commit only.
+	 *
+	 * To check if a commit is a visual root, call graph_is_visual_root_candidate()
+	 * but we won't know if it is really a visual root until we get to the
+	 * next commit state.
+	 *
+	 * The current commit is an actual visual root if it is a candidate and
+	 * the commit is not a non-first parent of a merge.
+	 *
+	 *   *
+	 *   |\
+	 *   | *    <- it is a visual root candidate but it shouldn't be indented
+	 *   *         because it is already connected by an edge.
+	 *   ^         if commit_in_columns && is_merge_parent means the commit
+	 *   |         was put by a merge and is connected.
+	 *   |
+	 *   `-------- if !is_next_visible means we're on the last commit, avoid
+	 *             indentation unless the one before is a visual root, then
+	 *             we need to differentiate from the one above.
+	 *
+	 * If next_has_columns means that the next commit has
+	 * already a column, so it will not be rendered below, the
+	 * current commit has to act as the last commit and omit
+	 * indentation.
+	 */
+	return graph_is_visual_root_candidate(graph->commit, graph) &&
+	       !(graph->commit_in_columns &&
+		 graph->columns[graph->commit_index].is_merge_parent) &&
+	       flags->is_next_visible &&
+	       (!flags->next_has_column || graph->visual_root_depth > 0);
+}
+
+/*
+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.
+ */
+static void graph_peek_next_visible(struct git_graph *graph,
+				    struct graph_lookahead_flags *flags)
+{
+	flags->is_next_visible = 0;
+	flags->is_next_visual_root = 0;
+	flags->next_has_column = 0;
+
+	if (!graph->lookahead_nr)
+		return;
+
+	flags->is_next_visible = 1;
+	flags->next_has_column =
+		graph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;
+
+	if (!graph_is_visual_root_candidate(graph->lookahead[0], graph))
+		return;
+
+	if (graph->lookahead_nr >= 2)
+		flags->is_next_visual_root = 1;
+}
+
+static int graph_needs_pre_root_line(struct git_graph *graph)
+{
+	return graph->commit_in_columns && graph->is_visual_root &&
+	       graph->num_columns > 0 && !graph->visual_root_cascade &&
+	       !graph->revs->no_graph_indent;
+}
+
 void graph_update(struct git_graph *graph, struct commit *commit)
 {
 	struct commit_list *parent;
+	struct graph_lookahead_flags flags;
 
 	/*
 	 * Set the new commit
@@ -796,6 +1042,40 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
+	graph_peek_next_visible(graph, &flags);
+
+	graph->is_visual_root = graph_is_visual_root(graph, &flags);
+
+	if (graph->is_visual_root) {
+		/*
+		 * If next is a visual root we can omit the indent for the first
+		 * visual root and start cascading.
+		 */
+		if (!graph->visual_root_depth && flags.is_next_visual_root)
+			graph->visual_root_cascade = 1;
+
+		/*
+		 * We wrap the cascading at a max of four columns at most, after
+		 * that we wrap it back to the initial column.
+		 *
+		 * This could cause ambiguity in case of the next commit not
+		 * being a visual root and be at the initial column after the
+		 * first wrap.
+		 *
+		 * In case of being a non-visual-root the next, stop the
+		 * cascading to get the commit indented.
+		 */
+		if (!flags.is_next_visual_root &&
+		    graph->visual_root_depth &&
+		    !(graph->visual_root_depth % 4))
+			graph->visual_root_cascade = 0;
+
+		graph->visual_root_depth++;
+	} else {
+		graph->visual_root_depth = 0;
+		graph->visual_root_cascade = 0;
+	}
+
 	graph->expansion_row = 0;
 
 	/*
@@ -813,11 +1093,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 * room for it.  We need to do this only if there is a branch row
 	 * (or more) to the right of this commit.
 	 *
+	 * If it is a visual root, we need to print an extra row to
+	 * connect the indentation.
+	 *
 	 * If there are less than 3 parents, we can immediately print the
 	 * commit line.
 	 */
 	if (graph->state != GRAPH_PADDING)
 		graph->state = GRAPH_SKIP;
+	else if (graph_needs_pre_root_line(graph))
+		graph->state = GRAPH_PRE_ROOT;
 	else if (graph_needs_pre_commit_line(graph))
 		graph->state = GRAPH_PRE_COMMIT;
 	else
@@ -1065,6 +1350,20 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 
 		if (col_commit == graph->commit) {
 			seen_this = 1;
+			if (graph->is_visual_root && !graph->revs->no_graph_indent) {
+				int depth = graph->visual_root_depth;
+				/*
+				 * Each visual column is 2 characters wide.
+				 * Omit the indentation for the first visual
+				 * root in cascade mode.
+				 *
+				 * Have a max of 4 columns when cascading, after
+				 * that wrap it and repeat.
+				 */
+				int padding = ((depth - graph->visual_root_cascade) % 4) * 2;
+				graph_line_addchars(line, ' ', padding);
+				graph->width += padding;
+			}
 			graph_output_commit_char(graph, line);
 
 			if (graph_needs_truncation(graph, i)) {
@@ -1436,6 +1735,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 		graph_update_state(graph, GRAPH_PADDING);
 }
 
+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)
+{
+	/*
+	 * This function adds a row before a visual root, to connect the
+	 * branch to the indented commit. It must only be called on a
+	 * visual root.
+	 */
+	if (!graph->is_visual_root)
+		BUG("commit must be a visual root to call pre_root_line");
+
+	for (int i = 0; i < graph->num_columns; i++) {
+		struct column *col = &graph->columns[i];
+		if (col->commit == graph->commit) {
+			graph_line_addch(line, ' ');
+			graph_line_write_column(line, col, '\\');
+		} else {
+			graph_line_write_column(line, col, '|');
+		}
+		graph_line_addch(line, ' ');
+	}
+
+	graph_update_state(graph, GRAPH_COMMIT);
+}
+
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int shown_commit_line = 0;
@@ -1461,6 +1784,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 	case GRAPH_PRE_COMMIT:
 		graph_output_pre_commit_line(graph, &line);
 		break;
+	case GRAPH_PRE_ROOT:
+		graph_output_pre_root_line(graph, &line);
+		break;
 	case GRAPH_COMMIT:
 		graph_output_commit_line(graph, &line);
 		shown_commit_line = 1;
diff --git a/graph.h b/graph.h
index 3fd1dcb..1193711 100644
--- a/graph.h
+++ b/graph.h
@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,
 			   FILE *file,
 			   struct strbuf const *sb);
 
+/*
+ * Pop the first commit from the graph's lookahead buffer.
+ * Returns NULL if the buffer is empty.
+ */
+struct commit *graph_pop_lookahead(struct git_graph *graph);
+
+/*
+ * Returns how many more commits can be added to the lookahead buffer.
+ */
+int graph_get_lookahead_room(struct git_graph *graph);
+
+/*
+ * Push a commit into the lookahead buffer. Must only be called when
+ * graph_get_lookahead_room() returns > 0.
+ */
+void graph_push_lookahead(struct git_graph *graph, struct commit *c);
+
 #endif /* GRAPH_H */
diff --git a/hash.c b/hash.c
index e925b97..82f7e24 100644
--- a/hash.c
+++ b/hash.c
@@ -72,6 +72,11 @@ static void git_hash_sha1_final_oid(struct object_id *oid, struct git_hash_ctx *
 	oid->algo = GIT_HASH_SHA1;
 }
 
+static void git_hash_sha1_discard(struct git_hash_ctx *ctx)
+{
+	git_SHA1_Discard(&ctx->state.sha1);
+}
+
 static void git_hash_sha1_init_unsafe(struct git_hash_ctx *ctx)
 {
 	ctx->algop = unsafe_hash_algo(&hash_algos[GIT_HASH_SHA1]);
@@ -102,6 +107,11 @@ static void git_hash_sha1_final_oid_unsafe(struct object_id *oid, struct git_has
 	oid->algo = GIT_HASH_SHA1;
 }
 
+static void git_hash_sha1_discard_unsafe(struct git_hash_ctx *ctx)
+{
+	git_SHA1_Discard_unsafe(&ctx->state.sha1_unsafe);
+}
+
 static void git_hash_sha256_init(struct git_hash_ctx *ctx)
 {
 	ctx->algop = unsafe_hash_algo(&hash_algos[GIT_HASH_SHA256]);
@@ -135,6 +145,11 @@ static void git_hash_sha256_final_oid(struct object_id *oid, struct git_hash_ctx
 	oid->algo = GIT_HASH_SHA256;
 }
 
+static void git_hash_sha256_discard(struct git_hash_ctx *ctx)
+{
+	git_SHA256_Discard(&ctx->state.sha256);
+}
+
 static void git_hash_unknown_init(struct git_hash_ctx *ctx UNUSED)
 {
 	BUG("trying to init unknown hash");
@@ -165,6 +180,11 @@ static void git_hash_unknown_final_oid(struct object_id *oid UNUSED,
 	BUG("trying to finalize unknown hash");
 }
 
+static void git_hash_unknown_discard(struct git_hash_ctx *ctx UNUSED)
+{
+	BUG("trying to discard unknown hash");
+}
+
 static const struct git_hash_algo sha1_unsafe_algo = {
 	.name = "sha1",
 	.format_id = GIT_SHA1_FORMAT_ID,
@@ -176,6 +196,7 @@ static const struct git_hash_algo sha1_unsafe_algo = {
 	.update_fn = git_hash_sha1_update_unsafe,
 	.final_fn = git_hash_sha1_final_unsafe,
 	.final_oid_fn = git_hash_sha1_final_oid_unsafe,
+	.discard_fn = git_hash_sha1_discard_unsafe,
 	.empty_tree = &empty_tree_oid,
 	.empty_blob = &empty_blob_oid,
 	.null_oid = &null_oid_sha1,
@@ -193,6 +214,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = {
 		.update_fn = git_hash_unknown_update,
 		.final_fn = git_hash_unknown_final,
 		.final_oid_fn = git_hash_unknown_final_oid,
+		.discard_fn = git_hash_unknown_discard,
 		.empty_tree = NULL,
 		.empty_blob = NULL,
 		.null_oid = NULL,
@@ -208,6 +230,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = {
 		.update_fn = git_hash_sha1_update,
 		.final_fn = git_hash_sha1_final,
 		.final_oid_fn = git_hash_sha1_final_oid,
+		.discard_fn = git_hash_sha1_discard,
 		.unsafe = &sha1_unsafe_algo,
 		.empty_tree = &empty_tree_oid,
 		.empty_blob = &empty_blob_oid,
@@ -224,6 +247,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = {
 		.update_fn = git_hash_sha256_update,
 		.final_fn = git_hash_sha256_final,
 		.final_oid_fn = git_hash_sha256_final_oid,
+		.discard_fn = git_hash_sha256_discard,
 		.empty_tree = &empty_tree_oid_sha256,
 		.empty_blob = &empty_blob_oid_sha256,
 		.null_oid = &null_oid_sha256,
@@ -261,26 +285,47 @@ void git_hash_free(struct git_hash_ctx *ctx)
 void git_hash_init(struct git_hash_ctx *ctx, const struct git_hash_algo *algop)
 {
 	algop->init_fn(ctx);
+	ctx->active = true;
 }
 
 void git_hash_clone(struct git_hash_ctx *dst, const struct git_hash_ctx *src)
 {
+	if (!src->active)
+		BUG("attempt to copy from an inactive hash context");
+	if (!dst->active)
+		BUG("attempt to copy to an inactive hash context");
 	src->algop->clone_fn(dst, src);
 }
 
 void git_hash_update(struct git_hash_ctx *ctx, const void *in, size_t len)
 {
+	if (!ctx->active)
+		BUG("attempt to update an inactive hash context");
 	ctx->algop->update_fn(ctx, in, len);
 }
 
 void git_hash_final(unsigned char *hash, struct git_hash_ctx *ctx)
 {
+	if (!ctx->active)
+		BUG("attempt to finalize an inactive hash context");
 	ctx->algop->final_fn(hash, ctx);
+	ctx->active = false;
 }
 
 void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx)
 {
+	if (!ctx->active)
+		BUG("attempt to finalize an inactive hash context");
 	ctx->algop->final_oid_fn(oid, ctx);
+	ctx->active = false;
+}
+
+void git_hash_discard(struct git_hash_ctx *ctx)
+{
+	if (!ctx->active)
+		return;
+	ctx->algop->discard_fn(ctx);
+	ctx->active = false;
 }
 
 uint32_t hash_algo_by_name(const char *name)
diff --git a/hash.h b/hash.h
index c082a53..cf94ad5 100644
--- a/hash.h
+++ b/hash.h
@@ -37,6 +37,7 @@
 #    define platform_SHA1_Clone_unsafe openssl_SHA1_Clone
 #    define platform_SHA1_Update_unsafe openssl_SHA1_Update
 #    define platform_SHA1_Final_unsafe openssl_SHA1_Final
+#    define platform_SHA1_Discard_unsafe openssl_SHA1_Discard
 #  else
 #    define platform_SHA_CTX_unsafe SHA_CTX
 #    define platform_SHA1_Init_unsafe SHA1_Init
@@ -92,6 +93,7 @@
 #  define platform_SHA1_Final_unsafe   platform_SHA1_Final
 #  ifdef platform_SHA1_Clone
 #    define platform_SHA1_Clone_unsafe platform_SHA1_Clone
+#    define platform_SHA1_Discard_unsafe platform_SHA1_Discard
 #  endif
 #  ifdef SHA1_NEEDS_CLONE_HELPER
 #    define SHA1_NEEDS_CLONE_HELPER_UNSAFE
@@ -110,9 +112,11 @@
 
 #ifdef platform_SHA1_Clone
 #define git_SHA1_Clone	platform_SHA1_Clone
+#define git_SHA1_Discard platform_SHA1_Discard
 #endif
 #ifdef platform_SHA1_Clone_unsafe
 #  define git_SHA1_Clone_unsafe platform_SHA1_Clone_unsafe
+#  define git_SHA1_Discard_unsafe platform_SHA1_Discard_unsafe
 #endif
 
 #ifndef platform_SHA256_CTX
@@ -129,6 +133,7 @@
 
 #ifdef platform_SHA256_Clone
 #define git_SHA256_Clone	platform_SHA256_Clone
+#define git_SHA256_Discard	platform_SHA256_Discard
 #endif
 
 #ifdef SHA1_MAX_BLOCK_SIZE
@@ -142,6 +147,10 @@ static inline void git_SHA1_Clone(git_SHA_CTX *dst, const git_SHA_CTX *src)
 {
 	memcpy(dst, src, sizeof(*dst));
 }
+static inline void git_SHA1_Discard(git_SHA_CTX *ctx UNUSED)
+{
+	/* noop */
+}
 #endif
 #ifndef SHA1_NEEDS_CLONE_HELPER_UNSAFE
 static inline void git_SHA1_Clone_unsafe(git_SHA_CTX_unsafe *dst,
@@ -149,6 +158,10 @@ static inline void git_SHA1_Clone_unsafe(git_SHA_CTX_unsafe *dst,
 {
 	memcpy(dst, src, sizeof(*dst));
 }
+static inline void git_SHA1_Discard_unsafe(git_SHA_CTX_unsafe *ctx UNUSED)
+{
+	/* noop */
+}
 #endif
 
 #ifndef SHA256_NEEDS_CLONE_HELPER
@@ -156,6 +169,10 @@ static inline void git_SHA256_Clone(git_SHA256_CTX *dst, const git_SHA256_CTX *s
 {
 	memcpy(dst, src, sizeof(*dst));
 }
+static inline void git_SHA256_Discard(git_SHA256_CTX *ctx UNUSED)
+{
+	/* noop */
+}
 #endif
 
 /*
@@ -264,6 +281,7 @@ struct git_hash_ctx {
 		git_SHA_CTX_unsafe sha1_unsafe;
 		git_SHA256_CTX sha256;
 	} state;
+	bool active;
 };
 
 typedef void (*git_hash_init_fn)(struct git_hash_ctx *ctx);
@@ -271,6 +289,7 @@ typedef void (*git_hash_clone_fn)(struct git_hash_ctx *dst, const struct git_has
 typedef void (*git_hash_update_fn)(struct git_hash_ctx *ctx, const void *in, size_t len);
 typedef void (*git_hash_final_fn)(unsigned char *hash, struct git_hash_ctx *ctx);
 typedef void (*git_hash_final_oid_fn)(struct object_id *oid, struct git_hash_ctx *ctx);
+typedef void (*git_hash_discard_fn)(struct git_hash_ctx *ctx);
 
 struct git_hash_algo {
 	/*
@@ -291,20 +310,16 @@ struct git_hash_algo {
 	/* The block size of the hash. */
 	size_t blksz;
 
-	/* The hash initialization function. */
+	/*
+	 * Low-level implementation hooks. Callers should use the git_hash_*
+	 * wrappers below rather than invoking these directly.
+	 */
 	git_hash_init_fn init_fn;
-
-	/* The hash context cloning function. */
 	git_hash_clone_fn clone_fn;
-
-	/* The hash update function. */
 	git_hash_update_fn update_fn;
-
-	/* The hash finalization function. */
 	git_hash_final_fn final_fn;
-
-	/* The hash finalization function for object IDs. */
 	git_hash_final_oid_fn final_oid_fn;
+	git_hash_discard_fn discard_fn;
 
 	/* The OID of the empty tree. */
 	const struct object_id *empty_tree;
@@ -320,11 +335,40 @@ struct git_hash_algo {
 };
 extern const struct git_hash_algo hash_algos[GIT_HASH_NALGOS];
 
+/*
+ * Prepare an uninitialized hash context for use. You must eventually release
+ * the context with git_hash_final() (or final_oid()) or by calling
+ * git_hash_discard().
+ */
 void git_hash_init(struct git_hash_ctx *ctx, const struct git_hash_algo *algop);
+
+/*
+ * Clone the state of a hash. Both src and dst must have been initialized with
+ * git_hash_init().
+ */
 void git_hash_clone(struct git_hash_ctx *dst, const struct git_hash_ctx *src);
+
+/*
+ * Add more data to an initialized hash context.
+ */
 void git_hash_update(struct git_hash_ctx *ctx, const void *in, size_t len);
+
+/*
+ * Retrieve the final hash value from a context, releasing any resources.
+ */
 void git_hash_final(unsigned char *hash, struct git_hash_ctx *ctx);
+
+/*
+ * Like git_hash_final(), but write the result into an object_id.
+ */
 void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx);
+
+/*
+ * Discard a hash context without computing the final value, but still
+ * releasing any resources.
+ */
+void git_hash_discard(struct git_hash_ctx *ctx);
+
 const struct git_hash_algo *hash_algo_ptr_by_number(uint32_t algo);
 struct git_hash_ctx *git_hash_alloc(void);
 void git_hash_free(struct git_hash_ctx *ctx);
diff --git a/http-push.c b/http-push.c
index 3c23cbb..94a1fac 100644
--- a/http-push.c
+++ b/http-push.c
@@ -776,7 +776,7 @@ static void handle_new_lock_ctx(struct xml_ctx *ctx, int tag_closed)
 		} else if (!strcmp(ctx->name, DAV_ACTIVELOCK_TOKEN)) {
 			lock->token = xstrdup(ctx->cdata);
 
-			the_hash_algo->init_fn(&hash_ctx);
+			git_hash_init(&hash_ctx, the_hash_algo);
 			git_hash_update(&hash_ctx, lock->token, strlen(lock->token));
 			git_hash_final(lock_token_hash, &hash_ctx);
 
@@ -1716,7 +1716,7 @@ int cmd_main(int argc, const char **argv)
 {
 	struct transfer_request *request;
 	struct transfer_request *next_request;
-	struct refspec rs = REFSPEC_INIT_PUSH;
+	struct refspec rs = REFSPEC_INIT_PUSH(the_hash_algo);
 	struct remote_lock *ref_lock = NULL;
 	struct remote_lock *info_ref_lock = NULL;
 	int delete_branch = 0;
diff --git a/http.c b/http.c
index b4e7b8d..caccf21 100644
--- a/http.c
+++ b/http.c
@@ -2879,7 +2879,7 @@ struct http_object_request *new_http_object_request(const char *base_url,
 
 	git_inflate_init(&freq->stream);
 
-	the_hash_algo->init_fn(&freq->c);
+	git_hash_init(&freq->c, the_hash_algo);
 
 	freq->url = get_remote_object_url(base_url, hex, 0);
 
@@ -2915,7 +2915,7 @@ struct http_object_request *new_http_object_request(const char *base_url,
 		git_inflate_end(&freq->stream);
 		memset(&freq->stream, 0, sizeof(freq->stream));
 		git_inflate_init(&freq->stream);
-		the_hash_algo->init_fn(&freq->c);
+		git_hash_init(&freq->c, the_hash_algo);
 		if (prev_posn>0) {
 			prev_posn = 0;
 			lseek(freq->localfile, 0, SEEK_SET);
@@ -3028,6 +3028,7 @@ void release_http_object_request(struct http_object_request **freq_p)
 	curl_slist_free_all(freq->headers);
 	strbuf_release(&freq->tmpfile);
 	git_inflate_end(&freq->stream);
+	git_hash_discard(&freq->c);
 
 	free(freq);
 	*freq_p = NULL;
diff --git a/imap-send.c b/imap-send.c
index cfd6a51..0d16d02 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1750,6 +1750,7 @@ static int curl_append_msgs_to_imap(struct imap_server_conf *server,
 
 	curl_easy_cleanup(curl);
 	curl_global_cleanup();
+	strbuf_release(&msgbuf.buf);
 
 	if (cred.username) {
 		if (res == CURLE_OK)
diff --git a/line-log.c b/line-log.c
index 5fc75ae..0179f13 100644
--- a/line-log.c
+++ b/line-log.c
@@ -1141,8 +1141,7 @@ int line_log_process_ranges_arbitrary_commit(struct rev_info *rev, struct commit
 
 	if (range) {
 		if (commit->parents && !bloom_filter_check(rev, commit, range)) {
-			struct line_log_data *prange = line_log_data_copy(range);
-			add_line_range(rev, commit->parents->item, prange);
+			add_line_range(rev, commit->parents->item, range);
 			clear_commit_line_range(rev, commit);
 		} else if (commit->parents && commit->parents->next)
 			changed = process_ranges_merge_commit(rev, commit, range);
diff --git a/lockfile.c b/lockfile.c
index 7add2f1..100f603 100644
--- a/lockfile.c
+++ b/lockfile.c
@@ -2,11 +2,14 @@
  * Copyright (c) 2005, Junio C Hamano
  */
 
+#define USE_THE_REPOSITORY_VARIABLE
+
 #include "git-compat-util.h"
 #include "abspath.h"
 #include "gettext.h"
 #include "lockfile.h"
 #include "parse.h"
+#include "repository.h"
 #include "strbuf.h"
 #include "wrapper.h"
 
@@ -162,8 +165,8 @@ static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)
 }
 
 /* Make sure errno contains a meaningful value on error */
-static int lock_file(struct lock_file *lk, const char *path, int flags,
-		     int mode)
+static int lock_file(struct repository *r, struct lock_file *lk,
+		     const char *path, int flags, int mode)
 {
 	struct strbuf base_path = STRBUF_INIT;
 	struct strbuf lock_path = STRBUF_INIT;
@@ -176,7 +179,7 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,
 	get_lock_path(&lock_path, base_path.buf);
 	get_pid_path(&pid_path, base_path.buf);
 
-	lk->tempfile = create_tempfile_mode(lock_path.buf, mode);
+	lk->tempfile = repo_create_tempfile_mode(r, lock_path.buf, mode);
 	if (lk->tempfile)
 		lk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);
 
@@ -200,8 +203,9 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,
  * timeout_ms milliseconds. If timeout_ms is 0, try locking the file
  * exactly once. If timeout_ms is -1, try indefinitely.
  */
-static int lock_file_timeout(struct lock_file *lk, const char *path,
-			     int flags, long timeout_ms, int mode)
+static int lock_file_timeout(struct repository *r, struct lock_file *lk,
+			     const char *path, int flags, long timeout_ms,
+			     int mode)
 {
 	int n = 1;
 	int multiplier = 1;
@@ -209,7 +213,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
 	static int random_initialized = 0;
 
 	if (timeout_ms == 0)
-		return lock_file(lk, path, flags, mode);
+		return lock_file(r, lk, path, flags, mode);
 
 	if (!random_initialized) {
 		srand((unsigned int)getpid());
@@ -223,7 +227,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
 		long backoff_ms, wait_ms;
 		int fd;
 
-		fd = lock_file(lk, path, flags, mode);
+		fd = lock_file(r, lk, path, flags, mode);
 
 		if (fd >= 0)
 			return fd; /* success */
@@ -308,7 +312,17 @@ int hold_lock_file_for_update_timeout_mode(struct lock_file *lk,
 					   const char *path, int flags,
 					   long timeout_ms, int mode)
 {
-	int fd = lock_file_timeout(lk, path, flags, timeout_ms, mode);
+	return repo_hold_lock_file_for_update_timeout_mode(the_repository,
+							   lk, path, flags,
+							   timeout_ms, mode);
+}
+
+int repo_hold_lock_file_for_update_timeout_mode(struct repository *r,
+						struct lock_file *lk,
+						const char *path, int flags,
+						long timeout_ms, int mode)
+{
+	int fd = lock_file_timeout(r, lk, path, flags, timeout_ms, mode);
 	if (fd < 0) {
 		if (flags & LOCK_DIE_ON_ERROR)
 			unable_to_lock_die(path, errno);
diff --git a/lockfile.h b/lockfile.h
index e7233f2..1667612 100644
--- a/lockfile.h
+++ b/lockfile.h
@@ -189,6 +189,11 @@ int hold_lock_file_for_update_timeout_mode(
 	struct lock_file *lk, const char *path,
 	int flags, long timeout_ms, int mode);
 
+int repo_hold_lock_file_for_update_timeout_mode(struct repository *r,
+						struct lock_file *lk,
+						const char *path, int flags,
+						long timeout_ms, int mode);
+
 static inline int hold_lock_file_for_update_timeout(
 	struct lock_file *lk, const char *path,
 	int flags, long timeout_ms)
@@ -197,6 +202,16 @@ static inline int hold_lock_file_for_update_timeout(
 						      timeout_ms, 0666);
 }
 
+static inline int repo_hold_lock_file_for_update_timeout(struct repository *r,
+							 struct lock_file *lk,
+							 const char *path,
+							 int flags,
+							 long timeout_ms)
+{
+	return repo_hold_lock_file_for_update_timeout_mode(r, lk, path, flags,
+							   timeout_ms, 0666);
+}
+
 /*
  * Attempt to create a lockfile for the file at `path` and return a
  * file descriptor for writing to it, or -1 on error. The flags
@@ -208,6 +223,13 @@ static inline int hold_lock_file_for_update(
 	return hold_lock_file_for_update_timeout(lk, path, flags, 0);
 }
 
+static inline int repo_hold_lock_file_for_update(struct repository *r,
+						 struct lock_file *lk,
+						 const char *path, int flags)
+{
+	return repo_hold_lock_file_for_update_timeout(r, lk, path, flags, 0);
+}
+
 static inline int hold_lock_file_for_update_mode(
 	struct lock_file *lk, const char *path,
 	int flags, int mode)
@@ -215,6 +237,15 @@ static inline int hold_lock_file_for_update_mode(
 	return hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);
 }
 
+static inline int repo_hold_lock_file_for_update_mode(struct repository *r,
+						      struct lock_file *lk,
+						      const char *path,
+						      int flags, int mode)
+{
+	return repo_hold_lock_file_for_update_timeout_mode(r, lk, path, flags,
+							   0, mode);
+}
+
 /*
  * Return a nonzero value iff `lk` is currently locked.
  */
diff --git a/loose.c b/loose.c
index 0b626c1..e97c3bb 100644
--- a/loose.c
+++ b/loose.c
@@ -65,6 +65,7 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source_
 {
 	struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
 	FILE *fp;
+	int ret = -1;
 
 	if (!loose->map)
 		loose_object_map_init(&loose->map);
@@ -84,7 +85,6 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source_
 		return 0;
 	}
 
-	errno = 0;
 	if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header))
 		goto err;
 	while (!strbuf_getline_lf(&buf, fp)) {
@@ -98,13 +98,12 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source_
 		insert_loose_map(loose, &oid, &compat_oid);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&path);
-	return errno ? -1 : 0;
+	ret = ferror(fp) ? -1 : 0;
 err:
+	fclose(fp);
 	strbuf_release(&buf);
 	strbuf_release(&path);
-	return -1;
+	return ret;
 }
 
 int repo_read_loose_object_map(struct repository *repo)
@@ -138,7 +137,8 @@ int repo_write_loose_object_map(struct repository *repo)
 		return 0;
 
 	repo_common_path_replace(repo, &path, "objects/loose-object-idx");
-	fd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);
+	fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
+						    LOCK_DIE_ON_ERROR, -1);
 	iter = kh_begin(map);
 	if (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)
 		goto errout;
@@ -180,7 +180,8 @@ static int write_one_object(struct odb_source_loose *loose,
 	struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
 
 	strbuf_addf(&path, "%s/loose-object-idx", loose->base.path);
-	hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);
+	repo_hold_lock_file_for_update_timeout(loose->base.odb->repo, &lock,
+					       path.buf, LOCK_DIE_ON_ERROR, -1);
 
 	fd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);
 	if (fd < 0)
@@ -202,7 +203,8 @@ static int write_one_object(struct odb_source_loose *loose,
 	return 0;
 errout:
 	error_errno(_("failed to write loose object index %s"), path.buf);
-	close(fd);
+	if (fd >= 0)
+		close(fd);
 	rollback_lock_file(&lock);
 	strbuf_release(&buf);
 	strbuf_release(&path);
diff --git a/meson.build b/meson.build
index ca23580..7073d58 100644
--- a/meson.build
+++ b/meson.build
@@ -161,6 +161,21 @@
 # These machine files can be passed to `meson setup` via the `--native-file`
 # option.
 #
+# Fuzzing
+# =======
+#
+# Meson supports building the fuzzing targets by setting `-Dfuzzers=true`. By
+# default, the targets will be built without libFuzzer and thus won't be usable
+# for fuzzing. You have to configure a couple of options to properly wire up
+# libFuzzer:
+#
+#   $ meson setup build-fuzzers \
+#       -Db_sanitize=address,fuzzer-no-link \
+#       -Dfuzzers=true \
+#       -Dfuzzers_link_args=-fsanitize=fuzzer
+#   $ meson compile -C build-fuzzers
+#   $ ./build-fuzzers/oss-fuzz/fuzz-config <args>
+#
 # Cross compilation
 # =================
 #
@@ -278,7 +293,20 @@
   'compat/terminal.c',
 ]
 
+hook_list = custom_target(
+  input: 'Documentation/githooks.adoc',
+  output: 'hook-list.h',
+  command: [
+    shell,
+    meson.current_source_dir() + '/tools/generate-hooklist.sh',
+    meson.current_source_dir(),
+    '@OUTPUT@',
+  ],
+  env: script_environment,
+)
+
 libgit_sources = [
+  hook_list,
   'abspath.c',
   'add-interactive.c',
   'add-patch.c',
@@ -346,6 +374,7 @@
   'ewah/ewah_rlw.c',
   'exec-cmd.c',
   'fetch-negotiator.c',
+  'fetch-object-info.c',
   'fetch-pack.c',
   'fmt-merge-msg.c',
   'fsck.c',
@@ -568,19 +597,8 @@
   env: script_environment,
 )
 
-libgit_sources += custom_target(
-  input: 'Documentation/githooks.adoc',
-  output: 'hook-list.h',
-  command: [
-    shell,
-    meson.current_source_dir() + '/tools/generate-hooklist.sh',
-    meson.current_source_dir(),
-    '@OUTPUT@',
-  ],
-  env: script_environment,
-)
-
 builtin_sources = [
+  hook_list,
   'builtin/add.c',
   'builtin/am.c',
   'builtin/annotate.c',
diff --git a/meson_options.txt b/meson_options.txt
index d936ada..dc88f13 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -131,3 +131,5 @@
   description: 'Name of a UTF-8 locale used for testing.')
 option('fuzzers', type: 'boolean', value: false,
   description: 'Enable building fuzzers.')
+option('fuzzers_link_args', type: 'array', value: [],
+  description: 'Linker arguments used to link fuzzers. Use -fsanitize=fuzzer for fuzzing.')
diff --git a/midx-write.c b/midx-write.c
index 8c1837f..580724d 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1627,8 +1627,8 @@ static int write_midx_internal(struct write_midx_opts *opts)
 			struct strbuf lock_name = STRBUF_INIT;
 
 			get_midx_chain_filename(opts->source, &lock_name);
-			hold_lock_file_for_update(&lk, lock_name.buf,
-						  LOCK_DIE_ON_ERROR);
+			repo_hold_lock_file_for_update(r, &lk, lock_name.buf,
+						       LOCK_DIE_ON_ERROR);
 			strbuf_release(&lock_name);
 		}
 
@@ -1647,7 +1647,8 @@ static int write_midx_internal(struct write_midx_opts *opts)
 		f = hashfd(r->hash_algo, get_tempfile_fd(incr),
 			   get_tempfile_path(incr));
 	} else {
-		hold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR);
+		repo_hold_lock_file_for_update(r, &lk, midx_name.buf,
+					       LOCK_DIE_ON_ERROR);
 		f = hashfd(r->hash_algo, get_lock_file_fd(&lk),
 			   get_lock_file_path(&lk));
 	}
diff --git a/midx.c b/midx.c
index cc6b94f..76c3f92 100644
--- a/midx.c
+++ b/midx.c
@@ -101,7 +101,7 @@ static int midx_read_object_offsets(const unsigned char *chunk_start,
 
 struct multi_pack_index *get_multi_pack_index(struct odb_source_packed *source)
 {
-	odb_source_packed_prepare(source);
+	odb_source_prepare(&source->base, 0);
 	return source->midx;
 }
 
diff --git a/name-hash.c b/name-hash.c
index b91e276..83757db 100644
--- a/name-hash.c
+++ b/name-hash.c
@@ -126,7 +126,7 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce)
 		hashmap_add(&istate->name_hash, &ce->ent);
 	}
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		add_dir_entry(istate, ce);
 }
 
@@ -207,7 +207,7 @@ static int lookup_lazy_params(struct index_state *istate)
 	 * code to build the "istate->name_hash".  We don't
 	 * need the complexity here.
 	 */
-	if (!ignore_case)
+	if (!repo_ignore_case(the_repository))
 		return 0;
 
 	nr_cpus = online_cpus();
@@ -651,7 +651,7 @@ void remove_name_hash(struct index_state *istate, struct cache_entry *ce)
 	ce->ce_flags &= ~CE_HASHED;
 	hashmap_remove(&istate->name_hash, &ce->ent, ce);
 
-	if (ignore_case)
+	if (repo_ignore_case(the_repository))
 		remove_dir_entry(istate, ce);
 }
 
diff --git a/negotiator/default.c b/negotiator/default.c
index 78d58d5..19cdf38 100644
--- a/negotiator/default.c
+++ b/negotiator/default.c
@@ -113,10 +113,12 @@ static const struct object_id *get_rev(struct negotiation_state *ns)
 		unsigned int mark;
 		struct commit_list *parents;
 
-		if (ns->rev_list.nr == 0 || ns->non_common_revs == 0)
+		if (ns->non_common_revs == 0)
 			return NULL;
 
 		commit = prio_queue_get(&ns->rev_list);
+		if (!commit)
+			return NULL;
 		repo_parse_commit(the_repository, commit);
 		parents = commit->parents;
 
diff --git a/negotiator/skipping.c b/negotiator/skipping.c
index 68c9b3b..db90fa7 100644
--- a/negotiator/skipping.c
+++ b/negotiator/skipping.c
@@ -143,8 +143,7 @@ static int push_parent(struct data *data, struct entry *entry,
 		/*
 		 * Find the existing entry and use it.
 		 */
-		for (size_t i = 0; i < data->rev_list.nr; i++) {
-			parent_entry = data->rev_list.array[i].data;
+		prio_queue_for_each(&data->rev_list, parent_entry) {
 			if (parent_entry->commit == to_push)
 				goto parent_found;
 		}
@@ -181,10 +180,12 @@ static const struct object_id *get_rev(struct data *data)
 		struct commit_list *p;
 		int parent_pushed = 0;
 
-		if (data->rev_list.nr == 0 || data->non_common_revs == 0)
+		if (data->non_common_revs == 0)
 			return NULL;
 
 		entry = prio_queue_get(&data->rev_list);
+		if (!entry)
+			return NULL;
 		commit = entry->commit;
 		commit->object.flags |= POPPED;
 		if (!(commit->object.flags & COMMON))
@@ -253,8 +254,9 @@ static void have_sent(struct fetch_negotiator *n, struct commit *c)
 static void release(struct fetch_negotiator *n)
 {
 	struct data *data = n->data;
-	for (size_t i = 0; i < data->rev_list.nr; i++)
-		free(data->rev_list.array[i].data);
+	void *entry;
+	prio_queue_for_each(&data->rev_list, entry)
+		free(entry);
 	clear_prio_queue(&data->rev_list);
 	FREE_AND_NULL(data);
 }
diff --git a/object-file.c b/object-file.c
index 6453b1d..ec35c31 100644
--- a/object-file.c
+++ b/object-file.c
@@ -27,6 +27,7 @@
 #include "path.h"
 #include "read-cache-ll.h"
 #include "setup.h"
+#include "strvec.h"
 #include "tempfile.h"
 #include "tmp-objdir.h"
 
@@ -66,9 +67,17 @@ const char *odb_loose_path(struct odb_source_loose *loose,
 }
 
 /* Returns 1 if we have successfully freshened the file, 0 otherwise. */
-static int freshen_file(const char *fn)
+static int freshen_file(const char *fn, const time_t *mtime)
 {
-	return !utime(fn, NULL);
+	struct utimbuf times, *timesp = NULL;
+
+	if (mtime) {
+		times.actime = *mtime;
+		times.modtime = *mtime;
+		timesp = &times;
+	}
+
+	return !utime(fn, timesp);
 }
 
 /*
@@ -78,11 +87,12 @@ static int freshen_file(const char *fn)
  * either does not exist on disk, or has a stale mtime and may be subject to
  * pruning).
  */
-int check_and_freshen_file(const char *fn, int freshen)
+int check_and_freshen_file(const char *fn, int freshen,
+			   const time_t *mtime)
 {
 	if (access(fn, F_OK))
 		return 0;
-	if (freshen && !freshen_file(fn))
+	if (freshen && !freshen_file(fn, mtime))
 		return 0;
 	return 1;
 }
@@ -124,16 +134,13 @@ int stream_object_signature(struct repository *r,
 	hdrlen = format_object_header(hdr, sizeof(hdr), st->type, st->size);
 
 	/* Sha1.. */
-	r->hash_algo->init_fn(&c);
+	git_hash_init(&c, r->hash_algo);
 	git_hash_update(&c, hdr, hdrlen);
 	for (;;) {
 		char buf[1024 * 16];
 		ssize_t readlen = odb_read_stream_read(st, buf, sizeof(buf));
-
-		if (readlen < 0) {
-			odb_read_stream_close(st);
+		if (readlen < 0)
 			return -1;
-		}
 		if (!readlen)
 			break;
 		git_hash_update(&c, buf, readlen);
@@ -315,31 +322,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi)
 	return 0;
 }
 
-static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,
-			     const void *buf, size_t len,
-			     struct object_id *oid,
-			     char *hdr, size_t *hdrlen)
-{
-	algo->init_fn(c);
-	git_hash_update(c, hdr, *hdrlen);
-	git_hash_update(c, buf, len);
-	git_hash_final_oid(oid, c);
-}
-
-void write_object_file_prepare(const struct git_hash_algo *algo,
-			       const void *buf, size_t len,
-			       enum object_type type, struct object_id *oid,
-			       char *hdr, size_t *hdrlen)
-{
-	struct git_hash_ctx c;
-
-	/* Generate the header */
-	*hdrlen = format_object_header(hdr, *hdrlen, type, len);
-
-	/* Hash (function pointers) computation */
-	hash_object_body(algo, &c, buf, len, oid, hdr, hdrlen);
-}
-
 #define CHECK_COLLISION_DEST_VANISHED -2
 
 static int check_collision(const char *source, const char *dest)
@@ -411,11 +393,12 @@ int finalize_object_file_flags(struct repository *repo,
 {
 	unsigned retries = 0;
 	int ret;
+	struct repo_config_values *cfg = repo_config_values(repo);
 
 retry:
 	ret = 0;
 
-	if (object_creation_mode == OBJECT_CREATION_USES_RENAMES)
+	if (cfg->object_creation_mode == OBJECT_CREATION_USES_RENAMES)
 		goto try_rename;
 	else if (link(tmpfile, filename))
 		ret = errno;
@@ -475,10 +458,16 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf,
 		      size_t len, enum object_type type,
 		      struct object_id *oid)
 {
+	struct git_hash_ctx c;
 	char hdr[MAX_HEADER_LEN];
-	size_t hdrlen = sizeof(hdr);
+	int hdrlen;
 
-	write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
+	hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
+
+	git_hash_init(&c, algo);
+	git_hash_update(&c, hdr, hdrlen);
+	git_hash_update(&c, buf, len);
+	git_hash_final_oid(oid, &c);
 }
 
 struct transaction_packfile {
@@ -497,9 +486,10 @@ struct odb_transaction_files {
 
 	struct tmp_objdir *objdir;
 	struct transaction_packfile packfile;
+	const char *prefix;
 };
 
-static void prepare_loose_object_transaction(struct odb_transaction *base)
+int odb_transaction_files_prepare(struct odb_transaction *base)
 {
 	struct odb_transaction_files *transaction =
 		container_of_or_null(base, struct odb_transaction_files, base);
@@ -511,19 +501,28 @@ static void prepare_loose_object_transaction(struct odb_transaction *base)
 	 * added at the time they call odb_transaction_files_begin.
 	 */
 	if (!transaction || transaction->objdir)
-		return;
+		return 0;
 
-	transaction->objdir = tmp_objdir_create(base->source->odb->repo, "bulk-fsync");
-	if (transaction->objdir)
-		tmp_objdir_replace_primary_odb(transaction->objdir, 0);
+	transaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);
+	if (!transaction->objdir)
+		return error(_("unable to create temporary object directory"));
+
+	tmp_objdir_replace_primary_odb(transaction->objdir, 0);
+
+	return 0;
 }
 
-static void fsync_loose_object_transaction(struct odb_transaction *base,
-					   int fd, const char *filename)
+void odb_transaction_files_fsync(struct odb_transaction *base,
+				 int fd, const char *filename)
 {
 	struct odb_transaction_files *transaction =
 		container_of_or_null(base, struct odb_transaction_files, base);
 
+	if (!transaction || !transaction->objdir) {
+		fsync_or_die(fd, filename);
+		return;
+	}
+
 	/*
 	 * If we have an active ODB transaction, we issue a call that
 	 * cleans the filesystem page cache but avoids a hardware flush
@@ -531,8 +530,7 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,
 	 * before renaming the objects to their final names as part of
 	 * flush_batch_fsync.
 	 */
-	if (!transaction || !transaction->objdir ||
-	    git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {
+	if (git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {
 		if (errno == ENOSYS)
 			warning(_("core.fsyncMethod = batch is unsupported on this platform"));
 		fsync_or_die(fd, filename);
@@ -540,432 +538,6 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,
 }
 
 /*
- * Cleanup after batch-mode fsync_object_files.
- */
-static void flush_loose_object_transaction(struct odb_transaction_files *transaction)
-{
-	struct strbuf temp_path = STRBUF_INIT;
-	struct tempfile *temp;
-
-	if (!transaction->objdir)
-		return;
-
-	/*
-	 * Issue a full hardware flush against a temporary file to ensure
-	 * that all objects are durable before any renames occur. The code in
-	 * fsync_loose_object_transaction has already issued a writeout
-	 * request, but it has not flushed any writeback cache in the storage
-	 * hardware or any filesystem logs. This fsync call acts as a barrier
-	 * to ensure that the data in each new object file is durable before
-	 * the final name is visible.
-	 */
-	strbuf_addf(&temp_path, "%s/bulk_fsync_XXXXXX",
-		    repo_get_object_directory(transaction->base.source->odb->repo));
-	temp = xmks_tempfile(temp_path.buf);
-	fsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));
-	delete_tempfile(&temp);
-	strbuf_release(&temp_path);
-
-	/*
-	 * Make the object files visible in the primary ODB after their data is
-	 * fully durable.
-	 */
-	tmp_objdir_migrate(transaction->objdir);
-	transaction->objdir = NULL;
-}
-
-/* Finalize a file on disk, and close it. */
-static void close_loose_object(struct odb_source_loose *loose,
-			       int fd, const char *filename)
-{
-	if (loose->base.will_destroy)
-		goto out;
-
-	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
-		fsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);
-	else if (fsync_object_files > 0)
-		fsync_or_die(fd, filename);
-	else
-		fsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd,
-				       filename);
-
-out:
-	if (close(fd) != 0)
-		die_errno(_("error when closing loose object file"));
-}
-
-/* Size of directory component, including the ending '/' */
-static inline int directory_size(const char *filename)
-{
-	const char *s = strrchr(filename, '/');
-	if (!s)
-		return 0;
-	return s - filename + 1;
-}
-
-/*
- * This creates a temporary file in the same directory as the final
- * 'filename'
- *
- * We want to avoid cross-directory filename renames, because those
- * can have problems on various filesystems (FAT, NFS, Coda).
- */
-static int create_tmpfile(struct repository *repo,
-			  struct strbuf *tmp, const char *filename)
-{
-	int fd, dirlen = directory_size(filename);
-
-	strbuf_reset(tmp);
-	strbuf_add(tmp, filename, dirlen);
-	strbuf_addstr(tmp, "tmp_obj_XXXXXX");
-	fd = git_mkstemp_mode(tmp->buf, 0444);
-	if (fd < 0 && dirlen && errno == ENOENT) {
-		/*
-		 * Make sure the directory exists; note that the contents
-		 * of the buffer are undefined after mkstemp returns an
-		 * error, so we have to rewrite the whole buffer from
-		 * scratch.
-		 */
-		strbuf_reset(tmp);
-		strbuf_add(tmp, filename, dirlen - 1);
-		if (mkdir(tmp->buf, 0777) && errno != EEXIST)
-			return -1;
-		if (adjust_shared_perm(repo, tmp->buf))
-			return -1;
-
-		/* Try again */
-		strbuf_addstr(tmp, "/tmp_obj_XXXXXX");
-		fd = git_mkstemp_mode(tmp->buf, 0444);
-	}
-	return fd;
-}
-
-/**
- * Common steps for loose object writers to start writing loose
- * objects:
- *
- * - Create tmpfile for the loose object.
- * - Setup zlib stream for compression.
- * - Start to feed header to zlib stream.
- *
- * Returns a "fd", which should later be provided to
- * end_loose_object_common().
- */
-static int start_loose_object_common(struct odb_source_loose *loose,
-				     struct strbuf *tmp_file,
-				     const char *filename, unsigned flags,
-				     git_zstream *stream,
-				     unsigned char *buf, size_t buflen,
-				     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
-				     char *hdr, int hdrlen)
-{
-	const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;
-	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
-	int fd;
-	struct repo_config_values *cfg = repo_config_values(the_repository);
-
-	fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);
-	if (fd < 0) {
-		if (flags & ODB_WRITE_OBJECT_SILENT)
-			return -1;
-		else if (errno == EACCES)
-			return error(_("insufficient permission for adding "
-				       "an object to repository database %s"),
-				     loose->base.path);
-		else
-			return error_errno(
-				_("unable to create temporary file"));
-	}
-
-	/*  Setup zlib stream for compression */
-	git_deflate_init(stream, cfg->zlib_compression_level);
-	stream->next_out = buf;
-	stream->avail_out = buflen;
-	algo->init_fn(c);
-	if (compat && compat_c)
-		compat->init_fn(compat_c);
-
-	/*  Start to feed header to zlib stream */
-	stream->next_in = (unsigned char *)hdr;
-	stream->avail_in = hdrlen;
-	while (git_deflate(stream, 0) == Z_OK)
-		; /* nothing */
-	git_hash_update(c, hdr, hdrlen);
-	if (compat && compat_c)
-		git_hash_update(compat_c, hdr, hdrlen);
-
-	return fd;
-}
-
-/**
- * Common steps for the inner git_deflate() loop for writing loose
- * objects. Returns what git_deflate() returns.
- */
-static int write_loose_object_common(struct odb_source_loose *loose,
-				     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
-				     git_zstream *stream, const int flush,
-				     unsigned char *in0, const int fd,
-				     unsigned char *compressed,
-				     const size_t compressed_len)
-{
-	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
-	int ret;
-
-	ret = git_deflate(stream, flush ? Z_FINISH : 0);
-	git_hash_update(c, in0, stream->next_in - in0);
-	if (compat && compat_c)
-		git_hash_update(compat_c, in0, stream->next_in - in0);
-	if (write_in_full(fd, compressed, stream->next_out - compressed) < 0)
-		die_errno(_("unable to write loose object file"));
-	stream->next_out = compressed;
-	stream->avail_out = compressed_len;
-
-	return ret;
-}
-
-/**
- * Common steps for loose object writers to end writing loose objects:
- *
- * - End the compression of zlib stream.
- * - Get the calculated oid to "oid".
- */
-static int end_loose_object_common(struct odb_source_loose *loose,
-				   struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
-				   git_zstream *stream, struct object_id *oid,
-				   struct object_id *compat_oid)
-{
-	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
-	int ret;
-
-	ret = git_deflate_end_gently(stream);
-	if (ret != Z_OK)
-		return ret;
-	git_hash_final_oid(oid, c);
-	if (compat && compat_c)
-		git_hash_final_oid(compat_oid, compat_c);
-
-	return Z_OK;
-}
-
-int write_loose_object(struct odb_source_loose *loose,
-		       const struct object_id *oid, char *hdr,
-		       int hdrlen, const void *buf, unsigned long len,
-		       time_t mtime, unsigned flags)
-{
-	int fd, ret;
-	unsigned char compressed[4096];
-	git_zstream stream;
-	struct git_hash_ctx c;
-	struct object_id parano_oid;
-	static struct strbuf tmp_file = STRBUF_INIT;
-	static struct strbuf filename = STRBUF_INIT;
-
-	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
-		prepare_loose_object_transaction(loose->base.odb->transaction);
-
-	odb_loose_path(loose, &filename, oid);
-
-	fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,
-				       &stream, compressed, sizeof(compressed),
-				       &c, NULL, hdr, hdrlen);
-	if (fd < 0)
-		return -1;
-
-	/* Then the data itself.. */
-	stream.next_in = (void *)buf;
-	stream.avail_in = len;
-	do {
-		unsigned char *in0 = stream.next_in;
-
-		ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,
-						compressed, sizeof(compressed));
-	} while (ret == Z_OK);
-
-	if (ret != Z_STREAM_END)
-		die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid),
-		    ret);
-	ret = end_loose_object_common(loose, &c, NULL, &stream, &parano_oid, NULL);
-	if (ret != Z_OK)
-		die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid),
-		    ret);
-	if (!oideq(oid, &parano_oid))
-		die(_("confused by unstable object source data for %s"),
-		    oid_to_hex(oid));
-
-	close_loose_object(loose, fd, tmp_file.buf);
-
-	if (mtime) {
-		struct utimbuf utb;
-		utb.actime = mtime;
-		utb.modtime = mtime;
-		if (utime(tmp_file.buf, &utb) < 0 &&
-		    !(flags & ODB_WRITE_OBJECT_SILENT))
-			warning_errno(_("failed utime() on %s"), tmp_file.buf);
-	}
-
-	return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
-					  FOF_SKIP_COLLISION_CHECK);
-}
-
-int odb_source_loose_write_stream(struct odb_source_loose *loose,
-				  struct odb_write_stream *in_stream, size_t len,
-				  struct object_id *oid)
-{
-	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
-	struct object_id compat_oid;
-	int fd, ret, err = 0, flush = 0;
-	unsigned char compressed[4096];
-	git_zstream stream;
-	struct git_hash_ctx c, compat_c;
-	struct strbuf tmp_file = STRBUF_INIT;
-	struct strbuf filename = STRBUF_INIT;
-	unsigned char buf[8192];
-	int dirlen;
-	char hdr[MAX_HEADER_LEN];
-	int hdrlen;
-
-	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
-		prepare_loose_object_transaction(loose->base.odb->transaction);
-
-	/* Since oid is not determined, save tmp file to odb path. */
-	strbuf_addf(&filename, "%s/", loose->base.path);
-	hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);
-
-	/*
-	 * Common steps for write_loose_object and stream_loose_object to
-	 * start writing loose objects:
-	 *
-	 *  - Create tmpfile for the loose object.
-	 *  - Setup zlib stream for compression.
-	 *  - Start to feed header to zlib stream.
-	 */
-	fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,
-				       &stream, compressed, sizeof(compressed),
-				       &c, &compat_c, hdr, hdrlen);
-	if (fd < 0) {
-		err = -1;
-		goto cleanup;
-	}
-
-	/* Then the data itself.. */
-	do {
-		unsigned char *in0 = stream.next_in;
-
-		if (!stream.avail_in && !in_stream->is_finished) {
-			ssize_t read_len = odb_write_stream_read(in_stream, buf,
-								 sizeof(buf));
-			if (read_len < 0) {
-				close(fd);
-				err = -1;
-				goto cleanup;
-			}
-
-			stream.avail_in = read_len;
-			stream.next_in = buf;
-			in0 = buf;
-			/* All data has been read. */
-			if (in_stream->is_finished)
-				flush = 1;
-		}
-		ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,
-						compressed, sizeof(compressed));
-		/*
-		 * Unlike write_loose_object(), we do not have the entire
-		 * buffer. If we get Z_BUF_ERROR due to too few input bytes,
-		 * then we'll replenish them in the next input_stream->read()
-		 * call when we loop.
-		 */
-	} while (ret == Z_OK || ret == Z_BUF_ERROR);
-
-	if (stream.total_in != len + hdrlen)
-		die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in,
-		    (uintmax_t)len + hdrlen);
-
-	/*
-	 * Common steps for write_loose_object and stream_loose_object to
-	 * end writing loose object:
-	 *
-	 *  - End the compression of zlib stream.
-	 *  - Get the calculated oid.
-	 */
-	if (ret != Z_STREAM_END)
-		die(_("unable to stream deflate new object (%d)"), ret);
-	ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);
-	if (ret != Z_OK)
-		die(_("deflateEnd on stream object failed (%d)"), ret);
-	close_loose_object(loose, fd, tmp_file.buf);
-
-	if (odb_freshen_object(loose->base.odb, oid)) {
-		unlink_or_warn(tmp_file.buf);
-		goto cleanup;
-	}
-	odb_loose_path(loose, &filename, oid);
-
-	/* We finally know the object path, and create the missing dir. */
-	dirlen = directory_size(filename.buf);
-	if (dirlen) {
-		struct strbuf dir = STRBUF_INIT;
-		strbuf_add(&dir, filename.buf, dirlen);
-
-		if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&
-		    errno != EEXIST) {
-			err = error_errno(_("unable to create directory %s"), dir.buf);
-			strbuf_release(&dir);
-			goto cleanup;
-		}
-		strbuf_release(&dir);
-	}
-
-	err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
-					 FOF_SKIP_COLLISION_CHECK);
-	if (!err && compat)
-		err = repo_add_loose_object_map(loose, oid, &compat_oid);
-cleanup:
-	strbuf_release(&tmp_file);
-	strbuf_release(&filename);
-	return err;
-}
-
-int force_object_loose(struct odb_source *source,
-		       const struct object_id *oid, time_t mtime)
-{
-	struct odb_source_files *files = odb_source_files_downcast(source);
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
-	void *buf;
-	size_t len;
-	struct object_info oi = OBJECT_INFO_INIT;
-	struct object_id compat_oid;
-	enum object_type type;
-	char hdr[MAX_HEADER_LEN];
-	int hdrlen;
-	int ret;
-
-	for (struct odb_source *s = source->odb->sources; s; s = s->next) {
-		struct odb_source_files *files = odb_source_files_downcast(s);
-		if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
-			return 0;
-	}
-
-	oi.typep = &type;
-	oi.sizep = &len;
-	oi.contentp = &buf;
-	if (odb_read_object_info_extended(source->odb, oid, &oi, 0))
-		return error(_("cannot read object for %s"), oid_to_hex(oid));
-	if (compat) {
-		if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid))
-			return error(_("cannot map object %s to %s"),
-				     oid_to_hex(oid), compat->name);
-	}
-	hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
-	ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);
-	if (!ret && compat)
-		ret = repo_add_loose_object_map(files->loose, oid, &compat_oid);
-	free(buf);
-
-	return ret;
-}
-
-/*
  * We can't use the normal fsck_error_function() for index_mem(),
  * because we don't yet have a valid oid for it to report. Instead,
  * report the minimal fsck error here, and rely on the caller to
@@ -1141,7 +713,7 @@ static int hash_blob_stream(struct odb_write_stream *stream,
 
 	header_len = format_object_header((char *)buf, sizeof(buf),
 					  OBJ_BLOB, size);
-	hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, hash_algo);
 	git_hash_update(&ctx, buf, header_len);
 
 	while (!stream->is_finished) {
@@ -1313,7 +885,7 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas
 
 	header_len = format_object_header((char *)obuf, sizeof(obuf),
 					  OBJ_BLOB, size);
-	transaction->base.source->odb->repo->hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, transaction->base.source->odb->repo->hash_algo);
 	git_hash_update(&ctx, obuf, header_len);
 
 	/*
@@ -1352,6 +924,8 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas
 			   state->alloc_written);
 		state->written[state->nr_written++] = idx;
 	}
+
+	hashfile_checkpoint_release(&checkpoint);
 	return 0;
 }
 
@@ -1381,13 +955,17 @@ int index_fd(struct index_state *istate, struct object_id *oid,
 
 		if (flags & INDEX_WRITE_OBJECT) {
 			struct object_database *odb = the_repository->objects;
-			struct odb_transaction *transaction = odb_transaction_begin(odb);
+			struct odb_transaction *transaction = odb->transaction;
+			int inflight = !!transaction;
 
-			ret = odb_transaction_write_object_stream(odb->transaction,
+			if (!inflight)
+				odb_transaction_begin_or_die(odb, &transaction, 0);
+			ret = odb_transaction_write_object_stream(transaction,
 								  &stream,
 								  xsize_t(st->st_size),
 								  oid);
-			odb_transaction_commit(transaction);
+			if (!inflight)
+				odb_transaction_commit(transaction);
 		} else {
 			ret = hash_blob_stream(&stream,
 					       the_repository->hash_algo, oid,
@@ -1558,7 +1136,7 @@ static int check_stream_oid(git_zstream *stream,
 	unsigned long total_read;
 	int status = Z_OK;
 
-	algop->init_fn(&c);
+	git_hash_init(&c, algop);
 	git_hash_update(&c, hdr, stream->total_out);
 
 	/*
@@ -1585,11 +1163,13 @@ static int check_stream_oid(git_zstream *stream,
 
 	if (status != Z_STREAM_END) {
 		error(_("corrupt loose object '%s'"), oid_to_hex(expected_oid));
+		git_hash_discard(&c);
 		return -1;
 	}
 	if (stream->avail_in) {
 		error(_("garbage at end of loose object '%s'"),
 		      oid_to_hex(expected_oid));
+		git_hash_discard(&c);
 		return -1;
 	}
 
@@ -1670,27 +1250,103 @@ int read_loose_object(struct repository *repo,
 	return ret;
 }
 
-static void odb_transaction_files_commit(struct odb_transaction *base)
+static int odb_transaction_files_commit(struct odb_transaction *base)
 {
 	struct odb_transaction_files *transaction =
 		container_of(base, struct odb_transaction_files, base);
 
-	flush_loose_object_transaction(transaction);
+	if (transaction->objdir) {
+		struct strbuf temp_path = STRBUF_INIT;
+		struct tempfile *temp;
+
+		/*
+		 * Issue a full hardware flush against a temporary file to ensure
+		 * that all objects are durable before any renames occur. The code in
+		 * odb_transaction_files_fsync has already issued a writeout
+		 * request, but it has not flushed any writeback cache in the storage
+		 * hardware or any filesystem logs. This fsync call acts as a barrier
+		 * to ensure that the data in each new object file is durable before
+		 * the final name is visible.
+		 */
+		strbuf_addf(&temp_path, "%s/bulk_fsync_XXXXXX",
+			    repo_get_object_directory(transaction->base.source->odb->repo));
+		temp = xmks_tempfile(temp_path.buf);
+		fsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));
+		delete_tempfile(&temp);
+		strbuf_release(&temp_path);
+
+		/*
+		 * Make the object files visible in the primary ODB after their data is
+		 * fully durable.
+		 */
+		if (tmp_objdir_migrate(transaction->objdir))
+			return error(_("unable to migrate temporary objects"));
+
+		transaction->objdir = NULL;
+	}
+
 	flush_packfile_transaction(transaction);
+
+	return 0;
 }
 
-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)
+static int odb_transaction_files_env(struct odb_transaction *base,
+				     struct strvec *env)
+{
+	struct odb_transaction_files *transaction =
+		container_of(base, struct odb_transaction_files, base);
+	int ret;
+
+	ret = odb_transaction_files_prepare(&transaction->base);
+	if (!ret)
+		strvec_pushv(env, tmp_objdir_env(transaction->objdir));
+
+	return ret;
+}
+
+int odb_transaction_files_begin(struct odb_source *source,
+				struct odb_transaction **out,
+				enum odb_transaction_flags flags)
 {
 	struct odb_transaction_files *transaction;
-	struct object_database *odb = source->odb;
-
-	if (odb->transaction)
-		return NULL;
 
 	transaction = xcalloc(1, sizeof(*transaction));
 	transaction->base.source = source;
 	transaction->base.commit = odb_transaction_files_commit;
 	transaction->base.write_object_stream = odb_transaction_files_write_object_stream;
+	transaction->base.env = odb_transaction_files_env;
 
-	return &transaction->base;
+	transaction->prefix = "bulk-fsync";
+	if (flags & ODB_TRANSACTION_RECEIVE) {
+		/*
+		 * ODB transactions for git-receive-pack(1) eagerly create a
+		 * temporary directory and use a different temporary directory
+		 * prefix.
+		 *
+		 * NEEDSWORK: This transaction flag is only used by the "files"
+		 * backend to special case temporary directory set up and
+		 * handling. Ideally transaction users should not have to care
+		 * though. To avoid this, we could eagerly create the temporary
+		 * directory and use the same prefix name for all transactions.
+		 */
+		transaction->prefix = "incoming";
+		if (odb_transaction_files_prepare(&transaction->base)) {
+			free(transaction);
+			return -1;
+		}
+	}
+
+	*out = &transaction->base;
+
+	return 0;
+}
+
+void free_object_info_contents(struct object_info *object_info)
+{
+	if (!object_info)
+		return;
+	free(object_info->typep);
+	free(object_info->sizep);
+	free(object_info->disk_sizep);
+	free(object_info->delta_base_oid);
 }
diff --git a/object-file.h b/object-file.h
index 4c87cd1..805f2cf 100644
--- a/object-file.h
+++ b/object-file.h
@@ -5,6 +5,7 @@
 #include "object.h"
 #include "odb.h"
 #include "odb/source-loose.h"
+#include "odb/transaction.h"
 
 /* The maximum size for an object header. */
 #define MAX_HEADER_LEN 32
@@ -24,20 +25,6 @@ struct object_info;
 struct odb_source;
 
 /*
- * Write the given stream into the loose object source. The only difference
- * from the generic implementation of this function is that we don't perform an
- * object existence check here.
- *
- * TODO: We should stop exposing this function altogether and move it into
- * "odb/source-loose.c". This requires a couple of refactorings though to make
- * `force_object_loose()` generic and is thus postponed to a later point in
- * time.
- */
-int odb_source_loose_write_stream(struct odb_source_loose *source,
-				  struct odb_write_stream *stream, size_t len,
-				  struct object_id *oid);
-
-/*
  * Put in `buf` the name of the file in the local object database that
  * would be used to store a loose object with the specified oid.
  */
@@ -97,9 +84,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,
 int format_object_header(char *str, size_t size, enum object_type type,
 			 size_t objsize);
 
-int force_object_loose(struct odb_source *source,
-		       const struct object_id *oid, time_t mtime);
-
 /**
  * With in-core object data in "buf", rehash it to make sure the
  * object name actually matches "oid" to detect object corruption.
@@ -133,17 +117,10 @@ int finalize_object_file_flags(struct repository *repo,
 void hash_object_file(const struct git_hash_algo *algo, const void *buf,
 		      size_t len, enum object_type type,
 		      struct object_id *oid);
-void write_object_file_prepare(const struct git_hash_algo *algo,
-			       const void *buf, size_t len,
-			       enum object_type type, struct object_id *oid,
-			       char *hdr, size_t *hdrlen);
-int write_loose_object(struct odb_source_loose *loose,
-		       const struct object_id *oid, char *hdr,
-		       int hdrlen, const void *buf, unsigned long len,
-		       time_t mtime, unsigned flags);
 
 /* Helper to check and "touch" a file */
-int check_and_freshen_file(const char *fn, int freshen);
+int check_and_freshen_file(const char *fn, int freshen,
+			   const time_t *mtime);
 
 /*
  * Open the loose object at path, check its hash, and return the contents,
@@ -194,9 +171,14 @@ struct odb_transaction;
 /*
  * Tell the object database to optimize for adding
  * multiple objects. odb_transaction_files_commit must be called
- * to make new objects visible. If a transaction is already
- * pending, NULL is returned.
+ * to make new objects visible.
  */
-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);
+int odb_transaction_files_begin(struct odb_source *source,
+				struct odb_transaction **out,
+				enum odb_transaction_flags flags);
+
+int odb_transaction_files_prepare(struct odb_transaction *base);
+void odb_transaction_files_fsync(struct odb_transaction *base,
+				 int fd, const char *filename);
 
 #endif /* OBJECT_FILE_H */
diff --git a/object-name.c b/object-name.c
index 4615946..83efba0 100644
--- a/object-name.c
+++ b/object-name.c
@@ -1209,7 +1209,7 @@ static int get_oid_oneline(struct repository *r,
 		l->item->object.flags |= ONELINE_SEEN;
 		prio_queue_put(&copy, l->item);
 	}
-	while (copy.nr) {
+	while (prio_queue_size(&copy)) {
 		const char *p, *buf;
 		struct commit *commit;
 		int matches;
@@ -1708,8 +1708,8 @@ static char *resolve_relative_path(struct repository *r, const char *rel)
 		die(_("relative path syntax can't be used outside working tree"));
 
 	/* die() inside prefix_path() if resolved path is outside worktree */
-	return prefix_path(the_repository, startup_info->prefix,
-			   startup_info->prefix ? strlen(startup_info->prefix) : 0,
+	return prefix_path(the_repository, the_repository->prefix,
+			   the_repository->prefix ? strlen(the_repository->prefix) : 0,
 			   rel);
 }
 
diff --git a/odb.c b/odb.c
index 8966098..caf1d0f 100644
--- a/odb.c
+++ b/odb.c
@@ -691,8 +691,8 @@ static int oid_object_info_convert(struct repository *r,
 			return -1;
 		}
 	}
-	input_oi->whence = new_oi.whence;
-	input_oi->u = new_oi.u;
+	if (input_oi->source_infop)
+		*input_oi->source_infop = *new_oi.source_infop;
 	return ret;
 }
 
@@ -738,7 +738,7 @@ int odb_pretend_object(struct object_database *odb,
 		return 0;
 
 	return odb_source_write_object(odb->inmemory_objects,
-				       buf, len, type, oid, NULL, 0);
+				       buf, len, type, oid, NULL, NULL, 0);
 }
 
 void *odb_read_object(struct object_database *odb,
@@ -829,7 +829,7 @@ int odb_freshen_object(struct object_database *odb,
 	struct odb_source *source;
 	odb_prepare_alternates(odb);
 	for (source = odb->sources; source; source = source->next)
-		if (odb_source_freshen_object(source, oid))
+		if (odb_source_freshen_object(source, oid, NULL))
 			return 1;
 	return 0;
 }
@@ -989,11 +989,42 @@ int odb_write_object_ext(struct object_database *odb,
 			 const void *buf, unsigned long len,
 			 enum object_type type,
 			 struct object_id *oid,
-			 struct object_id *compat_oid,
+			 const struct object_id *compat_oid_in,
 			 enum odb_write_object_flags flags)
 {
+	const struct git_hash_algo *compat = odb->repo->compat_hash_algo;
+	struct object_id compat_oid, *compat_oid_p = NULL;
+
+	hash_object_file(odb->repo->hash_algo, buf, len, type, oid);
+
+	/*
+	 * We can skip the write in case we already have the object available.
+	 * In that case, we only freshen its mtime.
+	 */
+	if (odb_freshen_object(odb, oid))
+		return 0;
+
+	if (compat) {
+		const struct git_hash_algo *algo = odb->repo->hash_algo;
+
+		if (compat_oid_in) {
+			oidcpy(&compat_oid, compat_oid_in);
+		} else if (type == OBJ_BLOB) {
+			hash_object_file(compat, buf, len, type, &compat_oid);
+		} else {
+			struct strbuf converted = STRBUF_INIT;
+			convert_object_file(odb->repo, &converted, algo, compat,
+					    buf, len, type, 0);
+			hash_object_file(compat, converted.buf, converted.len,
+					 type, &compat_oid);
+			strbuf_release(&converted);
+		}
+
+		compat_oid_p = &compat_oid;
+	}
+
 	return odb_source_write_object(odb->sources, buf, len, type,
-				       oid, compat_oid, flags);
+				       oid, compat_oid_p, NULL, flags);
 }
 
 int odb_write_object_stream(struct object_database *odb,
@@ -1082,7 +1113,7 @@ void odb_free(struct object_database *o)
 	free(o);
 }
 
-void odb_reprepare(struct object_database *o)
+void odb_prepare(struct object_database *o, enum odb_prepare_flags flags)
 {
 	struct odb_source *source;
 
@@ -1094,13 +1125,19 @@ void odb_reprepare(struct object_database *o)
 	 * the linked list, so existing odbs will continue to exist for
 	 * the lifetime of the process.
 	 */
-	o->loaded_alternates = 0;
+	if (flags & ODB_PREPARE_FLUSH_CACHES) {
+		o->loaded_alternates = 0;
+		o->object_count_valid = 0;
+	}
+
 	odb_prepare_alternates(o);
-
 	for (source = o->sources; source; source = source->next)
-		odb_source_reprepare(source);
-
-	o->object_count_valid = 0;
+		odb_source_prepare(source, flags);
 
 	obj_read_unlock();
 }
+
+void odb_reprepare(struct object_database *o)
+{
+	odb_prepare(o, ODB_PREPARE_FLUSH_CACHES);
+}
diff --git a/odb.h b/odb.h
index 7e1c85c..fca67e8 100644
--- a/odb.h
+++ b/odb.h
@@ -8,6 +8,7 @@
 #include "thread-utils.h"
 
 struct cached_object_entry;
+struct list_objects_filter_options;
 struct odb_source_inmemory;
 struct packed_git;
 struct repository;
@@ -169,10 +170,22 @@ bool odb_optimize_required(struct object_database *odb,
  */
 void odb_close(struct object_database *o);
 
+enum odb_prepare_flags {
+	/*
+	 * Flush caches, reload alternates and then re-prepare each object
+	 * source so that new objects may become accessible.
+	 */
+	ODB_PREPARE_FLUSH_CACHES = (1 << 0),
+};
+
 /*
- * Clear caches, reload alternates and then reload object sources so that new
- * objects may become accessible.
+ * Prepare the object database for use. Calling this function is generally not
+ * needed, but can be useful in case the caller wants to pre-open individual
+ * sources.
  */
+void odb_prepare(struct object_database *o, enum odb_prepare_flags flags);
+
+/* Equivalent to `odb_prepare(o, ODB_PREPARE_FLUSH_CACHES)`. */
 void odb_reprepare(struct object_database *o);
 
 /*
@@ -293,33 +306,19 @@ int odb_pretend_object(struct object_database *odb,
 		       void *buf, size_t len, enum object_type type,
 		       struct object_id *oid);
 
-struct object_info {
-	/* Request */
-	enum object_type *typep;
-	size_t *sizep;
-	off_t *disk_sizep;
-	struct object_id *delta_base_oid;
-	void **contentp;
+/*
+ * Object database source information that can be used to uniquely identify an
+ * object and learn more about how exactly it is stored.
+ */
+struct odb_source_info {
+	/* The source that this object has been looked up from. */
+	struct odb_source *source;
 
 	/*
-	 * The time the given looked-up object has been last modified.
-	 *
-	 * Note: the mtime may be ambiguous in case the object exists multiple
-	 * times in the object database. It is thus _not_ recommended to use
-	 * this field outside of contexts where you would read every instance
-	 * of the object, like for example with `odb_for_each_object()`. As it
-	 * is impossible to say at the ODB level what the intent of the caller
-	 * is (e.g. whether to find the oldest or newest object), it is the
-	 * responsibility of the caller to disambiguate the mtimes.
+	 * Backend-specific information about the specific object. This can be
+	 * used for example to uniquely identify a given object in case it
+	 * exists multiple times.
 	 */
-	time_t *mtimep;
-
-	/* Response */
-	enum {
-		OI_CACHED,
-		OI_LOOSE,
-		OI_PACKED,
-	} whence;
 	union {
 		/*
 		 * struct {
@@ -343,6 +342,58 @@ struct object_info {
 };
 
 /*
+ * The object info contains the query and response that is to be used for
+ * functions that end up reading object information. Callers are expected to
+ * populate pointers whose information they want to request.
+ */
+struct object_info {
+	/* The object type. */
+	enum object_type *typep;
+
+	/* The inflated object size in bytes. */
+	size_t *sizep;
+
+	/* The object size as stored on disk. */
+	off_t *disk_sizep;
+
+	/*
+	 * The base the object is deltified against, in case it is stored as a
+	 * delta.
+	 */
+	struct object_id *delta_base_oid;
+
+	/* The object contents. Ownership of memory goes over to the caller. */
+	void **contentp;
+
+	/*
+	 * The time the given looked-up object has been last modified.
+	 *
+	 * Note: the mtime may be ambiguous in case the object exists multiple
+	 * times in the object database. It is thus _not_ recommended to use
+	 * this field outside of contexts where you would read every instance
+	 * of the object, like for example with `odb_for_each_object()`. As it
+	 * is impossible to say at the ODB level what the intent of the caller
+	 * is (e.g. whether to find the oldest or newest object), it is the
+	 * responsibility of the caller to disambiguate the mtimes.
+	 */
+	time_t *mtimep;
+
+	/*
+	 * Backend-specific information that tells the caller where exactly an
+	 * object was looked up from. This information should help disambiguate
+	 * object lookups in case the same object exists in multiple sources,
+	 * or multiple times in the same source.
+	 */
+	struct odb_source_info *source_infop;
+
+	/*
+	 * object-info protocol specific. Set by the protocol when the remote
+	 * does not recognize the requested object.
+	 */
+	unsigned int unrecognized:1;
+};
+
+/*
  * Initializer for a "struct object_info" that wants no items. You may
  * also memset() the memory to all-zeroes.
  */
@@ -503,6 +554,17 @@ struct odb_for_each_object_options {
 	 */
 	const struct object_id *prefix;
 	size_t prefix_hex_len;
+
+	/*
+	 * Optional object filter that allows backends to skip yielding
+	 * objects that are excluded by the filter as an optimization. The
+	 * filter is a best-effort hint: backends may use it to skip
+	 * excluded objects (e.g. by consulting a reachability bitmap), but
+	 * are also free to ignore it entirely and yield every object. As a
+	 * consequence, callers must re-apply the filter on yielded objects
+	 * if they require strict filtering semantics.
+	 */
+	const struct list_objects_filter_options *filter;
 };
 
 /*
@@ -586,9 +648,11 @@ enum odb_write_object_flags {
 
 /*
  * Write an object into the object database. The object is being written into
- * the local alternate of the repository. If provided, the converted object ID
- * as well as the compatibility object ID are written to the respective
- * pointers.
+ * the local alternate of the repository. If provided, the object ID of the
+ * final object is written into `oid`.
+ *
+ * If the caller provides a `compat_oid`, then this compatibility object hash
+ * will be stored instead of computing the compatibility hash ad-hoc.
  *
  * Returns 0 on success, a negative error code otherwise.
  */
@@ -596,7 +660,7 @@ int odb_write_object_ext(struct object_database *odb,
 			 const void *buf, unsigned long len,
 			 enum object_type type,
 			 struct object_id *oid,
-			 struct object_id *compat_oid,
+			 const struct object_id *compat_oid,
 			 enum odb_write_object_flags flags);
 
 static inline int odb_write_object(struct object_database *odb,
@@ -618,4 +682,7 @@ void parse_alternates(const char *string,
 		      const char *relative_base,
 		      struct strvec *out);
 
+/* Free pointers inside of object_info, but not object_info itself */
+void free_object_info_contents(struct object_info *object_info);
+
 #endif /* ODB_H */
diff --git a/odb/source-files.c b/odb/source-files.c
index 82cf61d..5a68af7 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -51,11 +51,12 @@ static void odb_source_files_close(struct odb_source *source)
 	odb_source_close(&files->packed->base);
 }
 
-static void odb_source_files_reprepare(struct odb_source *source)
+static void odb_source_files_prepare(struct odb_source *source,
+				     enum odb_prepare_flags flags)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
-	odb_source_reprepare(&files->loose->base);
-	odb_source_reprepare(&files->packed->base);
+	odb_source_prepare(&files->loose->base, flags);
+	odb_source_prepare(&files->packed->base, flags);
 }
 
 static int odb_source_files_read_object_info(struct odb_source *source,
@@ -159,11 +160,12 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,
 }
 
 static int odb_source_files_freshen_object(struct odb_source *source,
-					   const struct object_id *oid)
+					   const struct object_id *oid,
+					   const time_t *mtime)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
-	if (odb_source_freshen_object(&files->packed->base, oid) ||
-	    odb_source_freshen_object(&files->loose->base, oid))
+	if (odb_source_freshen_object(&files->packed->base, oid, mtime) ||
+	    odb_source_freshen_object(&files->loose->base, oid, mtime))
 		return 1;
 	return 0;
 }
@@ -171,13 +173,14 @@ static int odb_source_files_freshen_object(struct odb_source *source,
 static int odb_source_files_write_object(struct odb_source *source,
 					 const void *buf, size_t len,
 					 enum object_type type,
-					 struct object_id *oid,
-					 struct object_id *compat_oid,
+					 const struct object_id *oid,
+					 const struct object_id *compat_oid,
+					 const time_t *mtime,
 					 enum odb_write_object_flags flags)
 {
 	struct odb_source_files *files = odb_source_files_downcast(source);
 	return odb_source_write_object(&files->loose->base, buf, len, type,
-				       oid, compat_oid, flags);
+				       oid, compat_oid, mtime, flags);
 }
 
 static int odb_source_files_write_object_stream(struct odb_source *source,
@@ -190,13 +193,10 @@ static int odb_source_files_write_object_stream(struct odb_source *source,
 }
 
 static int odb_source_files_begin_transaction(struct odb_source *source,
-					      struct odb_transaction **out)
+					      struct odb_transaction **out,
+					      enum odb_transaction_flags flags)
 {
-	struct odb_transaction *tx = odb_transaction_files_begin(source);
-	if (!tx)
-		return -1;
-	*out = tx;
-	return 0;
+	return odb_transaction_files_begin(source, out, flags);
 }
 
 static int odb_source_files_read_alternates(struct odb_source *source,
@@ -227,7 +227,8 @@ static int odb_source_files_write_alternate(struct odb_source *source,
 	int found = 0;
 	int ret;
 
-	hold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR);
+	repo_hold_lock_file_for_update(source->odb->repo, &lock, path,
+				       LOCK_DIE_ON_ERROR);
 	out = fdopen_lock_file(&lock, "w");
 	if (!out) {
 		ret = error_errno(_("unable to fdopen alternates lockfile"));
@@ -741,7 +742,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,
 
 	files->base.free = odb_source_files_free;
 	files->base.close = odb_source_files_close;
-	files->base.reprepare = odb_source_files_reprepare;
+	files->base.prepare = odb_source_files_prepare;
 	files->base.read_object_info = odb_source_files_read_object_info;
 	files->base.read_object_stream = odb_source_files_read_object_stream;
 	files->base.for_each_object = odb_source_files_for_each_object;
diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c
index 7f1b6f4..3e71611 100644
--- a/odb/source-inmemory.c
+++ b/odb/source-inmemory.c
@@ -52,7 +52,8 @@ static void populate_object_info(struct odb_source_inmemory *source,
 		*oi->contentp = xmemdupz(object->buf, object->size);
 	if (oi->mtimep)
 		*oi->mtimep = 0;
-	oi->whence = OI_CACHED;
+	if (oi->source_infop)
+		oi->source_infop->source = &source->base;
 }
 
 static int odb_source_inmemory_read_object_info(struct odb_source *source,
@@ -229,15 +230,14 @@ static int odb_source_inmemory_count_objects(struct odb_source *source,
 static int odb_source_inmemory_write_object(struct odb_source *source,
 					    const void *buf, size_t len,
 					    enum object_type type,
-					    struct object_id *oid,
-					    struct object_id *compat_oid UNUSED,
+					    const struct object_id *oid,
+					    const struct object_id *compat_oid UNUSED,
+					    const time_t *mtime UNUSED,
 					    enum odb_write_object_flags flags UNUSED)
 {
 	struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);
 	struct inmemory_object *object;
 
-	hash_object_file(source->odb->repo->hash_algo, buf, len, type, oid);
-
 	if (!inmemory->objects) {
 		CALLOC_ARRAY(inmemory->objects, 1);
 		oidtree_init(inmemory->objects);
@@ -284,8 +284,10 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,
 		goto out;
 	}
 
+	hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid);
+
 	ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid,
-					       NULL, 0);
+					       NULL, NULL, 0);
 	if (ret < 0)
 		goto out;
 
@@ -295,7 +297,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,
 }
 
 static int odb_source_inmemory_freshen_object(struct odb_source *source,
-					      const struct object_id *oid)
+					      const struct object_id *oid,
+					      const time_t *mtime UNUSED)
 {
 	struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);
 	if (find_cached_object(inmemory, oid))
@@ -304,7 +307,8 @@ static int odb_source_inmemory_freshen_object(struct odb_source *source,
 }
 
 static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSED,
-						 struct odb_transaction **out UNUSED)
+						 struct odb_transaction **out UNUSED,
+						 enum odb_transaction_flags flags UNUSED)
 {
 	return error("in-memory source does not support transactions");
 }
@@ -325,7 +329,8 @@ static void odb_source_inmemory_close(struct odb_source *source UNUSED)
 {
 }
 
-static void odb_source_inmemory_reprepare(struct odb_source *source UNUSED)
+static void odb_source_inmemory_prepare(struct odb_source *source UNUSED,
+					enum odb_prepare_flags flags UNUSED)
 {
 }
 
@@ -365,7 +370,7 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb)
 
 	source->base.free = odb_source_inmemory_free;
 	source->base.close = odb_source_inmemory_close;
-	source->base.reprepare = odb_source_inmemory_reprepare;
+	source->base.prepare = odb_source_inmemory_prepare;
 	source->base.read_object_info = odb_source_inmemory_read_object_info;
 	source->base.read_object_stream = odb_source_inmemory_read_object_stream;
 	source->base.for_each_object = odb_source_inmemory_for_each_object;
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 6211348..ef0e919 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -11,8 +11,11 @@
 #include "odb/source-loose.h"
 #include "odb/streaming.h"
 #include "oidtree.h"
+#include "path.h"
 #include "repository.h"
 #include "strbuf.h"
+#include "tempfile.h"
+#include "write-or-die.h"
 
 static int append_loose_object(const struct object_id *oid,
 			       const char *path UNUSED,
@@ -196,8 +199,8 @@ static int read_object_info_from_path(struct odb_source_loose *loose,
 			oi->typep = NULL;
 		if (oi->delta_base_oid)
 			oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);
-		if (!ret)
-			oi->whence = OI_LOOSE;
+		if (oi->source_infop && !ret)
+			oi->source_infop->source = &loose->base;
 	}
 
 	return ret;
@@ -574,53 +577,270 @@ static int odb_source_loose_count_objects(struct odb_source *source,
 }
 
 static int odb_source_loose_freshen_object(struct odb_source *source,
-					   const struct object_id *oid)
+					   const struct object_id *oid,
+					   const time_t *mtime)
 {
 	struct odb_source_loose *loose = odb_source_loose_downcast(source);
 	static struct strbuf path = STRBUF_INIT;
 	odb_loose_path(loose, &path, oid);
-	return !!check_and_freshen_file(path.buf, 1);
+	return !!check_and_freshen_file(path.buf, 1, mtime);
+}
+
+/* Finalize a file on disk, and close it. */
+static void close_loose_object(struct odb_source_loose *loose,
+			       int fd, const char *filename)
+{
+	if (loose->base.will_destroy)
+		goto out;
+
+	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
+		odb_transaction_files_fsync(loose->base.odb->transaction, fd, filename);
+	else if (fsync_object_files > 0)
+		fsync_or_die(fd, filename);
+	else
+		fsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd,
+				       filename);
+
+out:
+	if (close(fd) != 0)
+		die_errno(_("error when closing loose object file"));
+}
+
+/* Size of directory component, including the ending '/' */
+static inline int directory_size(const char *filename)
+{
+	const char *s = strrchr(filename, '/');
+	if (!s)
+		return 0;
+	return s - filename + 1;
+}
+
+/*
+ * This creates a temporary file in the same directory as the final
+ * 'filename'
+ *
+ * We want to avoid cross-directory filename renames, because those
+ * can have problems on various filesystems (FAT, NFS, Coda).
+ */
+static int create_tmpfile(struct repository *repo,
+			  struct strbuf *tmp, const char *filename)
+{
+	int fd, dirlen = directory_size(filename);
+
+	strbuf_reset(tmp);
+	strbuf_add(tmp, filename, dirlen);
+	strbuf_addstr(tmp, "tmp_obj_XXXXXX");
+	fd = git_mkstemp_mode(tmp->buf, 0444);
+	if (fd < 0 && dirlen && errno == ENOENT) {
+		/*
+		 * Make sure the directory exists; note that the contents
+		 * of the buffer are undefined after mkstemp returns an
+		 * error, so we have to rewrite the whole buffer from
+		 * scratch.
+		 */
+		strbuf_reset(tmp);
+		strbuf_add(tmp, filename, dirlen - 1);
+		if (mkdir(tmp->buf, 0777) && errno != EEXIST)
+			return -1;
+		if (adjust_shared_perm(repo, tmp->buf))
+			return -1;
+
+		/* Try again */
+		strbuf_addstr(tmp, "/tmp_obj_XXXXXX");
+		fd = git_mkstemp_mode(tmp->buf, 0444);
+	}
+	return fd;
+}
+
+/**
+ * Common steps for loose object writers to start writing loose
+ * objects:
+ *
+ * - Create tmpfile for the loose object.
+ * - Setup zlib stream for compression.
+ * - Start to feed header to zlib stream.
+ *
+ * Returns a "fd", which should later be provided to
+ * end_loose_object_common().
+ */
+static int start_loose_object_common(struct odb_source_loose *loose,
+				     struct strbuf *tmp_file,
+				     const char *filename, unsigned flags,
+				     git_zstream *stream,
+				     unsigned char *buf, size_t buflen,
+				     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
+				     char *hdr, int hdrlen)
+{
+	const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+	int fd;
+	struct repo_config_values *cfg = repo_config_values(loose->base.odb->repo);
+
+	fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);
+	if (fd < 0) {
+		if (flags & ODB_WRITE_OBJECT_SILENT)
+			return -1;
+		else if (errno == EACCES)
+			return error(_("insufficient permission for adding "
+				       "an object to repository database %s"),
+				     loose->base.path);
+		else
+			return error_errno(
+				_("unable to create temporary file"));
+	}
+
+	/*  Setup zlib stream for compression */
+	git_deflate_init(stream, cfg->zlib_compression_level);
+	stream->next_out = buf;
+	stream->avail_out = buflen;
+	git_hash_init(c, algo);
+	if (compat && compat_c)
+		git_hash_init(compat_c, compat);
+
+	/*  Start to feed header to zlib stream */
+	stream->next_in = (unsigned char *)hdr;
+	stream->avail_in = hdrlen;
+	while (git_deflate(stream, 0) == Z_OK)
+		; /* nothing */
+	git_hash_update(c, hdr, hdrlen);
+	if (compat && compat_c)
+		git_hash_update(compat_c, hdr, hdrlen);
+
+	return fd;
+}
+
+/**
+ * Common steps for the inner git_deflate() loop for writing loose
+ * objects. Returns what git_deflate() returns.
+ */
+static int write_loose_object_common(struct odb_source_loose *loose,
+				     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
+				     git_zstream *stream, const int flush,
+				     unsigned char *in0, const int fd,
+				     unsigned char *compressed,
+				     const size_t compressed_len)
+{
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+	int ret;
+
+	ret = git_deflate(stream, flush ? Z_FINISH : 0);
+	git_hash_update(c, in0, stream->next_in - in0);
+	if (compat && compat_c)
+		git_hash_update(compat_c, in0, stream->next_in - in0);
+	if (write_in_full(fd, compressed, stream->next_out - compressed) < 0)
+		die_errno(_("unable to write loose object file"));
+	stream->next_out = compressed;
+	stream->avail_out = compressed_len;
+
+	return ret;
+}
+
+/**
+ * Common steps for loose object writers to end writing loose objects:
+ *
+ * - End the compression of zlib stream.
+ * - Get the calculated oid to "oid".
+ */
+static int end_loose_object_common(struct odb_source_loose *loose,
+				   struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
+				   git_zstream *stream, struct object_id *oid,
+				   struct object_id *compat_oid)
+{
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+	int ret;
+
+	ret = git_deflate_end_gently(stream);
+	if (ret != Z_OK)
+		return ret;
+	git_hash_final_oid(oid, c);
+	if (compat && compat_c)
+		git_hash_final_oid(compat_oid, compat_c);
+
+	return Z_OK;
+}
+
+static int write_loose_object(struct odb_source_loose *loose,
+			      const struct object_id *oid, char *hdr,
+			      int hdrlen, const void *buf, unsigned long len,
+			      const time_t *mtime, unsigned flags)
+{
+	int fd, ret;
+	unsigned char compressed[4096];
+	git_zstream stream;
+	struct git_hash_ctx c;
+	struct object_id parano_oid;
+	static struct strbuf tmp_file = STRBUF_INIT;
+	static struct strbuf filename = STRBUF_INIT;
+
+	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
+		odb_transaction_files_prepare(loose->base.odb->transaction);
+
+	odb_loose_path(loose, &filename, oid);
+
+	fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,
+				       &stream, compressed, sizeof(compressed),
+				       &c, NULL, hdr, hdrlen);
+	if (fd < 0)
+		return -1;
+
+	/* Then the data itself.. */
+	stream.next_in = (void *)buf;
+	stream.avail_in = len;
+	do {
+		unsigned char *in0 = stream.next_in;
+
+		ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,
+						compressed, sizeof(compressed));
+	} while (ret == Z_OK);
+
+	if (ret != Z_STREAM_END)
+		die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid),
+		    ret);
+	ret = end_loose_object_common(loose, &c, NULL, &stream, &parano_oid, NULL);
+	if (ret != Z_OK)
+		die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid),
+		    ret);
+	if (!oideq(oid, &parano_oid))
+		die(_("confused by unstable object source data for %s"),
+		    oid_to_hex(oid));
+
+	close_loose_object(loose, fd, tmp_file.buf);
+
+	if (mtime) {
+		struct utimbuf utb = {
+			.actime = *mtime,
+			.modtime = *mtime,
+		};
+
+		if (utime(tmp_file.buf, &utb) < 0 &&
+		    !(flags & ODB_WRITE_OBJECT_SILENT))
+			warning_errno(_("failed utime() on %s"), tmp_file.buf);
+	}
+
+	return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
+					  FOF_SKIP_COLLISION_CHECK);
 }
 
 static int odb_source_loose_write_object(struct odb_source *source,
 					 const void *buf, size_t len,
-					 enum object_type type, struct object_id *oid,
-					 struct object_id *compat_oid_in,
+					 enum object_type type,
+					 const struct object_id *oid,
+					 const struct object_id *compat_oid,
+					 const time_t *mtime,
 					 enum odb_write_object_flags flags)
 {
 	struct odb_source_loose *loose = odb_source_loose_downcast(source);
-	const struct git_hash_algo *algo = source->odb->repo->hash_algo;
-	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
-	struct object_id compat_oid;
 	char hdr[MAX_HEADER_LEN];
-	size_t hdrlen = sizeof(hdr);
+	int hdrlen;
 
-	/* Generate compat_oid */
-	if (compat) {
-		if (compat_oid_in)
-			oidcpy(&compat_oid, compat_oid_in);
-		else if (type == OBJ_BLOB)
-			hash_object_file(compat, buf, len, type, &compat_oid);
-		else {
-			struct strbuf converted = STRBUF_INIT;
-			convert_object_file(source->odb->repo, &converted, algo, compat,
-					    buf, len, type, 0);
-			hash_object_file(compat, converted.buf, converted.len,
-					 type, &compat_oid);
-			strbuf_release(&converted);
-		}
-	}
+	hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
 
-	/* Normally if we have it in the pack then we do not bother writing
-	 * it out into .git/objects/??/?{38} file.
-	 */
-	write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
-	if (odb_freshen_object(source->odb, oid))
-		return 0;
-	if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))
+	if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, mtime, flags))
 		return -1;
-	if (compat)
-		return repo_add_loose_object_map(loose, oid, &compat_oid);
+
+	if (compat_oid)
+		return repo_add_loose_object_map(loose, oid, compat_oid);
+
 	return 0;
 }
 
@@ -629,16 +849,125 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,
 						size_t len,
 						struct object_id *oid)
 {
-	/*
-	 * TODO: the implementation should be moved here, see the comment on
-	 * the called function in "object-file.h".
-	 */
 	struct odb_source_loose *loose = odb_source_loose_downcast(source);
-	return odb_source_loose_write_stream(loose, in_stream, len, oid);
+	const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+	struct object_id compat_oid;
+	int fd, ret, err = 0, flush = 0;
+	unsigned char compressed[4096];
+	git_zstream stream;
+	struct git_hash_ctx c, compat_c;
+	struct strbuf tmp_file = STRBUF_INIT;
+	struct strbuf filename = STRBUF_INIT;
+	unsigned char buf[8192];
+	int dirlen;
+	char hdr[MAX_HEADER_LEN];
+	int hdrlen;
+
+	if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
+		odb_transaction_files_prepare(loose->base.odb->transaction);
+
+	/* Since oid is not determined, save tmp file to odb path. */
+	strbuf_addf(&filename, "%s/", loose->base.path);
+	hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);
+
+	/*
+	 * Common steps for write_loose_object and stream_loose_object to
+	 * start writing loose objects:
+	 *
+	 *  - Create tmpfile for the loose object.
+	 *  - Setup zlib stream for compression.
+	 *  - Start to feed header to zlib stream.
+	 */
+	fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,
+				       &stream, compressed, sizeof(compressed),
+				       &c, &compat_c, hdr, hdrlen);
+	if (fd < 0) {
+		err = -1;
+		goto cleanup;
+	}
+
+	/* Then the data itself.. */
+	do {
+		unsigned char *in0 = stream.next_in;
+
+		if (!stream.avail_in && !in_stream->is_finished) {
+			ssize_t read_len = odb_write_stream_read(in_stream, buf,
+								 sizeof(buf));
+			if (read_len < 0) {
+				close(fd);
+				err = -1;
+				goto cleanup;
+			}
+
+			stream.avail_in = read_len;
+			stream.next_in = buf;
+			in0 = buf;
+			/* All data has been read. */
+			if (in_stream->is_finished)
+				flush = 1;
+		}
+		ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,
+						compressed, sizeof(compressed));
+		/*
+		 * Unlike write_loose_object(), we do not have the entire
+		 * buffer. If we get Z_BUF_ERROR due to too few input bytes,
+		 * then we'll replenish them in the next input_stream->read()
+		 * call when we loop.
+		 */
+	} while (ret == Z_OK || ret == Z_BUF_ERROR);
+
+	if (stream.total_in != len + hdrlen)
+		die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in,
+		    (uintmax_t)len + hdrlen);
+
+	/*
+	 * Common steps for write_loose_object and stream_loose_object to
+	 * end writing loose object:
+	 *
+	 *  - End the compression of zlib stream.
+	 *  - Get the calculated oid.
+	 */
+	if (ret != Z_STREAM_END)
+		die(_("unable to stream deflate new object (%d)"), ret);
+	ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);
+	if (ret != Z_OK)
+		die(_("deflateEnd on stream object failed (%d)"), ret);
+	close_loose_object(loose, fd, tmp_file.buf);
+
+	if (odb_freshen_object(loose->base.odb, oid)) {
+		unlink_or_warn(tmp_file.buf);
+		goto cleanup;
+	}
+	odb_loose_path(loose, &filename, oid);
+
+	/* We finally know the object path, and create the missing dir. */
+	dirlen = directory_size(filename.buf);
+	if (dirlen) {
+		struct strbuf dir = STRBUF_INIT;
+		strbuf_add(&dir, filename.buf, dirlen);
+
+		if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&
+		    errno != EEXIST) {
+			err = error_errno(_("unable to create directory %s"), dir.buf);
+			strbuf_release(&dir);
+			goto cleanup;
+		}
+		strbuf_release(&dir);
+	}
+
+	err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
+					 FOF_SKIP_COLLISION_CHECK);
+	if (!err && compat)
+		err = repo_add_loose_object_map(loose, oid, &compat_oid);
+cleanup:
+	strbuf_release(&tmp_file);
+	strbuf_release(&filename);
+	return err;
 }
 
 static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,
-					      struct odb_transaction **out UNUSED)
+					      struct odb_transaction **out UNUSED,
+					      enum odb_transaction_flags flags UNUSED)
 {
 	/* TODO: this is a known omission that we'll want to address eventually. */
 	return error("loose source does not support transactions");
@@ -664,10 +993,12 @@ static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
 	       sizeof(loose->subdir_seen));
 }
 
-static void odb_source_loose_reprepare(struct odb_source *source)
+static void odb_source_loose_prepare(struct odb_source *source,
+				     enum odb_prepare_flags flags)
 {
 	struct odb_source_loose *loose = odb_source_loose_downcast(source);
-	odb_source_loose_clear_cache(loose);
+	if (flags & ODB_PREPARE_FLUSH_CACHES)
+		odb_source_loose_clear_cache(loose);
 }
 
 static void odb_source_loose_close(struct odb_source *source UNUSED)
@@ -708,7 +1039,7 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb,
 
 	loose->base.free = odb_source_loose_free;
 	loose->base.close = odb_source_loose_close;
-	loose->base.reprepare = odb_source_loose_reprepare;
+	loose->base.prepare = odb_source_loose_prepare;
 	loose->base.read_object_info = odb_source_loose_read_object_info;
 	loose->base.read_object_stream = odb_source_loose_read_object_stream;
 	loose->base.for_each_object = odb_source_loose_for_each_object;
diff --git a/odb/source-packed.c b/odb/source-packed.c
index 06b31dd..0890704 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -3,11 +3,13 @@
 #include "chdir-notify.h"
 #include "dir.h"
 #include "git-zlib.h"
+#include "list-objects-filter-options.h"
 #include "mergesort.h"
 #include "midx.h"
 #include "odb/source-packed.h"
 #include "odb/streaming.h"
 #include "packfile.h"
+#include "pack-bitmap.h"
 
 static int find_pack_entry(struct odb_source_packed *store,
 			   const struct object_id *oid,
@@ -15,7 +17,7 @@ static int find_pack_entry(struct odb_source_packed *store,
 {
 	struct packfile_list_entry *l;
 
-	odb_source_packed_prepare(store);
+	odb_source_prepare(&store->base, 0);
 	if (store->midx && fill_midx_entry(store->midx, oid, e))
 		return 1;
 
@@ -47,7 +49,7 @@ static int odb_source_packed_read_object_info(struct odb_source *source,
 	 * been added since the last time we have prepared the packfile store.
 	 */
 	if (flags & OBJECT_INFO_SECOND_READ)
-		odb_source_reprepare(source);
+		odb_source_prepare(source, ODB_PREPARE_FLUSH_CACHES);
 
 	if (!find_pack_entry(packed, oid, &e))
 		return 1;
@@ -59,7 +61,7 @@ static int odb_source_packed_read_object_info(struct odb_source *source,
 	if (!oi)
 		return 0;
 
-	ret = packed_object_info(e.p, e.offset, oi);
+	ret = packed_object_info(packed, e.p, e.offset, oi);
 	if (ret < 0) {
 		mark_bad_packed_object(e.p, oid);
 		return -1;
@@ -99,7 +101,7 @@ static int odb_source_packed_for_each_object_wrapper(const struct object_id *oid
 		off_t offset = nth_packed_object_offset(pack, index_pos);
 		struct object_info oi = *data->request;
 
-		if (packed_object_info_with_index_pos(pack, offset,
+		if (packed_object_info_with_index_pos(data->store, pack, offset,
 						      &index_pos, &oi) < 0) {
 			mark_bad_packed_object(pack, oid);
 			return -1;
@@ -143,7 +145,7 @@ static bool should_exclude_pack(struct packed_git *p, enum odb_for_each_object_f
 }
 
 static int for_each_prefixed_object_in_midx(
-	struct odb_source_packed *store,
+	struct odb_source_packed *source,
 	struct multi_pack_index *m,
 	const struct odb_for_each_object_options *opts,
 	struct odb_source_packed_for_each_object_wrapper_data *data)
@@ -170,6 +172,7 @@ static int for_each_prefixed_object_in_midx(
 		 */
 		for (i = first; i < num; i++) {
 			const struct object_id *current = NULL;
+			struct packed_git *pack;
 			struct object_id oid;
 
 			current = nth_midxed_object_oid(&oid, m, i);
@@ -177,9 +180,8 @@ static int for_each_prefixed_object_in_midx(
 			if (!match_hash(len, opts->prefix->hash, current->hash))
 				break;
 
-			if (opts->flags) {
+			if (opts->flags || data->request) {
 				uint32_t pack_id = nth_midxed_pack_int_id(m, i);
-				struct packed_git *pack;
 
 				if (prepare_midx_pack(m, pack_id)) {
 					pack_errors = true;
@@ -193,9 +195,9 @@ static int for_each_prefixed_object_in_midx(
 
 			if (data->request) {
 				struct object_info oi = *data->request;
+				off_t offset = nth_midxed_offset(m, i);
 
-				ret = odb_source_read_object_info(&store->base, current,
-								  &oi, 0);
+				ret = packed_object_info(source, pack, offset, &oi);
 				if (ret)
 					goto out;
 
@@ -219,7 +221,7 @@ static int for_each_prefixed_object_in_midx(
 }
 
 static int for_each_prefixed_object_in_pack(
-	struct odb_source_packed *store,
+	struct odb_source_packed *source,
 	struct packed_git *p,
 	const struct odb_for_each_object_options *opts,
 	struct odb_source_packed_for_each_object_wrapper_data *data)
@@ -246,8 +248,9 @@ static int for_each_prefixed_object_in_pack(
 
 		if (data->request) {
 			struct object_info oi = *data->request;
+			off_t offset = nth_packed_object_offset(p, i);
 
-			ret = odb_source_read_object_info(&store->base, &oid, &oi, 0);
+			ret = packed_object_info(source, p, offset, &oi);
 			if (ret)
 				goto out;
 
@@ -314,6 +317,37 @@ static int odb_source_packed_for_each_prefixed_object(
 	return ret;
 }
 
+struct bitmapped_for_each_object_data {
+	struct odb_source_packed *packed;
+	const struct object_info *request;
+	const struct odb_for_each_object_options *opts;
+	odb_for_each_object_cb cb;
+	void *cb_data;
+};
+
+static int bitmapped_for_each_object(const struct object_id *oid,
+				     enum object_type type UNUSED,
+				     int flags UNUSED,
+				     uint32_t hash UNUSED,
+				     struct packed_git *pack,
+				     off_t offset,
+				     void *cb_data)
+{
+	struct bitmapped_for_each_object_data *data = cb_data;
+
+	if (should_exclude_pack(pack, data->opts->flags))
+		return 0;
+
+	if (data->request) {
+		struct object_info oi = *data->request;
+		if (packed_object_info(data->packed, pack, offset, &oi) < 0)
+			return -1;
+		return data->cb(oid, &oi, data->cb_data);
+	}
+
+	return data->cb(oid, NULL, data->cb_data);
+}
+
 static int odb_source_packed_for_each_object(struct odb_source *source,
 					     const struct object_info *request,
 					     odb_for_each_object_cb cb,
@@ -327,12 +361,33 @@ static int odb_source_packed_for_each_object(struct odb_source *source,
 		.cb = cb,
 		.cb_data = cb_data,
 	};
+	struct bitmap_index *bitmap = NULL;
 	struct packfile_list_entry *e;
 	int pack_errors = 0, ret;
 
 	if (opts->prefix)
 		return odb_source_packed_for_each_prefixed_object(packed, opts, &data);
 
+	if (opts->filter &&
+	    opts->filter->choice != LOFC_DISABLED &&
+	    can_filter_bitmap(opts->filter))
+		bitmap = prepare_bitmap_git_for_source(packed);
+	if (bitmap) {
+		struct bitmapped_for_each_object_data bitmap_data = {
+			.packed = packed,
+			.request = request,
+			.opts = opts,
+			.cb = cb,
+			.cb_data = cb_data,
+		};
+
+		ret = for_each_bitmapped_object(bitmap, opts->filter,
+						bitmapped_for_each_object,
+						&bitmap_data);
+		if (ret)
+			goto out;
+	}
+
 	packed->skip_mru_updates = true;
 
 	for (e = packfile_store_get_packs(packed); e; e = e->next) {
@@ -341,6 +396,13 @@ static int odb_source_packed_for_each_object(struct odb_source *source,
 		if (should_exclude_pack(p, opts->flags))
 			continue;
 
+		/*
+		 * Objects covered by the bitmap have already been yielded
+		 * above; skip them here to avoid duplicates.
+		 */
+		if (bitmap && bitmap_index_contains_pack(bitmap, p))
+			continue;
+
 		if (open_pack_index(p)) {
 			pack_errors = 1;
 			continue;
@@ -356,6 +418,7 @@ static int odb_source_packed_for_each_object(struct odb_source *source,
 
 out:
 	packed->skip_mru_updates = false;
+	free_bitmap_index(bitmap);
 
 	if (!ret && pack_errors)
 		ret = -1;
@@ -507,18 +570,26 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source,
 }
 
 static int odb_source_packed_freshen_object(struct odb_source *source,
-					    const struct object_id *oid)
+					    const struct object_id *oid,
+					    const time_t *mtime)
 {
 	struct odb_source_packed *packed = odb_source_packed_downcast(source);
+	struct utimbuf times, *timesp = NULL;
 	struct pack_entry e;
 
+	if (mtime) {
+		times.actime = *mtime;
+		times.modtime = *mtime;
+		timesp = &times;
+	}
+
 	if (!find_pack_entry(packed, oid, &e))
 		return 0;
 	if (e.p->is_cruft)
 		return 0;
 	if (e.p->freshened)
 		return 1;
-	if (utime(e.p->pack_name, NULL))
+	if (utime(e.p->pack_name, timesp))
 		return 0;
 	e.p->freshened = 1;
 
@@ -529,8 +600,9 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,
 					  const void *buf UNUSED,
 					  size_t len UNUSED,
 					  enum object_type type UNUSED,
-					  struct object_id *oid UNUSED,
-					  struct object_id *compat_oid UNUSED,
+					  const struct object_id *oid UNUSED,
+					  const struct object_id *compat_oid UNUSED,
+					  const time_t *mtime UNUSED,
 					  unsigned flags UNUSED)
 {
 	return error("packed backend cannot write objects");
@@ -545,7 +617,8 @@ static int odb_source_packed_write_object_stream(struct odb_source *source UNUSE
 }
 
 static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED,
-					       struct odb_transaction **out UNUSED)
+					       struct odb_transaction **out UNUSED,
+					       enum odb_transaction_flags flags UNUSED)
 {
 	return error("packed backend cannot begin transactions");
 }
@@ -692,27 +765,25 @@ static int sort_pack(const struct packfile_list_entry *a,
 	return -1;
 }
 
-void odb_source_packed_prepare(struct odb_source_packed *source)
-{
-	if (source->initialized)
-		return;
-
-	prepare_multi_pack_index_one(source);
-	prepare_packed_git_one(source);
-
-	sort_packs(&source->packs.head, sort_pack);
-	for (struct packfile_list_entry *e = source->packs.head; e; e = e->next)
-		if (!e->next)
-			source->packs.tail = e;
-
-	source->initialized = true;
-}
-
-static void odb_source_packed_reprepare(struct odb_source *source)
+static void odb_source_packed_prepare(struct odb_source *source,
+				      enum odb_prepare_flags flags)
 {
 	struct odb_source_packed *packed = odb_source_packed_downcast(source);
-	packed->initialized = false;
-	odb_source_packed_prepare(packed);
+
+	if (flags & ODB_PREPARE_FLUSH_CACHES)
+		packed->initialized = false;
+	if (packed->initialized)
+		return;
+
+	prepare_multi_pack_index_one(packed);
+	prepare_packed_git_one(packed);
+
+	sort_packs(&packed->packs.head, sort_pack);
+	for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next)
+		if (!e->next)
+			packed->packs.tail = e;
+
+	packed->initialized = true;
 }
 
 static void odb_source_packed_reparent(const char *name UNUSED,
@@ -768,7 +839,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb,
 
 	packed->base.free = odb_source_packed_free;
 	packed->base.close = odb_source_packed_close;
-	packed->base.reprepare = odb_source_packed_reprepare;
+	packed->base.prepare = odb_source_packed_prepare;
 	packed->base.read_object_info = odb_source_packed_read_object_info;
 	packed->base.read_object_stream = odb_source_packed_read_object_stream;
 	packed->base.for_each_object = odb_source_packed_for_each_object;
diff --git a/odb/source-packed.h b/odb/source-packed.h
index f0724b2..77309dd 100644
--- a/odb/source-packed.h
+++ b/odb/source-packed.h
@@ -82,13 +82,4 @@ static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_so
 	return container_of(source, struct odb_source_packed, base);
 }
 
-/*
- * Prepare the source by loading packfiles and multi-pack indices for
- * all alternates. This becomes a no-op if the source is already prepared.
- *
- * It shouldn't typically be necessary to call this function directly, as
- * functions that access the source know to prepare it.
- */
-void odb_source_packed_prepare(struct odb_source_packed *source);
-
 #endif
diff --git a/odb/source.h b/odb/source.h
index 88a48ba..d69f8e2 100644
--- a/odb/source.h
+++ b/odb/source.h
@@ -3,6 +3,7 @@
 
 #include "object.h"
 #include "odb.h"
+#include "odb/transaction.h"
 
 enum odb_source_type {
 	/*
@@ -83,11 +84,12 @@ struct odb_source {
 	void (*close)(struct odb_source *source);
 
 	/*
-	 * This callback is expected to clear underlying caches of the object
-	 * database source. The function is called when the repository has for
-	 * example just been repacked so that new objects will become visible.
+	 * This callback is expected to prepare the source so that it becomes
+	 * ready for use. It optionally clears underlying caches of the object
+	 * database source.
 	 */
-	void (*reprepare)(struct odb_source *source);
+	void (*prepare)(struct odb_source *source,
+			enum odb_prepare_flags flags);
 
 	/*
 	 * This callback is expected to read object information from the object
@@ -188,7 +190,8 @@ struct odb_source {
 	 * has been freshened.
 	 */
 	int (*freshen_object)(struct odb_source *source,
-			      const struct object_id *oid);
+			      const struct object_id *oid,
+			      const time_t *mtime);
 
 	/*
 	 * This callback is expected to persist the given object into the
@@ -204,8 +207,9 @@ struct odb_source {
 	int (*write_object)(struct odb_source *source,
 			    const void *buf, size_t len,
 			    enum object_type type,
-			    struct object_id *oid,
-			    struct object_id *compat_oid,
+			    const struct object_id *oid,
+			    const struct object_id *compat_oid,
+			    const time_t *mtime,
 			    enum odb_write_object_flags flags);
 
 	/*
@@ -231,7 +235,8 @@ struct odb_source {
 	 * negative error code otherwise.
 	 */
 	int (*begin_transaction)(struct odb_source *source,
-				 struct odb_transaction **out);
+				 struct odb_transaction **out,
+				 enum odb_transaction_flags flags);
 
 	/*
 	 * This callback is expected to read the list of alternate object
@@ -323,13 +328,14 @@ static inline void odb_source_close(struct odb_source *source)
 }
 
 /*
- * Reprepare the object database source and clear any caches. Depending on the
+ * Prepare the object database source and clear any caches. Depending on the
  * backend used this may have the effect that concurrently-written objects
  * become visible.
  */
-static inline void odb_source_reprepare(struct odb_source *source)
+static inline void odb_source_prepare(struct odb_source *source,
+				      enum odb_prepare_flags flags)
 {
-	source->reprepare(source);
+	source->prepare(source, flags);
 }
 
 /*
@@ -414,9 +420,10 @@ static inline int odb_source_find_abbrev_len(struct odb_source *source,
  * not exist.
  */
 static inline int odb_source_freshen_object(struct odb_source *source,
-					    const struct object_id *oid)
+					    const struct object_id *oid,
+					    const time_t *mtime)
 {
-	return source->freshen_object(source, oid);
+	return source->freshen_object(source, oid, mtime);
 }
 
 /*
@@ -427,12 +434,13 @@ static inline int odb_source_freshen_object(struct odb_source *source,
 static inline int odb_source_write_object(struct odb_source *source,
 					  const void *buf, unsigned long len,
 					  enum object_type type,
-					  struct object_id *oid,
-					  struct object_id *compat_oid,
+					  const struct object_id *oid,
+					  const struct object_id *compat_oid,
+					  const time_t *mtime,
 					  enum odb_write_object_flags flags)
 {
 	return source->write_object(source, buf, len, type, oid,
-				    compat_oid, flags);
+				    compat_oid, mtime, flags);
 }
 
 /*
@@ -485,9 +493,10 @@ static inline int odb_source_write_alternate(struct odb_source *source,
  * Returns 0 on success, a negative error code otherwise.
  */
 static inline int odb_source_begin_transaction(struct odb_source *source,
-					       struct odb_transaction **out)
+					       struct odb_transaction **out,
+					       enum odb_transaction_flags flags)
 {
-	return source->begin_transaction(source, out);
+	return source->begin_transaction(source, out, flags);
 }
 
 /*
diff --git a/odb/transaction.c b/odb/transaction.c
index b16e07a..dab7da6 100644
--- a/odb/transaction.c
+++ b/odb/transaction.c
@@ -1,30 +1,41 @@
 #include "git-compat-util.h"
+#include "gettext.h"
 #include "odb/source.h"
 #include "odb/transaction.h"
 
-struct odb_transaction *odb_transaction_begin(struct object_database *odb)
+int odb_transaction_begin(struct object_database *odb,
+			  struct odb_transaction **out,
+			  enum odb_transaction_flags flags)
 {
+	int ret;
+
 	if (odb->transaction)
-		return NULL;
+		return error(_("object database transaction already pending"));
 
-	odb_source_begin_transaction(odb->sources, &odb->transaction);
+	ret = odb_source_begin_transaction(odb->sources, out, flags);
+	if (!ret)
+		odb->transaction = *out;
 
-	return odb->transaction;
+	return ret;
 }
 
-void odb_transaction_commit(struct odb_transaction *transaction)
+int odb_transaction_commit(struct odb_transaction *transaction)
 {
+	int ret;
+
 	if (!transaction)
-		return;
+		return 0;
 
 	/*
 	 * Ensure the transaction ending matches the pending transaction.
 	 */
 	ASSERT(transaction == transaction->source->odb->transaction);
 
-	transaction->commit(transaction);
+	ret = transaction->commit(transaction);
 	transaction->source->odb->transaction = NULL;
 	free(transaction);
+
+	return ret;
 }
 
 int odb_transaction_write_object_stream(struct odb_transaction *transaction,
@@ -33,3 +44,11 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,
 {
 	return transaction->write_object_stream(transaction, stream, len, oid);
 }
+
+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)
+{
+	if (!transaction)
+		return 0;
+
+	return transaction->env(transaction, env);
+}
diff --git a/odb/transaction.h b/odb/transaction.h
index 854fda0..4cb2eaf 100644
--- a/odb/transaction.h
+++ b/odb/transaction.h
@@ -1,8 +1,8 @@
 #ifndef ODB_TRANSACTION_H
 #define ODB_TRANSACTION_H
 
+#include "gettext.h"
 #include "odb.h"
-#include "odb/source.h"
 
 /*
  * A transaction may be started for an object database prior to writing new
@@ -16,8 +16,11 @@ struct odb_transaction {
 	/* The ODB source the transaction is opened against. */
 	struct odb_source *source;
 
-	/* The ODB source specific callback invoked to commit a transaction. */
-	void (*commit)(struct odb_transaction *transaction);
+	/*
+	 * The ODB source specific callback invoked to commit a transaction.
+	 * Returns 0 on success, a negative error code otherwise.
+	 */
+	int (*commit)(struct odb_transaction *transaction);
 
 	/*
 	 * This callback is expected to write the given object stream into
@@ -30,20 +33,47 @@ struct odb_transaction {
 	int (*write_object_stream)(struct odb_transaction *transaction,
 				   struct odb_write_stream *stream, size_t len,
 				   struct object_id *oid);
+
+	/*
+	 * This callback is expected to populate the provided strvec with the
+	 * environment variables that a child process should inherit so that its
+	 * object writes participate in the transaction. Returns 0 on success, a
+	 * negative error code otherwise.
+	 */
+	int (*env)(struct odb_transaction *transaction, struct strvec *env);
+};
+
+/* Flags used to configure an ODB transaction. */
+enum odb_transaction_flags {
+	/* Configures the transaction for use with git-receive-pack(1). */
+	ODB_TRANSACTION_RECEIVE = (1 << 0),
 };
 
 /*
- * Starts an ODB transaction. Subsequent objects are written to the transaction
- * and not committed until odb_transaction_commit() is invoked on the
- * transaction. If the ODB already has a pending transaction, NULL is returned.
+ * Starts an ODB transaction and returns it via `out`. Subsequent objects are
+ * written to the transaction and not committed until odb_transaction_commit()
+ * is invoked on the transaction. Returns 0 on success and a negative value on
+ * error. Note that it is considered an error to start a new transaction if the
+ * ODB already has an inflight transaction pending.
  */
-struct odb_transaction *odb_transaction_begin(struct object_database *odb);
+int odb_transaction_begin(struct object_database *odb,
+			  struct odb_transaction **out,
+			  enum odb_transaction_flags flags);
+
+static inline void odb_transaction_begin_or_die(struct object_database *odb,
+						struct odb_transaction **out,
+						enum odb_transaction_flags flags)
+{
+	if (odb_transaction_begin(odb, out, flags))
+		die(_("failed to start ODB transaction"));
+}
 
 /*
- * Commits an ODB transaction making the written objects visible. If the
- * specified transaction is NULL, the function is a no-op.
+ * Commits an ODB transaction making the written objects visible. Returns 0 on
+ * success, a negative error code otherwise. Note that, if the specified
+ * transaction is NULL, the function is a no-op and no error is returned.
  */
-void odb_transaction_commit(struct odb_transaction *transaction);
+int odb_transaction_commit(struct odb_transaction *transaction);
 
 /*
  * Writes the object in the provided stream into the transaction. The resulting
@@ -54,4 +84,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,
 					struct odb_write_stream *stream,
 					size_t len, struct object_id *oid);
 
+/*
+ * Populates the provided strvec with the environment variables that a child
+ * process should inherit so that its object writes participate in the
+ * transaction, suitable for using via child_process.env. Returns 0 on success,
+ * a negative error code otherwise. Note that, if the specified transaction is
+ * NULL, the function is a no-op and no error is returned.
+ */
+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env);
+
 #endif
diff --git a/oss-fuzz/.gitignore b/oss-fuzz/.gitignore
index f2d74de..dc7a127 100644
--- a/oss-fuzz/.gitignore
+++ b/oss-fuzz/.gitignore
@@ -5,4 +5,5 @@
 fuzz-pack-headers
 fuzz-pack-idx
 fuzz-parse-attr-line
+fuzz-reftable
 fuzz-url-decode-mem
diff --git a/oss-fuzz/fuzz-reftable.c b/oss-fuzz/fuzz-reftable.c
new file mode 100644
index 0000000..c46eac2
--- /dev/null
+++ b/oss-fuzz/fuzz-reftable.c
@@ -0,0 +1,74 @@
+#include "git-compat-util.h"
+#include "reftable/basics.h"
+#include "reftable/blocksource.h"
+#include "reftable/reftable-blocksource.h"
+#include "reftable/reftable-error.h"
+#include "reftable/reftable-iterator.h"
+#include "reftable/reftable-record.h"
+#include "reftable/reftable-table.h"
+#include "reftable/reftable-writer.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+	struct reftable_block_source source = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_table *table = NULL;
+	int err;
+
+	if (reftable_buf_add(&buf, (const char *)data, size) < 0)
+		goto out;
+	block_source_from_buf(&source, &buf);
+
+	err = reftable_table_new(&table, &source, "fuzz-input");
+	if (err < 0)
+		goto out;
+
+	/*
+	 * Exercise the ref, log and raw block iterators so that we cover as
+	 * much of the parsing code as possible.
+	 */
+	{
+		struct reftable_ref_record ref = { 0 };
+		struct reftable_iterator it = { 0 };
+
+		reftable_table_init_ref_iterator(table, &it);
+		if (!reftable_iterator_seek_ref(&it, ""))
+			while (!reftable_iterator_next_ref(&it, &ref))
+				;
+
+		reftable_ref_record_release(&ref);
+		reftable_iterator_destroy(&it);
+	}
+
+	{
+		struct reftable_log_record log = { 0 };
+		struct reftable_iterator it = { 0 };
+
+		reftable_table_init_log_iterator(table, &it);
+		if (!reftable_iterator_seek_log(&it, ""))
+			while (!reftable_iterator_next_log(&it, &log))
+				;
+
+		reftable_log_record_release(&log);
+		reftable_iterator_destroy(&it);
+	}
+
+	{
+		struct reftable_table_iterator it = { 0 };
+		const struct reftable_block *block;
+
+		if (!reftable_table_iterator_init(&it, table))
+			while (!reftable_table_iterator_next(&it, &block))
+				;
+
+		reftable_table_iterator_release(&it);
+	}
+
+out:
+	if (table)
+		reftable_table_decref(table);
+	reftable_buf_release(&buf);
+	return 0;
+}
diff --git a/oss-fuzz/meson.build b/oss-fuzz/meson.build
index 878afd8..5a38542 100644
--- a/oss-fuzz/meson.build
+++ b/oss-fuzz/meson.build
@@ -6,6 +6,7 @@
   'fuzz-pack-headers.c',
   'fuzz-pack-idx.c',
   'fuzz-parse-attr-line.c',
+  'fuzz-reftable.c',
   'fuzz-url-decode-mem.c',
 ]
 
@@ -16,5 +17,6 @@
       fuzz_program,
     ],
     dependencies: [libgit_commonmain],
+    link_args: get_option('fuzzers_link_args'),
   )
 endforeach
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 1bcb3f9..acbea89 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -636,6 +636,8 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 			      struct bitmap_index *old_bitmap,
 			      const uint32_t *mapping)
 {
+	struct commit *c;
+	struct tree *t;
 	int found;
 	int from_pseudo_merge = commit->object.flags & BITMAP_PSEUDO_MERGE;
 	uint32_t pos;
@@ -650,9 +652,8 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 
 	prio_queue_put(queue, commit);
 
-	while (queue->nr) {
+	while ((c = prio_queue_get(queue))) {
 		struct commit_list *p;
-		struct commit *c = prio_queue_get(queue);
 
 		if (old_bitmap && mapping) {
 			struct ewah_bitmap *old;
@@ -740,8 +741,7 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
 		}
 	}
 
-	while (tree_queue->nr) {
-		struct tree *t = prio_queue_get(tree_queue);
+	while ((t = prio_queue_get(tree_queue))) {
 		int found;
 
 		pos = find_object_pos(writer, &t->object.oid, &found);
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 83eb47a..d8dc4ae 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -460,8 +460,8 @@ char *pack_bitmap_filename(struct packed_git *p)
 	return xstrfmt("%.*s.bitmap", (int)len, p->pack_name);
 }
 
-static int open_midx_bitmap_1(struct bitmap_index *bitmap_git,
-			      struct multi_pack_index *midx)
+static int open_midx_bitmap(struct bitmap_index *bitmap_git,
+			    struct multi_pack_index *midx)
 {
 	struct stat st;
 	char *bitmap_name = midx_bitmap_filename(midx);
@@ -523,6 +523,10 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git,
 
 	if (midx->base_midx) {
 		bitmap_git->base = prepare_midx_bitmap_git(midx->base_midx);
+		if (!bitmap_git->base) {
+			warning(_("could not open bitmap for base MIDX"));
+			goto cleanup;
+		}
 		bitmap_git->base_nr = bitmap_git->base->base_nr + 1;
 	} else {
 		bitmap_git->base_nr = 0;
@@ -539,7 +543,7 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git,
 	return -1;
 }
 
-static int open_pack_bitmap_1(struct bitmap_index *bitmap_git, struct packed_git *packfile)
+static int open_pack_bitmap(struct bitmap_index *bitmap_git, struct packed_git *packfile)
 {
 	int fd;
 	struct stat st;
@@ -603,7 +607,7 @@ static int load_reverse_index(struct repository *r, struct bitmap_index *bitmap_
 
 		/*
 		 * The multi-pack-index's .rev file is already loaded via
-		 * open_pack_bitmap_1().
+		 * open_pack_bitmap().
 		 *
 		 * But we still need to open the individual pack .rev files,
 		 * since we will need to make use of them in pack-objects.
@@ -680,60 +684,53 @@ static int load_bitmap(struct repository *r, struct bitmap_index *bitmap_git,
 	return 0;
 }
 
-static int open_pack_bitmap(struct repository *r,
-			    struct bitmap_index *bitmap_git)
+static int open_bitmap_for_source(struct odb_source_packed *source,
+				  struct bitmap_index *bitmap_git)
 {
-	struct packed_git *p;
-	int ret = -1;
+	struct multi_pack_index *midx = get_multi_pack_index(source);
+	struct packfile_list_entry *e;
+	bool found = false;
 
-	repo_for_each_pack(r, p) {
-		if (open_pack_bitmap_1(bitmap_git, p) == 0) {
-			ret = 0;
-			/*
-			 * The only reason to keep looking is to report
-			 * duplicates.
-			 */
-			if (!trace2_is_enabled())
-				break;
-		}
+	if (midx && !open_midx_bitmap(bitmap_git, midx))
+		found = true;
+
+	for (e = packfile_store_get_packs(source); e; e = e->next) {
+		/*
+		 * When tracing is enabled we want to keep looking to report
+		 * duplicates even if we have already found a bitmap.
+		 */
+		if (found && !trace2_is_enabled())
+			break;
+
+		if (!open_pack_bitmap(bitmap_git, e->pack))
+			found = true;
 	}
 
-	return ret;
+	return found ? 0 : -1;
 }
 
-static int open_midx_bitmap(struct repository *r,
-			    struct bitmap_index *bitmap_git)
+static int open_bitmap(struct repository *r,
+		       struct bitmap_index *bitmap_git)
 {
 	struct odb_source *source;
-	int ret = -1;
+	bool found = false;
 
 	assert(!bitmap_git->map);
 
 	odb_prepare_alternates(r->objects);
 	for (source = r->objects->sources; source; source = source->next) {
 		struct odb_source_files *files = odb_source_files_downcast(source);
-		struct multi_pack_index *midx = get_multi_pack_index(files->packed);
-		if (midx && !open_midx_bitmap_1(bitmap_git, midx))
-			ret = 0;
+
+		if (!open_bitmap_for_source(files->packed, bitmap_git))
+			found = true;
+
+		/*
+		 * The only reason to keep looking after having found a bitmap
+		 * is to report duplicates.
+		 */
+		if (found && !trace2_is_enabled())
+			break;
 	}
-	return ret;
-}
-
-static int open_bitmap(struct repository *r,
-		       struct bitmap_index *bitmap_git)
-{
-	int found;
-
-	assert(!bitmap_git->map);
-
-	found = !open_midx_bitmap(r, bitmap_git);
-
-	/*
-	 * these will all be skipped if we opened a midx bitmap; but run it
-	 * anyway if tracing is enabled to report the duplicates
-	 */
-	if (!found || trace2_is_enabled())
-		found |= !open_pack_bitmap(r, bitmap_git);
 
 	return found ? 0 : -1;
 }
@@ -753,7 +750,19 @@ struct bitmap_index *prepare_midx_bitmap_git(struct multi_pack_index *midx)
 {
 	struct bitmap_index *bitmap_git = xcalloc(1, sizeof(*bitmap_git));
 
-	if (!open_midx_bitmap_1(bitmap_git, midx))
+	if (!open_midx_bitmap(bitmap_git, midx))
+		return bitmap_git;
+
+	free_bitmap_index(bitmap_git);
+	return NULL;
+}
+
+struct bitmap_index *prepare_bitmap_git_for_source(struct odb_source_packed *source)
+{
+	struct bitmap_index *bitmap_git = xcalloc(1, sizeof(*bitmap_git));
+
+	if (!open_bitmap_for_source(source, bitmap_git) &&
+	    !load_bitmap(source->base.odb->repo, bitmap_git, 0))
 		return bitmap_git;
 
 	free_bitmap_index(bitmap_git);
@@ -1695,7 +1704,7 @@ static void init_type_iterator(struct ewah_or_iterator *it,
 	}
 }
 
-static void show_objects_for_type(
+static int show_objects_for_type(
 	struct bitmap_index *bitmap_git,
 	struct bitmap *objects,
 	enum object_type object_type,
@@ -1704,6 +1713,7 @@ static void show_objects_for_type(
 {
 	size_t i = 0;
 	uint32_t offset;
+	int ret;
 
 	struct ewah_or_iterator it;
 	eword_t filter;
@@ -1749,11 +1759,17 @@ static void show_objects_for_type(
 
 			hash = bitmap_name_hash(bitmap_git, index_pos);
 
-			show_reach(&oid, object_type, 0, hash, pack, ofs, payload);
+			ret = show_reach(&oid, object_type, 0, hash, pack, ofs, payload);
+			if (ret)
+				goto out;
 		}
 	}
 
+	ret = 0;
+
+out:
 	ewah_or_iterator_release(&it);
+	return ret;
 }
 
 static int in_bitmapped_pack(struct bitmap_index *bitmap_git,
@@ -1877,7 +1893,7 @@ static unsigned long get_size_by_pos(struct bitmap_index *bitmap_git,
 			ofs = pack_pos_to_offset(pack, pos);
 		}
 
-		if (packed_object_info(pack, ofs, &oi) < 0) {
+		if (packed_object_info(NULL, pack, ofs, &oi) < 0) {
 			struct object_id oid;
 			nth_bitmap_object_oid(bitmap_git, &oid,
 					      pack_pos_to_index(pack, pos));
@@ -1976,7 +1992,7 @@ static void filter_bitmap_object_type(struct bitmap_index *bitmap_git,
 static int filter_bitmap(struct bitmap_index *bitmap_git,
 			 struct object_list *tip_objects,
 			 struct bitmap *to_filter,
-			 struct list_objects_filter_options *filter)
+			 const struct list_objects_filter_options *filter)
 {
 	if (!filter || filter->choice == LOFC_DISABLED)
 		return 0;
@@ -2027,12 +2043,11 @@ static int filter_bitmap(struct bitmap_index *bitmap_git,
 	return -1;
 }
 
-static int can_filter_bitmap(struct list_objects_filter_options *filter)
+bool can_filter_bitmap(const struct list_objects_filter_options *filter)
 {
 	return !filter_bitmap(NULL, NULL, NULL, filter);
 }
 
-
 static void filter_packed_objects_from_bitmap(struct bitmap_index *bitmap_git,
 					      struct bitmap *result)
 {
@@ -2058,10 +2073,16 @@ static void filter_packed_objects_from_bitmap(struct bitmap_index *bitmap_git,
 }
 
 int for_each_bitmapped_object(struct bitmap_index *bitmap_git,
-			      struct list_objects_filter_options *filter,
+			      const struct list_objects_filter_options *filter,
 			      show_reachable_fn show_reach,
 			      void *payload)
 {
+	const enum object_type types[] = {
+		OBJ_COMMIT,
+		OBJ_TREE,
+		OBJ_BLOB,
+		OBJ_TAG,
+	};
 	struct bitmap *filtered_bitmap = NULL;
 	uint32_t objects_nr;
 	size_t full_word_count;
@@ -2086,14 +2107,12 @@ int for_each_bitmapped_object(struct bitmap_index *bitmap_git,
 		goto out;
 	}
 
-	show_objects_for_type(bitmap_git, filtered_bitmap,
-			      OBJ_COMMIT, show_reach, payload);
-	show_objects_for_type(bitmap_git, filtered_bitmap,
-			      OBJ_TREE, show_reach, payload);
-	show_objects_for_type(bitmap_git, filtered_bitmap,
-			      OBJ_BLOB, show_reach, payload);
-	show_objects_for_type(bitmap_git, filtered_bitmap,
-			      OBJ_TAG, show_reach, payload);
+	for (size_t i = 0; i < ARRAY_SIZE(types); i++) {
+		ret = show_objects_for_type(bitmap_git, filtered_bitmap,
+					    types[i], show_reach, payload);
+		if (ret)
+			goto out;
+	}
 
 	ret = 0;
 out:
diff --git a/pack-bitmap.h b/pack-bitmap.h
index 19a8655..1385027 100644
--- a/pack-bitmap.h
+++ b/pack-bitmap.h
@@ -9,6 +9,7 @@
 #include "string-list.h"
 
 struct commit;
+struct odb_source_packed;
 struct repository;
 struct rev_info;
 
@@ -68,6 +69,7 @@ struct bitmapped_pack {
 
 struct bitmap_index *prepare_bitmap_git(struct repository *r);
 struct bitmap_index *prepare_midx_bitmap_git(struct multi_pack_index *midx);
+struct bitmap_index *prepare_bitmap_git_for_source(struct odb_source_packed *source);
 
 /*
  * Given a bitmap index, determine whether it contains the pack either directly
@@ -90,13 +92,17 @@ int test_bitmap_pseudo_merge_objects(struct repository *r, uint32_t n);
 
 struct list_objects_filter_options;
 
+/* Check whether the filter can be computed via the bitmap. */
+bool can_filter_bitmap(const struct list_objects_filter_options *filter);
+
 /*
  * Filter bitmapped objects and iterate through all resulting objects,
  * executing `show_reach` for each of them. Returns `-1` in case the filter is
- * not supported, `0` otherwise.
+ * not supported, `0` otherwise. Aborts iteration and bubbles up the return
+ * value in case `show_reach()` returns non-zero.
  */
 int for_each_bitmapped_object(struct bitmap_index *bitmap_git,
-			      struct list_objects_filter_options *filter,
+			      const struct list_objects_filter_options *filter,
 			      show_reachable_fn show_reach,
 			      void *payload);
 
diff --git a/pack-check.c b/pack-check.c
index 5adfb3f..c3b8db7 100644
--- a/pack-check.c
+++ b/pack-check.c
@@ -69,7 +69,7 @@ static int verify_packfile(struct repository *r,
 	if (!is_pack_valid(p))
 		return error("packfile %s cannot be accessed", p->pack_name);
 
-	r->hash_algo->init_fn(&ctx);
+	git_hash_init(&ctx, r->hash_algo);
 	do {
 		unsigned long remaining;
 		unsigned char *in = use_pack(p, w_curs, offset, &remaining);
diff --git a/pack-write.c b/pack-write.c
index 83eaf88..24033a9 100644
--- a/pack-write.c
+++ b/pack-write.c
@@ -402,8 +402,8 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo,
 	char *buf;
 	ssize_t read_result;
 
-	hash_algo->init_fn(&old_hash_ctx);
-	hash_algo->init_fn(&new_hash_ctx);
+	git_hash_init(&old_hash_ctx, hash_algo);
+	git_hash_init(&new_hash_ctx, hash_algo);
 
 	if (lseek(pack_fd, 0, SEEK_SET) != 0)
 		die_errno("Failed seeking to start of '%s'", pack_name);
@@ -455,7 +455,7 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo,
 			 * pack, which also means making partial_pack_offset
 			 * big enough not to matter anymore.
 			 */
-			hash_algo->init_fn(&old_hash_ctx);
+			git_hash_init(&old_hash_ctx, hash_algo);
 			partial_pack_offset = ~partial_pack_offset;
 			partial_pack_offset -= MSB(partial_pack_offset, 1);
 		}
diff --git a/packfile.c b/packfile.c
index 1d1b23b..0eee450 100644
--- a/packfile.c
+++ b/packfile.c
@@ -855,7 +855,7 @@ void for_each_file_in_pack_dir(const char *objdir,
 
 struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store)
 {
-	odb_source_packed_prepare(store);
+	odb_source_prepare(&store->base, 0);
 
 	if (store->midx) {
 		struct multi_pack_index *m = store->midx;
@@ -1324,7 +1324,8 @@ static void add_delta_base_cache(struct packed_git *p, off_t base_offset,
 	hashmap_add(&delta_base_cache, &ent->ent);
 }
 
-int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset,
+int packed_object_info_with_index_pos(struct odb_source_packed *source,
+				      struct packed_git *p, off_t obj_offset,
 				      uint32_t *maybe_index_pos, struct object_info *oi)
 {
 	struct pack_window *w_curs = NULL;
@@ -1420,23 +1421,28 @@ int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset,
 			oidclr(oi->delta_base_oid, p->repo->hash_algo);
 	}
 
-	oi->whence = OI_PACKED;
-	oi->u.packed.offset = obj_offset;
-	oi->u.packed.pack = p;
+	if (oi->source_infop) {
+		if (!source)
+			BUG("cannot request source without an owning source");
+		oi->source_infop->source = &source->base;
 
-	switch (type) {
-	case OBJ_NONE:
-		oi->u.packed.type = PACKED_OBJECT_TYPE_UNKNOWN;
-		break;
-	case OBJ_REF_DELTA:
-		oi->u.packed.type = PACKED_OBJECT_TYPE_REF_DELTA;
-		break;
-	case OBJ_OFS_DELTA:
-		oi->u.packed.type = PACKED_OBJECT_TYPE_OFS_DELTA;
-		break;
-	default:
-		oi->u.packed.type = PACKED_OBJECT_TYPE_FULL;
-		break;
+		oi->source_infop->u.packed.offset = obj_offset;
+		oi->source_infop->u.packed.pack = p;
+
+		switch (type) {
+		case OBJ_NONE:
+			oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_UNKNOWN;
+			break;
+		case OBJ_REF_DELTA:
+			oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_REF_DELTA;
+			break;
+		case OBJ_OFS_DELTA:
+			oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_OFS_DELTA;
+			break;
+		default:
+			oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_FULL;
+			break;
+		}
 	}
 
 	ret = 0;
@@ -1446,10 +1452,11 @@ int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset,
 	return ret;
 }
 
-int packed_object_info(struct packed_git *p, off_t obj_offset,
+int packed_object_info(struct odb_source_packed *source,
+		       struct packed_git *p, off_t obj_offset,
 		       struct object_info *oi)
 {
-	return packed_object_info_with_index_pos(p, obj_offset, NULL, oi);
+	return packed_object_info_with_index_pos(source, p, obj_offset, NULL, oi);
 }
 
 static void *unpack_compressed_entry(struct packed_git *p,
diff --git a/packfile.h b/packfile.h
index 2329a69..e1f7715 100644
--- a/packfile.h
+++ b/packfile.h
@@ -320,9 +320,11 @@ extern int do_check_packed_object_crc;
  * Look up the object info for a specific offset in the packfile.
  * Returns zero on success, a negative error code otherwise.
  */
-int packed_object_info(struct packed_git *pack,
+int packed_object_info(struct odb_source_packed *source,
+		       struct packed_git *pack,
 		       off_t offset, struct object_info *);
-int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset,
+int packed_object_info_with_index_pos(struct odb_source_packed *source,
+				      struct packed_git *p, off_t obj_offset,
 				      uint32_t *maybe_index_pos, struct object_info *oi);
 
 void mark_bad_packed_object(struct packed_git *, const struct object_id *);
diff --git a/pager.c b/pager.c
index 35b210e..543ef12 100644
--- a/pager.c
+++ b/pager.c
@@ -5,6 +5,8 @@
 #include "run-command.h"
 #include "sigchain.h"
 #include "alias.h"
+#include "repository.h"
+#include "environment.h"
 
 int pager_use_color = 1;
 
@@ -13,7 +15,6 @@ int pager_use_color = 1;
 #endif
 
 static struct child_process pager_process;
-static char *pager_program;
 static int old_fd1 = -1, old_fd2 = -1;
 
 /* Is the value coming back from term_columns() just a guess? */
@@ -75,10 +76,17 @@ static void wait_for_pager_signal(int signo)
 
 static int core_pager_config(const char *var, const char *value,
 			     const struct config_context *ctx UNUSED,
-			     void *data UNUSED)
+			     void *data)
 {
-	if (!strcmp(var, "core.pager"))
-		return git_config_string(&pager_program, var, value);
+	struct repository *r = data;
+
+	if (!strcmp(var, "core.pager")) {
+		struct repo_config_values *cfg = repo_config_values(r);
+
+		FREE_AND_NULL(cfg->pager_program);
+		return git_config_string(&cfg->pager_program, var, value);
+	}
+
 	return 0;
 }
 
@@ -91,10 +99,12 @@ const char *git_pager(struct repository *r, int stdout_is_tty)
 
 	pager = getenv("GIT_PAGER");
 	if (!pager) {
-		if (!pager_program)
+		struct repo_config_values *cfg = repo_config_values(r);
+
+		if (!cfg->pager_program)
 			read_early_config(r,
-					  core_pager_config, NULL);
-		pager = pager_program;
+					  core_pager_config, r);
+		pager = cfg->pager_program;
 	}
 	if (!pager)
 		pager = getenv("PAGER");
@@ -302,7 +312,11 @@ int check_pager_config(struct repository *r, const char *cmd)
 
 	read_early_config(r, pager_command_config, &data);
 
-	if (data.value)
-		pager_program = data.value;
+	if (data.value) {
+		struct repo_config_values *cfg = repo_config_values(r);
+
+		free(cfg->pager_program);
+		cfg->pager_program = data.value;
+	}
 	return data.want;
 }
diff --git a/parse-options.c b/parse-options.c
index f4647e0..08c21d9 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -583,7 +583,7 @@ static enum parse_opt_result parse_long_opt(
 			ambiguous.option->long_name,
 			(abbrev.flags & OPT_UNSET) ?  "no-" : "",
 			abbrev.option->long_name);
-		return PARSE_OPT_HELP;
+		return PARSE_OPT_HELP_ERROR;
 	}
 	if (abbrev.option) {
 		if (*arg_end)
@@ -1037,6 +1037,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
 				usage_with_options(usagestr, options);
 			case PARSE_OPT_COMPLETE:
 			case PARSE_OPT_HELP:
+			case PARSE_OPT_HELP_ERROR:
 			case PARSE_OPT_ERROR:
 			case PARSE_OPT_DONE:
 			case PARSE_OPT_NON_OPTION:
@@ -1072,6 +1073,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
 			case PARSE_OPT_NON_OPTION:
 			case PARSE_OPT_SUBCOMMAND:
 			case PARSE_OPT_HELP:
+			case PARSE_OPT_HELP_ERROR:
 			case PARSE_OPT_COMPLETE:
 				BUG("parse_short_opt() cannot return these");
 			case PARSE_OPT_DONE:
@@ -1099,6 +1101,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
 				case PARSE_OPT_SUBCOMMAND:
 				case PARSE_OPT_COMPLETE:
 				case PARSE_OPT_HELP:
+				case PARSE_OPT_HELP_ERROR:
 					BUG("parse_short_opt() cannot return these");
 				case PARSE_OPT_DONE:
 					break;
@@ -1133,6 +1136,8 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
 			goto unknown;
 		case PARSE_OPT_HELP:
 			goto show_usage;
+		case PARSE_OPT_HELP_ERROR:
+			goto show_usage_stderr;
 		case PARSE_OPT_NON_OPTION:
 		case PARSE_OPT_SUBCOMMAND:
 		case PARSE_OPT_COMPLETE:
@@ -1166,6 +1171,9 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,
  show_usage:
 	return usage_with_options_internal(ctx, usagestr, options,
 					   USAGE_NORMAL, USAGE_TO_STDOUT);
+ show_usage_stderr:
+	return usage_with_options_internal(ctx, usagestr, options,
+					   USAGE_NORMAL, USAGE_TO_STDERR);
 }
 
 int parse_options_end(struct parse_opt_ctx_t *ctx)
@@ -1197,6 +1205,8 @@ int parse_options(int argc, const char **argv,
 	parse_options_start_1(&ctx, argc, argv, prefix, options, flags);
 	switch (parse_options_step(&ctx, options, usagestr)) {
 	case PARSE_OPT_HELP:
+		exit(0);
+	case PARSE_OPT_HELP_ERROR:
 	case PARSE_OPT_ERROR:
 		exit(129);
 	case PARSE_OPT_COMPLETE:
@@ -1363,7 +1373,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t
 	parse_options_check_harder(opts);
 
 	if (!usagestr)
-		return PARSE_OPT_HELP;
+		return err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;
 
 	if (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)
 		fprintf(outfile, "cat <<\\EOF\n");
@@ -1474,9 +1484,9 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t
 	fputc('\n', outfile);
 
 	if (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)
-		fputs("EOF\n", outfile);
+		fputs("EOF\nexit 0\n", outfile);
 
-	return PARSE_OPT_HELP;
+	return err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;
 }
 
 void NORETURN usage_with_options(const char * const *usagestr,
@@ -1495,11 +1505,11 @@ void show_usage_with_options_if_asked(int ac, const char **av,
 		if (!strcmp(av[1], "-h")) {
 			usage_with_options_internal(NULL, usagestr, opts,
 						    USAGE_NORMAL, USAGE_TO_STDOUT);
-			exit(129);
+			exit(0);
 		} else if (!strcmp(av[1], "--help-all")) {
 			usage_with_options_internal(NULL, usagestr, opts,
 						    USAGE_FULL, USAGE_TO_STDOUT);
-			exit(129);
+			exit(0);
 		}
 	}
 }
diff --git a/parse-options.h b/parse-options.h
index 0d1f738..3ec8ba5 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -57,7 +57,8 @@ enum parse_opt_option_flags {
 };
 
 enum parse_opt_result {
-	PARSE_OPT_COMPLETE = -3,
+	PARSE_OPT_COMPLETE = -4,
+	PARSE_OPT_HELP_ERROR = -3,
 	PARSE_OPT_HELP = -2,
 	PARSE_OPT_ERROR = -1,	/* must be the same as error() */
 	PARSE_OPT_DONE = 0,	/* fixed so that "return 0" works */
diff --git a/path-walk.c b/path-walk.c
index edc8e73..9ca3248 100644
--- a/path-walk.c
+++ b/path-walk.c
@@ -699,6 +699,7 @@ int walk_objects_by_path(struct path_walk_info *info)
 	int ret;
 	size_t commits_nr = 0, paths_nr = 0;
 	struct commit *c;
+	char *path;
 	struct type_and_oid_list *root_tree_list;
 	struct type_and_oid_list *commit_list;
 	struct path_walk_context ctx = {
@@ -808,8 +809,7 @@ int walk_objects_by_path(struct path_walk_info *info)
 	free(commit_list);
 
 	trace2_region_enter("path-walk", "path-walk", info->revs->repo);
-	while (!ret && ctx.path_stack.nr) {
-		char *path = prio_queue_get(&ctx.path_stack);
+	while (!ret && (path = prio_queue_get(&ctx.path_stack))) {
 		paths_nr++;
 
 		ret = walk_path(&ctx, path);
@@ -821,12 +821,12 @@ int walk_objects_by_path(struct path_walk_info *info)
 	if (!strmap_empty(&ctx.paths_to_lists)) {
 		struct hashmap_iter iter;
 		struct strmap_entry *entry;
+		char *path;
 
 		strmap_for_each_entry(&ctx.paths_to_lists, &iter, entry)
 			push_to_stack(&ctx, entry->key);
 
-		while (!ret && ctx.path_stack.nr) {
-			char *path = prio_queue_get(&ctx.path_stack);
+		while (!ret && (path = prio_queue_get(&ctx.path_stack))) {
 			paths_nr++;
 
 			ret = walk_path(&ctx, path);
diff --git a/pathspec.c b/pathspec.c
index f78b227..281858f 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -847,9 +847,9 @@ int pathspec_needs_expanded_index(struct index_state *istate,
 			 * - not-in-cone/bar*: may need expanded index
 			 * - **.c: may need expanded index
 			 */
-			if (strspn(item.original + item.nowildcard_len, "*") ==
+			if (strspn(item.match + item.nowildcard_len, "*") ==
 				    (unsigned int)(item.len - item.nowildcard_len) &&
-			    path_in_cone_mode_sparse_checkout(item.original, istate))
+			    path_in_cone_mode_sparse_checkout(item.match, istate))
 				continue;
 
 			for (pos = 0; pos < istate->cache_nr; pos++) {
@@ -865,7 +865,7 @@ int pathspec_needs_expanded_index(struct index_state *istate,
 				 */
 				if ((unsigned int)item.nowildcard_len >
 					    ce_namelen(ce) &&
-				    !strncmp(item.original, ce->name,
+				    !strncmp(item.match, ce->name,
 					     ce_namelen(ce))) {
 					res = 1;
 					break;
@@ -876,13 +876,13 @@ int pathspec_needs_expanded_index(struct index_state *istate,
 				 * directory and the pathspec does not match the whole
 				 * directory, need to expand the index.
 				 */
-				if (!strncmp(item.original, ce->name, item.nowildcard_len) &&
-				    wildmatch(item.original, ce->name, 0)) {
+				if (!strncmp(item.match, ce->name, item.nowildcard_len) &&
+				    wildmatch(item.match, ce->name, 0)) {
 					res = 1;
 					break;
 				}
 			}
-		} else if (!path_in_cone_mode_sparse_checkout(item.original, istate) &&
+		} else if (!path_in_cone_mode_sparse_checkout(item.match, istate) &&
 			   !matches_skip_worktree(pathspec, i, &skip_worktree_seen))
 			res = 1;
 
diff --git a/prio-queue.c b/prio-queue.c
index 9748528..199775d 100644
--- a/prio-queue.c
+++ b/prio-queue.c
@@ -22,40 +22,19 @@ void prio_queue_reverse(struct prio_queue *queue)
 
 	if (queue->compare)
 		BUG("prio_queue_reverse() on non-LIFO queue");
-	if (!queue->nr)
+	if (!queue->nr_)
 		return;
-	for (i = 0; i < (j = (queue->nr - 1) - i); i++)
+	for (i = 0; i < (j = (queue->nr_ - 1) - i); i++)
 		swap(queue, i, j);
 }
 
 void clear_prio_queue(struct prio_queue *queue)
 {
 	FREE_AND_NULL(queue->array);
-	queue->nr = 0;
+	queue->nr_ = 0;
 	queue->alloc = 0;
 	queue->insertion_ctr = 0;
-}
-
-void prio_queue_put(struct prio_queue *queue, void *thing)
-{
-	size_t ix, parent;
-
-	/* Append at the end */
-	ALLOC_GROW(queue->array, queue->nr + 1, queue->alloc);
-	queue->array[queue->nr].ctr = queue->insertion_ctr++;
-	queue->array[queue->nr].data = thing;
-	queue->nr++;
-	if (!queue->compare)
-		return; /* LIFO */
-
-	/* Bubble up the new one */
-	for (ix = queue->nr - 1; ix; ix = parent) {
-		parent = (ix - 1) / 2;
-		if (compare(queue, parent, ix) <= 0)
-			break;
-
-		swap(queue, parent, ix);
-	}
+	queue->get_pending = 0;
 }
 
 static void sift_down_root(struct prio_queue *queue)
@@ -63,9 +42,9 @@ static void sift_down_root(struct prio_queue *queue)
 	size_t ix, child;
 
 	/* Push down the one at the root */
-	for (ix = 0; ix * 2 + 1 < queue->nr; ix = child) {
+	for (ix = 0; ix * 2 + 1 < queue->nr_; ix = child) {
 		child = ix * 2 + 1; /* left */
-		if (child + 1 < queue->nr &&
+		if (child + 1 < queue->nr_ &&
 		    compare(queue, child, child + 1) >= 0)
 			child++; /* use right child */
 
@@ -76,43 +55,72 @@ static void sift_down_root(struct prio_queue *queue)
 	}
 }
 
+static inline void flush_get(struct prio_queue *queue)
+{
+	if (!queue->get_pending)
+		return;
+	queue->get_pending = 0;
+	queue->array[0] = queue->array[--queue->nr_];
+	sift_down_root(queue);
+}
+
+void prio_queue_put(struct prio_queue *queue, void *thing)
+{
+	size_t ix, parent;
+
+	if (queue->get_pending) {
+		queue->get_pending = 0;
+		queue->array[0].ctr = queue->insertion_ctr++;
+		queue->array[0].data = thing;
+		sift_down_root(queue);
+		return;
+	}
+
+	/* Append at the end */
+	ALLOC_GROW(queue->array, queue->nr_ + 1, queue->alloc);
+	queue->array[queue->nr_].ctr = queue->insertion_ctr++;
+	queue->array[queue->nr_].data = thing;
+	queue->nr_++;
+	if (!queue->compare)
+		return; /* LIFO */
+
+	/* Bubble up the new one */
+	for (ix = queue->nr_ - 1; ix; ix = parent) {
+		parent = (ix - 1) / 2;
+		if (compare(queue, parent, ix) <= 0)
+			break;
+
+		swap(queue, parent, ix);
+	}
+}
+
 void *prio_queue_get(struct prio_queue *queue)
 {
-	void *result;
-
-	if (!queue->nr)
+	if (queue->nr_ <= queue->get_pending) {
+		queue->nr_ = 0;
+		queue->get_pending = 0;
 		return NULL;
+	}
 	if (!queue->compare)
-		return queue->array[--queue->nr].data; /* LIFO */
+		return queue->array[--queue->nr_].data; /* LIFO */
 
-	result = queue->array[0].data;
-	if (!--queue->nr)
-		return result;
+	flush_get(queue);
 
-	queue->array[0] = queue->array[queue->nr];
-	sift_down_root(queue);
-	return result;
+	queue->get_pending = 1;
+	return queue->array[0].data;
 }
 
 void *prio_queue_peek(struct prio_queue *queue)
 {
-	if (!queue->nr)
+	if (queue->nr_ <= queue->get_pending) {
+		queue->nr_ = 0;
+		queue->get_pending = 0;
 		return NULL;
-	if (!queue->compare)
-		return queue->array[queue->nr - 1].data;
-	return queue->array[0].data;
-}
-
-void prio_queue_replace(struct prio_queue *queue, void *thing)
-{
-	if (!queue->nr) {
-		prio_queue_put(queue, thing);
-	} else if (!queue->compare) {
-		queue->array[queue->nr - 1].ctr = queue->insertion_ctr++;
-		queue->array[queue->nr - 1].data = thing;
-	} else {
-		queue->array[0].ctr = queue->insertion_ctr++;
-		queue->array[0].data = thing;
-		sift_down_root(queue);
 	}
+	if (!queue->compare)
+		return queue->array[queue->nr_ - 1].data;
+
+	flush_get(queue);
+
+	return queue->array[0].data;
 }
diff --git a/prio-queue.h b/prio-queue.h
index da7fad2..570b48e 100644
--- a/prio-queue.h
+++ b/prio-queue.h
@@ -30,8 +30,9 @@ struct prio_queue {
 	prio_queue_compare_fn compare;
 	size_t insertion_ctr;
 	void *cb_data;
-	size_t alloc, nr;
+	size_t alloc, nr_; /* use prio_queue_size() for logical count */
 	struct prio_queue_entry *array;
+	unsigned get_pending;
 };
 
 /*
@@ -52,13 +53,15 @@ void *prio_queue_get(struct prio_queue *);
  */
 void *prio_queue_peek(struct prio_queue *);
 
-/*
- * Replace the "thing" that compares the smallest with a new "thing",
- * like prio_queue_get()+prio_queue_put() would do, but in a more
- * efficient way.  Does the same as prio_queue_put() if the queue is
- * empty.
- */
-void prio_queue_replace(struct prio_queue *queue, void *thing);
+static inline size_t prio_queue_size(const struct prio_queue *queue)
+{
+	return queue->nr_ - queue->get_pending;
+}
+
+#define prio_queue_for_each(queue, it) \
+	for (size_t pq_ix_ = (queue)->get_pending; \
+	     pq_ix_ < (queue)->nr_ && ((it) = (queue)->array[pq_ix_].data, 1); \
+	     pq_ix_++)
 
 void clear_prio_queue(struct prio_queue *);
 
diff --git a/prompt.c b/prompt.c
index 706fba2..d8d74c7 100644
--- a/prompt.c
+++ b/prompt.c
@@ -3,6 +3,7 @@
 #include "git-compat-util.h"
 #include "parse.h"
 #include "environment.h"
+#include "repository.h"
 #include "run-command.h"
 #include "strbuf.h"
 #include "prompt.h"
@@ -51,7 +52,7 @@ char *git_prompt(const char *prompt, int flags)
 
 		askpass = getenv("GIT_ASKPASS");
 		if (!askpass)
-			askpass = askpass_program;
+			askpass = repo_config_values(the_repository)->askpass_program;
 		if (!askpass)
 			askpass = getenv("SSH_ASKPASS");
 		if (askpass && *askpass)
diff --git a/protocol-caps.c b/protocol-caps.c
index 8858ea4..02261be 100644
--- a/protocol-caps.c
+++ b/protocol-caps.c
@@ -31,6 +31,32 @@ static int parse_oid(const char *line, struct string_list *oid_str_list)
 }
 
 /*
+ * odb_read_object_info_extended() wrapper. Similar to odb_read_object_info()
+ * but uses the flags:
+ *
+ * - OBJECT_INFO_SKIP_FETCH_OBJECT so a server won't fetch an object when a
+ *   object-info request asks for an OID that it doesn't have.
+ *
+ * - OBJECT_INFO_QUICK to avoid re-scanning packs when the object is not found.
+ */
+static enum object_type get_object_info(struct object_database *odb,
+			   const struct object_id *oid,
+			   size_t *sizep)
+{
+	enum object_type type;
+	struct object_info oi = OBJECT_INFO_INIT;
+
+	oi.typep = &type;
+	oi.sizep = sizep;
+	if (odb_read_object_info_extended(odb, oid, &oi,
+					  OBJECT_INFO_LOOKUP_REPLACE |
+					  OBJECT_INFO_SKIP_FETCH_OBJECT |
+					  OBJECT_INFO_QUICK) < 0)
+		return OBJ_BAD;
+	return type;
+}
+
+/*
  * Validates and send requested info back to the client. Any errors detected
  * are returned as they are detected.
  */
@@ -62,15 +88,22 @@ static void send_info(struct repository *r, struct packet_writer *writer,
 
 		strbuf_addstr(&send_buffer, oid_str);
 
-		if (info->size) {
-			if (odb_read_object_info(r->objects, &oid, &object_size) < 0) {
-				strbuf_addstr(&send_buffer, " ");
-			} else {
-				strbuf_addf(&send_buffer, " %"PRIuMAX,
-					    (uintmax_t)object_size);
-			}
+		/*
+		 * Check the existence of the object first.
+		 * If an object is not recognized by the server append SP to
+		 * the response.
+		 */
+		if (get_object_info(r->objects, &oid, &object_size) <= OBJ_NONE) {
+			strbuf_addstr(&send_buffer, " ");
+			goto write;
 		}
 
+		if (info->size) {
+			strbuf_addf(&send_buffer, " %"PRIuMAX,
+				    (uintmax_t)object_size);
+		}
+
+write:
 		packet_writer_write(writer, "%s", send_buffer.buf);
 		strbuf_reset(&send_buffer);
 	}
diff --git a/reachable.c b/reachable.c
index 101cfc2..3079d2c 100644
--- a/reachable.c
+++ b/reachable.c
@@ -62,7 +62,7 @@ static void add_rebase_files(struct rev_info *revs)
 		"rebase-merge/autostash",
 		"rebase-merge/orig-head",
 	};
-	struct worktree **worktrees = get_worktrees();
+	struct worktree **worktrees = get_worktrees(the_repository);
 
 	for (struct worktree **wt = worktrees; *wt; wt++) {
 		char *wt_gitdir = get_worktree_git_dir(*wt);
@@ -234,8 +234,9 @@ static int add_recent_object(const struct object_id *oid,
 
 	add_pending_object(data->revs, obj, "");
 	if (data->cb) {
-		if (oi->whence == OI_PACKED)
-			data->cb(obj, oi->u.packed.pack, oi->u.packed.offset, *oi->mtimep);
+		if (oi->source_infop->source->type == ODB_SOURCE_PACKED)
+			data->cb(obj, oi->source_infop->u.packed.pack,
+				 oi->source_infop->u.packed.offset, *oi->mtimep);
 		else
 			data->cb(obj, NULL, 0, *oi->mtimep);
 	}
@@ -252,9 +253,11 @@ int add_unseen_recent_objects_to_traversal(struct rev_info *revs,
 	unsigned flags;
 	enum object_type type;
 	time_t mtime;
+	struct odb_source_info source_info;
 	struct object_info oi = {
 		.mtimep = &mtime,
 		.typep = &type,
+		.source_infop = &source_info,
 	};
 	int r;
 
@@ -319,7 +322,7 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
 
 	/* detached HEAD is not included in the list above */
 	refs_head_ref(get_main_ref_store(the_repository), add_one_ref, revs);
-	other_head_refs(add_one_ref, revs);
+	other_head_refs(the_repository, add_one_ref, revs);
 
 	/* rebase autostash and orig-head */
 	add_rebase_files(revs);
diff --git a/read-cache-ll.h b/read-cache-ll.h
index 2c8b4b2..71b8761 100644
--- a/read-cache-ll.h
+++ b/read-cache-ll.h
@@ -309,6 +309,7 @@ int write_locked_index(struct index_state *, struct lock_file *lock, unsigned fl
 void discard_index(struct index_state *);
 void move_index_extensions(struct index_state *dst, struct index_state *src);
 int unmerged_index(const struct index_state *);
+int index_state_unmerged_to_stage0(struct index_state *istate);
 
 /**
  * Returns 1 if istate differs from tree, 0 otherwise.  If tree is NULL,
diff --git a/read-cache.c b/read-cache.c
index 7c1cdcf..6c449f3 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -205,13 +205,11 @@ void fill_stat_cache_info(struct index_state *istate, struct cache_entry *ce, st
 
 static unsigned int st_mode_from_ce(const struct cache_entry *ce)
 {
-	extern int trust_executable_bit, has_symlinks;
-
 	switch (ce->ce_mode & S_IFMT) {
 	case S_IFLNK:
-		return has_symlinks ? S_IFLNK : (S_IFREG | 0644);
+		return repo_has_symlinks(the_repository) ? S_IFLNK : (S_IFREG | 0644);
 	case S_IFREG:
-		return (ce->ce_mode & (trust_executable_bit ? 0755 : 0644)) | S_IFREG;
+		return (ce->ce_mode & (repo_trust_executable_bit(the_repository) ? 0755 : 0644)) | S_IFREG;
 	case S_IFGITLINK:
 		return S_IFDIR | 0755;
 	case S_IFDIR:
@@ -321,13 +319,13 @@ static int ce_match_stat_basic(const struct cache_entry *ce, struct stat *st)
 		/* We consider only the owner x bit to be relevant for
 		 * "mode changes"
 		 */
-		if (trust_executable_bit &&
+		if (repo_trust_executable_bit(the_repository) &&
 		    (0100 & (ce->ce_mode ^ st->st_mode)))
 			changed |= MODE_CHANGED;
 		break;
 	case S_IFLNK:
 		if (!S_ISLNK(st->st_mode) &&
-		    (has_symlinks || !S_ISREG(st->st_mode)))
+		    (repo_has_symlinks(the_repository) || !S_ISREG(st->st_mode)))
 			changed |= TYPE_CHANGED;
 		break;
 	case S_IFGITLINK:
@@ -742,7 +740,8 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
 		ce->ce_flags |= CE_INTENT_TO_ADD;
 
 
-	if (trust_executable_bit && has_symlinks) {
+	if (repo_trust_executable_bit(istate->repo) &&
+	    repo_has_symlinks(istate->repo)) {
 		ce->ce_mode = create_ce_mode(st_mode);
 	} else {
 		/* If there is an existing entry, pick the mode bits and type
@@ -752,7 +751,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
 		int pos = index_name_pos_also_unmerged(istate, path, namelen);
 
 		ent = (0 <= pos) ? istate->cache[pos] : NULL;
-		ce->ce_mode = ce_mode_from_stat(ent, st_mode);
+		ce->ce_mode = ce_mode_from_stat(istate->repo, ent, st_mode);
 	}
 
 	/* When core.ignorecase=true, determine if a directory of the same name but differing
@@ -760,12 +759,12 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
 	 * case of the file being added to the repository matches (is folded into) the existing
 	 * entry's directory case.
 	 */
-	if (ignore_case) {
+	if (repo_ignore_case(the_repository)) {
 		adjust_dirname_case(istate, ce->name);
 	}
 	if (!(flags & ADD_CACHE_RENORMALIZE)) {
 		alias = index_file_exists(istate, ce->name,
-					  ce_namelen(ce), ignore_case);
+					  ce_namelen(ce), repo_ignore_case(the_repository));
 		if (alias &&
 		    !ce_stage(alias) &&
 		    !ie_match_stat(istate, alias, st, ce_option)) {
@@ -786,7 +785,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st,
 	} else
 		set_object_name_for_intent_to_add_entry(ce);
 
-	if (ignore_case && alias && different_name(ce, alias))
+	if (repo_ignore_case(the_repository) && alias && different_name(ce, alias))
 		ce = create_alias_ce(istate, ce, alias);
 	ce->ce_flags |= CE_ADDED;
 
@@ -1722,7 +1721,7 @@ static int verify_hdr(const struct cache_header *hdr, unsigned long size)
 	if (oideq(&oid, null_oid(the_hash_algo)))
 		return 0;
 
-	the_hash_algo->init_fn(&c);
+	git_hash_init(&c, the_hash_algo);
 	git_hash_update(&c, hdr, size - the_hash_algo->rawsz);
 	git_hash_final(hash, &c);
 	if (!hasheq(hash, start, the_repository->hash_algo))
@@ -2342,7 +2341,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist)
  */
 static void freshen_shared_index(const char *shared_index, int warn)
 {
-	if (!check_and_freshen_file(shared_index, 1) && warn)
+	if (!check_and_freshen_file(shared_index, 1, NULL) && warn)
 		warning(_("could not freshen shared index '%s'"), shared_index);
 }
 
@@ -2957,7 +2956,7 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
 	 */
 	if (offset && record_eoie()) {
 		CALLOC_ARRAY(eoie_c, 1);
-		the_hash_algo->init_fn(eoie_c);
+		git_hash_init(eoie_c, the_hash_algo);
 	}
 
 	/*
@@ -3404,13 +3403,15 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock,
  */
 int repo_read_index_unmerged(struct repository *repo)
 {
-	struct index_state *istate;
-	int i;
+	repo_read_index(repo);
+	return index_state_unmerged_to_stage0(repo->index);
+}
+
+int index_state_unmerged_to_stage0(struct index_state *istate)
+{
 	int unmerged = 0;
 
-	repo_read_index(repo);
-	istate = repo->index;
-	for (i = 0; i < istate->cache_nr; i++) {
+	for (unsigned int i = 0; i < istate->cache_nr; i++) {
 		struct cache_entry *ce = istate->cache[i];
 		struct cache_entry *new_ce;
 		int len;
@@ -3598,7 +3599,7 @@ static size_t read_eoie_extension(const char *mmap, size_t mmap_size)
 	 *	 "REUC" + <binary representation of M>)
 	 */
 	src_offset = offset;
-	the_hash_algo->init_fn(&c);
+	git_hash_init(&c, the_hash_algo);
 	while (src_offset < mmap_size - the_hash_algo->rawsz - EOIE_SIZE_WITH_HEADER) {
 		/* After an array of active_nr index entries,
 		 * there can be arbitrary number of extended
@@ -4013,6 +4014,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,
 		       const struct pathspec *pathspec, char *ps_matched,
 		       int include_sparse, int flags, int ignored_too )
 {
+	int inflight = !!repo->objects->transaction;
 	struct odb_transaction *transaction;
 	struct update_callback_data data;
 	struct rev_info rev;
@@ -4043,9 +4045,11 @@ int add_files_to_cache(struct repository *repo, const char *prefix,
 	 * This function is invoked from commands other than 'add', which
 	 * may not have their own transaction active.
 	 */
-	transaction = odb_transaction_begin(repo->objects);
+	if (!inflight)
+		odb_transaction_begin_or_die(repo->objects, &transaction, 0);
 	run_diff_files(&rev, DIFF_RACY_IS_MODIFIED);
-	odb_transaction_commit(transaction);
+	if (!inflight)
+		odb_transaction_commit(transaction);
 
 	release_revisions(&rev);
 	return !!data.add_errors;
diff --git a/read-cache.h b/read-cache.h
index 043da1f..ab9d40a 100644
--- a/read-cache.h
+++ b/read-cache.h
@@ -4,15 +4,23 @@
 #include "read-cache-ll.h"
 #include "object.h"
 #include "pathspec.h"
+#include "environment.h"
 
-static inline unsigned int ce_mode_from_stat(const struct cache_entry *ce,
+/*
+ * Determine the appropriate index mode for a file based on its stat()
+ * information and the existing cache entry (if any).
+ *
+ * This function handles degradation for filesystems that lack
+ * symlink support or reliable executable bits.
+ */
+static inline unsigned int ce_mode_from_stat(struct repository *repo,
+					     const struct cache_entry *ce,
 					     unsigned int mode)
 {
-	extern int trust_executable_bit, has_symlinks;
-	if (!has_symlinks && S_ISREG(mode) &&
+	if (S_ISREG(mode) && !repo_has_symlinks(repo) &&
 	    ce && S_ISLNK(ce->ce_mode))
 		return ce->ce_mode;
-	if (!trust_executable_bit && S_ISREG(mode)) {
+	if (S_ISREG(mode) && !repo_trust_executable_bit(repo)) {
 		if (ce && S_ISREG(ce->ce_mode))
 			return ce->ce_mode;
 		return create_ce_mode(0666);
diff --git a/ref-filter.c b/ref-filter.c
index 284796c..29aca08 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -2402,7 +2402,7 @@ static void lazy_init_worktree_map(void)
 	if (ref_to_worktree_map.worktrees)
 		return;
 
-	ref_to_worktree_map.worktrees = get_worktrees();
+	ref_to_worktree_map.worktrees = get_worktrees(the_repository);
 	hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
 	populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
 }
diff --git a/refs.c b/refs.c
index 1d24637..92d5df5 100644
--- a/refs.c
+++ b/refs.c
@@ -2,8 +2,6 @@
  * The backend-independent part of the reference module.
  */
 
-#define USE_THE_REPOSITORY_VARIABLE
-
 #include "git-compat-util.h"
 #include "abspath.h"
 #include "advice.h"
@@ -744,14 +742,15 @@ static char *substitute_branch_name(struct repository *r,
 	return NULL;
 }
 
-void copy_branchname(struct strbuf *sb, const char *name,
+void copy_branchname(struct repository *repo,
+		     struct strbuf *sb, const char *name,
 		     enum interpret_branch_kind allowed)
 {
 	int len = strlen(name);
 	struct interpret_branch_name_options options = {
 		.allowed = allowed
 	};
-	int used = repo_interpret_branch_name(the_repository, name, len, sb,
+	int used = repo_interpret_branch_name(repo, name, len, sb,
 					      &options);
 
 	if (used < 0)
@@ -759,10 +758,10 @@ void copy_branchname(struct strbuf *sb, const char *name,
 	strbuf_add(sb, name + used, len - used);
 }
 
-int check_branch_ref(struct strbuf *sb, const char *name)
+int check_branch_ref(struct repository *repo, struct strbuf *sb, const char *name)
 {
 	if (startup_info->have_repository)
-		copy_branchname(sb, name, INTERPRET_BRANCH_LOCAL);
+		copy_branchname(repo, sb, name, INTERPRET_BRANCH_LOCAL);
 	else
 		strbuf_addstr(sb, name);
 
@@ -3326,9 +3325,9 @@ static int move_files(const char *from_path, const char *to_path, struct strbuf
 	return ret;
 }
 
-static int has_worktrees(void)
+static int has_worktrees(struct repository *repo)
 {
-	struct worktree **worktrees = get_worktrees();
+	struct worktree **worktrees = get_worktrees(repo);
 	int ret = 0;
 	size_t i;
 
@@ -3373,12 +3372,8 @@ int repo_migrate_ref_storage_format(struct repository *repo,
 	 * Worktrees complicate the migration because every worktree has a
 	 * separate ref storage. While it should be feasible to implement, this
 	 * is pushed out to a future iteration.
-	 *
-	 * TODO: we should really be passing the caller-provided repository to
-	 * `has_worktrees()`, but our worktree subsystem doesn't yet support
-	 * that.
 	 */
-	if (has_worktrees()) {
+	if (has_worktrees(repo)) {
 		strbuf_addstr(errbuf, "migrating repositories with worktrees is not supported yet");
 		ret = -1;
 		goto done;
@@ -3503,7 +3498,7 @@ int repo_migrate_ref_storage_format(struct repository *repo,
 	 * repository format so that clients will use the new ref store.
 	 * We also need to swap out the repository's main ref store.
 	 */
-	initialize_repository_version(the_repository, hash_algo_by_ptr(repo->hash_algo), format, 1);
+	initialize_repository_version(repo, hash_algo_by_ptr(repo->hash_algo), format, 1);
 
 	/*
 	 * Unset the old ref store and release it. `get_main_ref_store()` will
diff --git a/refs.h b/refs.h
index a381022..9979446 100644
--- a/refs.h
+++ b/refs.h
@@ -234,7 +234,8 @@ char *repo_default_branch_name(struct repository *r, int quiet);
  * If "allowed" is non-zero, restrict the set of allowed expansions. See
  * repo_interpret_branch_name() for details.
  */
-void copy_branchname(struct strbuf *sb, const char *name,
+void copy_branchname(struct repository *repo,
+		     struct strbuf *sb, const char *name,
 		     enum interpret_branch_kind allowed);
 
 /*
@@ -243,7 +244,7 @@ void copy_branchname(struct strbuf *sb, const char *name,
  *
  * The return value is "0" if the result is valid, and "-1" otherwise.
  */
-int check_branch_ref(struct strbuf *sb, const char *name);
+int check_branch_ref(struct repository *repo, struct strbuf *sb, const char *name);
 
 /*
  * Similar for a tag name in refs/tags/.
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 3df56c2..1cc20aa 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "../git-compat-util.h"
@@ -29,6 +28,9 @@
 #include "../revision.h"
 #include <wildmatch.h>
 
+/* So that we can drop `USE_THE_REPOSITORY_VARIABLE`. */
+extern int ignore_case;
+
 /*
  * This backend uses the following flags in `ref_update::flags` for
  * internal bookkeeping purposes. Their numerical values must not
@@ -788,7 +790,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
 	files_ref_path(refs, &ref_file, refname);
 
 retry:
-	switch (safe_create_leading_directories(the_repository, ref_file.buf)) {
+	switch (safe_create_leading_directories(refs->base.repo, ref_file.buf)) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_EXISTS:
@@ -842,7 +844,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
 		goto error_return;
 	}
 
-	if (hold_lock_file_for_update_timeout(
+	if (repo_hold_lock_file_for_update_timeout(refs->base.repo,
 			    &lock->lk, ref_file.buf, LOCK_NO_DEREF,
 			    get_files_ref_lock_timeout_ms(transaction->ref_store->repo)) < 0) {
 		int myerr = errno;
@@ -857,7 +859,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
 		} else {
 			unable_to_lock_message(ref_file.buf, myerr, err);
 			if (myerr == EEXIST) {
-				if (ignore_case &&
+				if (repo_ignore_case(refs->base.repo) &&
 				    transaction_has_case_conflicting_update(transaction, update)) {
 					/*
 					 * In case-insensitive filesystems, ensure that conflicts within a
@@ -971,7 +973,7 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
 		 * conflicts between 'foo' and 'Foo/bar'. So let's lowercase
 		 * the refname.
 		 */
-		if (ignore_case) {
+		if (repo_ignore_case(refs->base.repo)) {
 			struct strbuf lower = STRBUF_INIT;
 
 			strbuf_addstr(&lower, refname);
@@ -1164,7 +1166,8 @@ typedef int create_file_fn(const char *path, void *cb);
  * recent call of fn. fn is always called at least once, and will be
  * called more than once if it returns ENOENT or EISDIR.
  */
-static int raceproof_create_file(const char *path, create_file_fn fn, void *cb)
+static int raceproof_create_file(struct files_ref_store *refs,
+				 const char *path, create_file_fn fn, void *cb)
 {
 	/*
 	 * The number of times we will try to remove empty directories
@@ -1220,7 +1223,7 @@ static int raceproof_create_file(const char *path, create_file_fn fn, void *cb)
 			strbuf_addstr(&path_copy, path);
 
 		do {
-			scld_result = safe_create_leading_directories(the_repository, path_copy.buf);
+			scld_result = safe_create_leading_directories(refs->base.repo, path_copy.buf);
 			if (scld_result == SCLD_OK)
 				goto retry_fn;
 		} while (scld_result == SCLD_VANISHED && create_directories_remaining-- > 0);
@@ -1250,8 +1253,8 @@ struct create_reflock_cb {
 static int create_reflock(const char *path, void *cb)
 {
 	struct create_reflock_cb *data = cb;
-	return hold_lock_file_for_update_timeout(
-			data->lk, path, LOCK_NO_DEREF,
+	return repo_hold_lock_file_for_update_timeout(
+			data->repo, data->lk, path, LOCK_NO_DEREF,
 			get_files_ref_lock_timeout_ms(data->repo)) < 0 ? -1 : 0;
 }
 
@@ -1289,7 +1292,7 @@ static struct ref_lock *lock_ref_oid_basic(struct files_ref_store *refs,
 	cb_data.lk   = &lock->lk;
 	cb_data.repo = refs->base.repo;
 
-	if (raceproof_create_file(ref_file.buf, create_reflock, &cb_data)) {
+	if (raceproof_create_file(refs, ref_file.buf, create_reflock, &cb_data)) {
 		unable_to_lock_message(ref_file.buf, errno, err);
 		goto error_return;
 	}
@@ -1383,7 +1386,7 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r)
 	ref_transaction_add_update(
 			transaction, r->name,
 			REF_NO_DEREF | REF_HAVE_NEW | REF_HAVE_OLD | REF_IS_PRUNING,
-			null_oid(the_hash_algo), &r->oid, NULL, NULL, NULL,
+			null_oid(refs->base.repo->hash_algo), &r->oid, NULL, NULL, NULL,
 			NULL, NULL);
 	if (ref_transaction_commit(transaction, &err))
 		goto cleanup;
@@ -1629,7 +1632,7 @@ static int rename_tmp_log(struct files_ref_store *refs, const char *newrefname)
 	files_reflog_path(refs, &path, newrefname);
 	files_reflog_path(refs, &tmp, TMP_RENAMED_LOG);
 	cb.tmp_renamed_log = tmp.buf;
-	ret = raceproof_create_file(path.buf, rename_tmp_log_callback, &cb);
+	ret = raceproof_create_file(refs, path.buf, rename_tmp_log_callback, &cb);
 	if (ret) {
 		if (errno == EISDIR)
 			error("directory not empty: %s", path.buf);
@@ -1736,7 +1739,7 @@ static int files_copy_or_rename_ref(struct ref_store *ref_store,
 		goto out;
 	}
 
-	if (copy && log && copy_file(tmp_renamed_log.buf, sb_oldref.buf, 0644)) {
+	if (copy && log && copy_file(refs->base.repo, tmp_renamed_log.buf, sb_oldref.buf, 0644)) {
 		ret = error("unable to copy logfile logs/%s to logs/"TMP_RENAMED_LOG": %s",
 			    oldrefname, strerror(errno));
 		goto out;
@@ -1916,13 +1919,13 @@ static int log_ref_setup(struct files_ref_store *refs,
 	char *logfile;
 
 	if (log_refs_cfg == LOG_REFS_UNSET)
-		log_refs_cfg = is_bare_repository(the_repository) ? LOG_REFS_NONE : LOG_REFS_NORMAL;
+		log_refs_cfg = is_bare_repository(refs->base.repo) ? LOG_REFS_NONE : LOG_REFS_NORMAL;
 
 	files_reflog_path(refs, &logfile_sb, refname);
 	logfile = strbuf_detach(&logfile_sb, NULL);
 
 	if (force_create || should_autocreate_reflog(log_refs_cfg, refname)) {
-		if (raceproof_create_file(logfile, open_or_create_logfile, logfd)) {
+		if (raceproof_create_file(refs, logfile, open_or_create_logfile, logfd)) {
 			if (errno == ENOENT)
 				strbuf_addf(err, "unable to create directory for '%s': "
 					    "%s", logfile, strerror(errno));
@@ -1955,7 +1958,7 @@ static int log_ref_setup(struct files_ref_store *refs,
 	}
 
 	if (*logfd >= 0)
-		adjust_shared_perm(the_repository, logfile);
+		adjust_shared_perm(refs->base.repo, logfile);
 
 	free(logfile);
 	return 0;
@@ -3581,7 +3584,9 @@ static int files_reflog_expire(struct ref_store *ref_store,
 		 * work we need, including cleaning up if the program
 		 * exits unexpectedly.
 		 */
-		if (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) {
+		if (repo_hold_lock_file_for_update(ref_store->repo,
+						   &reflog_lock, log_file,
+						   0) < 0) {
 			struct strbuf err = STRBUF_INIT;
 			unable_to_lock_message(log_file, errno, &err);
 			error("%s", err.buf);
@@ -3672,8 +3677,8 @@ static int files_ref_store_create_on_disk(struct ref_store *ref_store,
 	 *   they do not understand the reference format extension.
 	 */
 	strbuf_addf(&sb, "%s/refs", ref_store->gitdir);
-	safe_create_dir(the_repository, sb.buf, 1);
-	adjust_shared_perm(the_repository, sb.buf);
+	safe_create_dir(refs->base.repo, sb.buf, 1);
+	adjust_shared_perm(refs->base.repo, sb.buf);
 
 	/*
 	 * There is no need to create directories for common refs when creating
@@ -3685,11 +3690,11 @@ static int files_ref_store_create_on_disk(struct ref_store *ref_store,
 		 */
 		strbuf_reset(&sb);
 		files_ref_path(refs, &sb, "refs/heads");
-		safe_create_dir(the_repository, sb.buf, 1);
+		safe_create_dir(refs->base.repo, sb.buf, 1);
 
 		strbuf_reset(&sb);
 		files_ref_path(refs, &sb, "refs/tags");
-		safe_create_dir(the_repository, sb.buf, 1);
+		safe_create_dir(refs->base.repo, sb.buf, 1);
 	}
 
 	strbuf_release(&sb);
diff --git a/refs/packed-backend.c b/refs/packed-backend.c
index 499cb55..b9b04b7 100644
--- a/refs/packed-backend.c
+++ b/refs/packed-backend.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "../git-compat-util.h"
@@ -162,6 +161,13 @@ struct packed_ref_store {
 	 * `packed_ref_store`) must not be freed.
 	 */
 	struct tempfile *tempfile;
+
+	/*
+	 * Timeout when taking the "packed-refs.lock" file. configurable via
+	 * "core.packedRefsTimeout".
+	 */
+	bool timeout_configured;
+	int timeout_value;
 };
 
 /*
@@ -1233,12 +1239,12 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)
 	struct packed_ref_store *refs =
 		packed_downcast(ref_store, REF_STORE_WRITE | REF_STORE_MAIN,
 				"packed_refs_lock");
-	static int timeout_configured = 0;
-	static int timeout_value = 1000;
 
-	if (!timeout_configured) {
-		repo_config_get_int(the_repository, "core.packedrefstimeout", &timeout_value);
-		timeout_configured = 1;
+	if (!refs->timeout_configured) {
+		if (repo_config_get_int(ref_store->repo, "core.packedrefstimeout",
+					&refs->timeout_value))
+			refs->timeout_value = 1000;
+		refs->timeout_configured = true;
 	}
 
 	/*
@@ -1246,10 +1252,9 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)
 	 * don't write new content to it, but rather to a separate
 	 * tempfile.
 	 */
-	if (hold_lock_file_for_update_timeout(
-			    &refs->lock,
-			    refs->path,
-			    flags, timeout_value) < 0) {
+	if (repo_hold_lock_file_for_update_timeout(ref_store->repo, &refs->lock,
+						   refs->path, flags,
+						   refs->timeout_value) < 0) {
 		unable_to_lock_message(refs->path, errno, err);
 		return -1;
 	}
@@ -1393,7 +1398,7 @@ static enum ref_transaction_error write_with_updates(struct packed_ref_store *re
 	packed_refs_path = get_locked_file_path(&refs->lock);
 	strbuf_addf(&sb, "%s.new", packed_refs_path);
 	free(packed_refs_path);
-	refs->tempfile = create_tempfile(sb.buf);
+	refs->tempfile = repo_create_tempfile(refs->base.repo, sb.buf);
 	if (!refs->tempfile) {
 		strbuf_addf(err, "unable to create file %s: %s",
 			    sb.buf, strerror(errno));
diff --git a/refs/packed-backend.h b/refs/packed-backend.h
index 1db48e8..8a7b323 100644
--- a/refs/packed-backend.h
+++ b/refs/packed-backend.h
@@ -21,7 +21,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,
 
 /*
  * Lock the packed-refs file for writing. Flags is passed to
- * hold_lock_file_for_update(). Return 0 on success. On errors, write
+ * repo_hold_lock_file_for_update(). Return 0 on success. On errors, write
  * an error message to `err` and return a nonzero value.
  */
 int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err);
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 212408c..028f021 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -84,7 +84,8 @@ static int reftable_backend_read_ref(struct reftable_backend *be,
 	if (ret)
 		goto done;
 
-	if (strcmp(ref.refname, refname)) {
+	if (strcmp(ref.refname, refname) ||
+	    reftable_ref_record_is_deletion(&ref)) {
 		ret = 1;
 		goto done;
 	}
@@ -110,7 +111,6 @@ static int reftable_backend_read_ref(struct reftable_backend *be,
 		oidread(oid, reftable_ref_record_val1(&ref),
 			&hash_algos[hash_id]);
 	} else {
-		/* We got a tombstone, which should not happen. */
 		BUG("unhandled reference value type %d", ref.value_type);
 	}
 
@@ -652,6 +652,9 @@ static int reftable_ref_iterator_advance(struct ref_iterator *ref_iterator)
 			break;
 		}
 
+		if (iter->ref.value_type == REFTABLE_REF_DELETION)
+			continue;
+
 		if (iter->exclude_patterns && should_exclude_current_ref(iter))
 			continue;
 
@@ -1532,6 +1535,8 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data
 					ret = 0;
 					break;
 				}
+				if (reftable_log_record_is_deletion(&log))
+					continue;
 
 				ALLOC_GROW(logs, logs_nr + 1, logs_alloc);
 				tombstone = &logs[logs_nr++];
@@ -1929,6 +1934,8 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data)
 			ret = 0;
 			break;
 		}
+		if (reftable_log_record_is_deletion(&old_log))
+			continue;
 
 		free(old_log.refname);
 
@@ -2061,6 +2068,9 @@ static int reftable_reflog_iterator_advance(struct ref_iterator *ref_iterator)
 		if (iter->err)
 			break;
 
+		if (reftable_log_record_is_deletion(&iter->log))
+			continue;
+
 		/*
 		 * We want the refnames that we have reflogs for, so we skip if
 		 * we've already produced this name. This could be faster by
@@ -2220,6 +2230,8 @@ static int reftable_be_for_each_reflog_ent_reverse(struct ref_store *ref_store,
 			ret = 0;
 			break;
 		}
+		if (reftable_log_record_is_deletion(&log))
+			continue;
 
 		ret = yield_log_record(refs, &log, fn, cb_data);
 		if (ret)
@@ -2272,6 +2284,10 @@ static int reftable_be_for_each_reflog_ent(struct ref_store *ref_store,
 			ret = 0;
 			break;
 		}
+		if (reftable_log_record_is_deletion(&log)) {
+			reftable_log_record_release(&log);
+			continue;
+		}
 
 		ALLOC_GROW(logs, logs_nr + 1, logs_alloc);
 		logs[logs_nr++] = log;
@@ -2318,18 +2334,26 @@ static int reftable_be_reflog_exists(struct ref_store *ref_store,
 		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.
+	 * Check whether we get at least one non-deleted 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;
+	while (1) {
+		ret = reftable_iterator_next_log(&it, &log);
+		if (ret < 0)
+			goto done;
+		if (ret > 0) {
+			ret = 0;
+			goto done;
+		}
+		if (strcmp(log.refname, refname)) {
+			ret = 0;
+			goto done;
+		}
+		if (!reftable_log_record_is_deletion(&log))
+			break;
 	}
 
-	ret = strcmp(log.refname, refname) == 0;
+	ret = 1;
 
 done:
 	reftable_iterator_destroy(&it);
@@ -2442,6 +2466,8 @@ static int write_reflog_delete_table(struct reftable_writer *writer, void *cb_da
 			ret = 0;
 			break;
 		}
+		if (reftable_log_record_is_deletion(&log))
+			continue;
 
 		tombstone.refname = (char *)arg->refname;
 		tombstone.value_type = REFTABLE_LOG_DELETION;
@@ -2625,6 +2651,10 @@ static int reftable_be_reflog_expire(struct ref_store *ref_store,
 			reftable_log_record_release(&log);
 			break;
 		}
+		if (reftable_log_record_is_deletion(&log)) {
+			reftable_log_record_release(&log);
+			continue;
+		}
 
 		oidread(&old_oid, log.value.update.old_hash,
 			ref_store->repo->hash_algo);
@@ -2791,6 +2821,8 @@ static int reftable_be_fsck(struct ref_store *ref_store, struct fsck_options *o,
 		report.path = refname.buf;
 
 		switch (ref.value_type) {
+		case REFTABLE_REF_DELETION:
+			continue;
 		case REFTABLE_REF_VAL1:
 		case REFTABLE_REF_VAL2: {
 			struct object_id oid;
diff --git a/refspec.c b/refspec.c
index fb89bce..7cb4799 100644
--- a/refspec.c
+++ b/refspec.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
@@ -16,7 +15,8 @@
  * Parses the provided refspec 'refspec' and populates the refspec_item 'item'.
  * Returns 1 if successful and 0 if the refspec is invalid.
  */
-static int parse_refspec(struct refspec_item *item, const char *refspec, int fetch)
+static int parse_refspec(struct refspec_item *item, const char *refspec,
+			 const struct git_hash_algo *algo, int fetch)
 {
 	size_t llen;
 	int is_glob;
@@ -84,7 +84,7 @@ static int parse_refspec(struct refspec_item *item, const char *refspec, int fet
 		 */
 		if (!*item->src)
 			return 0; /* negative refspecs must not be empty */
-		else if (llen == the_hash_algo->hexsz && !get_oid_hex(item->src, &unused))
+		else if (llen == algo->hexsz && !get_oid_hex_algop(item->src, &unused, algo))
 			return 0; /* negative refspecs cannot be exact sha1 */
 		else if (!check_refname_format(item->src, flags))
 			; /* valid looking ref is ok */
@@ -101,7 +101,7 @@ static int parse_refspec(struct refspec_item *item, const char *refspec, int fet
 		/* LHS */
 		if (!*item->src)
 			; /* empty is ok; it means "HEAD" */
-		else if (llen == the_hash_algo->hexsz && !get_oid_hex(item->src, &unused))
+		else if (llen == algo->hexsz && !get_oid_hex_algop(item->src, &unused, algo))
 			item->exact_sha1 = 1; /* ok */
 		else if (!check_refname_format(item->src, flags))
 			; /* valid looking ref is ok */
@@ -154,21 +154,23 @@ static int parse_refspec(struct refspec_item *item, const char *refspec, int fet
 }
 
 static int refspec_item_init(struct refspec_item *item, const char *refspec,
-			     int fetch)
+			     const struct git_hash_algo *algo, int fetch)
 {
 	memset(item, 0, sizeof(*item));
 	item->raw = xstrdup(refspec);
-	return parse_refspec(item, refspec, fetch);
+	return parse_refspec(item, refspec, algo, fetch);
 }
 
-int refspec_item_init_fetch(struct refspec_item *item, const char *refspec)
+int refspec_item_init_fetch(struct refspec_item *item, const char *refspec,
+			    const struct git_hash_algo *algo)
 {
-	return refspec_item_init(item, refspec, 1);
+	return refspec_item_init(item, refspec, algo, 1);
 }
 
-int refspec_item_init_push(struct refspec_item *item, const char *refspec)
+int refspec_item_init_push(struct refspec_item *item, const char *refspec,
+			   const struct git_hash_algo *algo)
 {
-	return refspec_item_init(item, refspec, 0);
+	return refspec_item_init(item, refspec, algo, 0);
 }
 
 void refspec_item_clear(struct refspec_item *item)
@@ -182,15 +184,15 @@ void refspec_item_clear(struct refspec_item *item)
 	item->exact_sha1 = 0;
 }
 
-void refspec_init_fetch(struct refspec *rs)
+void refspec_init_fetch(struct refspec *rs, const struct git_hash_algo *algo)
 {
-	struct refspec blank = REFSPEC_INIT_FETCH;
+	struct refspec blank = REFSPEC_INIT_FETCH(algo);
 	memcpy(rs, &blank, sizeof(*rs));
 }
 
-void refspec_init_push(struct refspec *rs)
+void refspec_init_push(struct refspec *rs, const struct git_hash_algo *algo)
 {
-	struct refspec blank = REFSPEC_INIT_PUSH;
+	struct refspec blank = REFSPEC_INIT_PUSH(algo);
 	memcpy(rs, &blank, sizeof(*rs));
 }
 
@@ -200,9 +202,9 @@ void refspec_append(struct refspec *rs, const char *refspec)
 	int ret;
 
 	if (rs->fetch)
-		ret = refspec_item_init_fetch(&item, refspec);
+		ret = refspec_item_init_fetch(&item, refspec, rs->hash_algo);
 	else
-		ret = refspec_item_init_push(&item, refspec);
+		ret = refspec_item_init_push(&item, refspec, rs->hash_algo);
 	if (!ret)
 		die(_("invalid refspec '%s'"), refspec);
 
@@ -246,10 +248,11 @@ void refspec_clear(struct refspec *rs)
 	rs->fetch = 0;
 }
 
-int valid_fetch_refspec(const char *fetch_refspec_str)
+int valid_fetch_refspec(const char *fetch_refspec_str,
+			const struct git_hash_algo *algo)
 {
 	struct refspec_item refspec;
-	int ret = refspec_item_init_fetch(&refspec, fetch_refspec_str);
+	int ret = refspec_item_init_fetch(&refspec, fetch_refspec_str, algo);
 	refspec_item_clear(&refspec);
 	return ret;
 }
diff --git a/refspec.h b/refspec.h
index 8b04f99..fadef67 100644
--- a/refspec.h
+++ b/refspec.h
@@ -1,6 +1,10 @@
 #ifndef REFSPEC_H
 #define REFSPEC_H
 
+struct git_hash_algo;
+struct string_list;
+struct strvec;
+
 #define TAG_REFSPEC "refs/tags/*:refs/tags/*"
 
 /**
@@ -30,10 +34,11 @@ struct refspec_item {
 	char *raw;
 };
 
-struct string_list;
-
-#define REFSPEC_INIT_FETCH { .fetch = 1 }
-#define REFSPEC_INIT_PUSH { .fetch = 0 }
+int refspec_item_init_fetch(struct refspec_item *item, const char *refspec,
+			    const struct git_hash_algo *algo);
+int refspec_item_init_push(struct refspec_item *item, const char *refspec,
+			   const struct git_hash_algo *algo);
+void refspec_item_clear(struct refspec_item *item);
 
 /**
  * An array of strings can be parsed into a struct refspec using
@@ -44,23 +49,30 @@ struct refspec {
 	int alloc;
 	int nr;
 
+	const struct git_hash_algo *hash_algo;
 	unsigned fetch : 1;
 };
 
-int refspec_item_init_fetch(struct refspec_item *item, const char *refspec);
-int refspec_item_init_push(struct refspec_item *item, const char *refspec);
-void refspec_item_clear(struct refspec_item *item);
-void refspec_init_fetch(struct refspec *rs);
-void refspec_init_push(struct refspec *rs);
+#define REFSPEC_INIT_FETCH(algo) { \
+	.fetch = 1, \
+	.hash_algo = (algo), \
+}
+#define REFSPEC_INIT_PUSH(algo) { \
+	.fetch = 0, \
+	.hash_algo = (algo), \
+}
+
+void refspec_init_fetch(struct refspec *rs, const struct git_hash_algo *hash_algo);
+void refspec_init_push(struct refspec *rs, const struct git_hash_algo *hash_algo);
+void refspec_clear(struct refspec *rs);
+
 void refspec_append(struct refspec *rs, const char *refspec);
 __attribute__((format (printf,2,3)))
 void refspec_appendf(struct refspec *rs, const char *fmt, ...);
 void refspec_appendn(struct refspec *rs, const char **refspecs, int nr);
-void refspec_clear(struct refspec *rs);
 
-int valid_fetch_refspec(const char *refspec);
+int valid_fetch_refspec(const char *refspec, const struct git_hash_algo *algo);
 
-struct strvec;
 /*
  * Determine what <prefix> values to pass to the peer in ref-prefix lines
  * (see linkgit:gitprotocol-v2[5]).
@@ -76,7 +88,7 @@ int refname_matches_negative_refspec_item(const char *refname, struct refspec *r
  * Returns 1 if refname matches pattern, 0 otherwise.
  */
 int match_refname_with_pattern(const char *pattern, const char *refname,
-				   const char *replacement, char **result);
+			       const char *replacement, char **result);
 
 /*
  * Queries a refspec for a match and updates the query item.
@@ -89,8 +101,8 @@ int refspec_find_match(struct refspec *rs, struct refspec_item *query);
  * list.
  */
 void refspec_find_all_matches(struct refspec *rs,
-				    struct refspec_item *query,
-				    struct string_list *results);
+			      struct refspec_item *query,
+			      struct string_list *results);
 
 /*
  * Remove all entries in the input list which match any negative refspec in
diff --git a/reftable/basics.c b/reftable/basics.c
index e969927..f0442a4 100644
--- a/reftable/basics.c
+++ b/reftable/basics.c
@@ -152,6 +152,9 @@ size_t binsearch(size_t sz, int (*f)(size_t k, void *args), void *args)
 	size_t lo = 0;
 	size_t hi = sz;
 
+	if (!sz)
+		return 0;
+
 	/* Invariants:
 	 *
 	 *  (hi == sz) || f(hi) == true
diff --git a/reftable/block.c b/reftable/block.c
index 920b3f4..1fa8140 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -260,6 +260,15 @@ int reftable_block_init(struct reftable_block *block,
 			goto done;
 	}
 
+	/*
+	 * Verify that the block size covers at least the table header, block
+	 * header and the 2 byte restart counter.
+	 */
+	if (block_size < header_size + 4 + 2) {
+		err = REFTABLE_FORMAT_ERROR;
+		goto done;
+	}
+
 	if (block_type == REFTABLE_BLOCK_TYPE_LOG) {
 		uint32_t block_header_skip = 4 + header_size;
 		uLong dst_len = block_size - block_header_skip;
@@ -331,8 +340,21 @@ int reftable_block_init(struct reftable_block *block,
 		full_block_size = block_size;
 	}
 
+	/*
+	 * Ensure that we have sufficient data available now to satisfy the
+	 * claimed block size.
+	 */
+	if (block_size > block->block_data.len) {
+		err = REFTABLE_FORMAT_ERROR;
+		goto done;
+	}
+
 	restart_count = reftable_get_be16(block->block_data.data + block_size - 2);
 	restart_off = block_size - 2 - 3 * restart_count;
+	if (restart_off < header_size + 4 || restart_off > block_size - 2) {
+		err = REFTABLE_FORMAT_ERROR;
+		goto done;
+	}
 
 	block->block_type = block_type;
 	block->hash_size = hash_size;
@@ -419,6 +441,15 @@ static int restart_needle_less(size_t idx, void *_args)
 	int n;
 
 	/*
+	 * The restart offset must point to a record, which is stored before
+	 * the restart table. Verify that this is the case.
+	 */
+	if (off >= args->block->restart_off) {
+		args->error = 1;
+		return -1;
+	}
+
+	/*
 	 * Records at restart points are stored without prefix compression, so
 	 * there is no need to fully decode the record key here. This removes
 	 * the need for allocating memory.
@@ -495,6 +526,10 @@ int block_iter_seek_key(struct block_iter *it, struct reftable_buf *want)
 	int err = 0;
 	size_t i;
 
+	err = reftable_record_init(&rec, reftable_block_type(it->block));
+	if (err < 0)
+		goto done;
+
 	/*
 	 * Perform a binary search over the block's restart points, which
 	 * avoids doing a linear scan over the whole block. Like this, we
@@ -536,10 +571,6 @@ int block_iter_seek_key(struct block_iter *it, struct reftable_buf *want)
 	else
 		it->next_off = it->block->header_off + 4;
 
-	err = reftable_record_init(&rec, reftable_block_type(it->block));
-	if (err < 0)
-		goto done;
-
 	/*
 	 * We're looking for the last entry less than the wanted key so that
 	 * the next call to `block_reader_next()` would yield the wanted
diff --git a/reftable/record.c b/reftable/record.c
index fcd387b..1fce441 100644
--- a/reftable/record.c
+++ b/reftable/record.c
@@ -388,7 +388,6 @@ static int reftable_ref_record_decode(void *rec, struct reftable_buf key,
 	r->refname[key.len] = 0;
 
 	r->update_index = update_index;
-	r->value_type = val_type;
 	switch (val_type) {
 	case REFTABLE_REF_VAL1:
 		if (in.len < hash_size) {
@@ -426,9 +425,10 @@ static int reftable_ref_record_decode(void *rec, struct reftable_buf key,
 	case REFTABLE_REF_DELETION:
 		break;
 	default:
-		abort();
-		break;
+		err = REFTABLE_FORMAT_ERROR;
+		goto done;
 	}
+	r->value_type = val_type;
 
 	return start.len - in.len;
 
diff --git a/reftable/reftable-stack.h b/reftable/reftable-stack.h
index 11f9963..5d22d84 100644
--- a/reftable/reftable-stack.h
+++ b/reftable/reftable-stack.h
@@ -42,6 +42,8 @@ struct reftable_stack_options {
 	 */
 	void (*on_reload)(void *payload);
 	void *on_reload_payload;
+
+	int suppress_deletions;
 };
 
 /* open a new reftable stack. The tables along with the table list will be
diff --git a/reftable/stack.c b/reftable/stack.c
index ab12926..308f957 100644
--- a/reftable/stack.c
+++ b/reftable/stack.c
@@ -171,7 +171,8 @@ void reftable_stack_destroy(struct reftable_stack *st)
 		st->merged = NULL;
 	}
 
-	err = read_lines(st->list_file, &names);
+	if (st->list_file)
+		err = read_lines(st->list_file, &names);
 	if (err < 0) {
 		REFTABLE_FREE_AND_NULL(names);
 	}
@@ -337,7 +338,7 @@ static int reftable_stack_reload_once(struct reftable_stack *st,
 	/* Update the stack to point to the new tables. */
 	if (st->merged)
 		reftable_merged_table_free(st->merged);
-	new_merged->suppress_deletions = 1;
+	new_merged->suppress_deletions = st->opts.suppress_deletions;
 	st->merged = new_merged;
 
 	if (st->tables)
diff --git a/reftable/table.c b/reftable/table.c
index 56362df..ea1e410 100644
--- a/reftable/table.c
+++ b/reftable/table.c
@@ -242,6 +242,8 @@ static int table_iter_seek_to(struct table_iter *ti, uint64_t off, uint8_t typ)
 	int err;
 
 	err = table_init_block(ti->table, &ti->block, off, typ);
+	if (err > 0)
+		return REFTABLE_FORMAT_ERROR;
 	if (err != 0)
 		return err;
 
@@ -560,6 +562,11 @@ int reftable_table_new(struct reftable_table **out,
 		goto done;
 	}
 
+	if (file_size < header_size(t->version) + footer_size(t->version)) {
+		err = REFTABLE_FORMAT_ERROR;
+		goto done;
+	}
+
 	t->size = file_size - footer_size(t->version);
 	t->source = *source;
 	t->name = reftable_strdup(name);
@@ -709,6 +716,10 @@ static int reftable_table_refs_for_unindexed(struct reftable_table *t,
 		if (ti)
 			table_iter_close(ti);
 		reftable_free(ti);
+		if (filter) {
+			reftable_buf_release(&filter->oid);
+			reftable_free(filter);
+		}
 	}
 	return err;
 }
diff --git a/reftable/writer.c b/reftable/writer.c
index f850e9d..d969a6a 100644
--- a/reftable/writer.c
+++ b/reftable/writer.c
@@ -151,10 +151,6 @@ int reftable_writer_new(struct reftable_writer **out,
 	struct reftable_write_options opts = {0};
 	struct reftable_writer *wp;
 
-	wp = reftable_calloc(1, sizeof(*wp));
-	if (!wp)
-		return REFTABLE_OUT_OF_MEMORY_ERROR;
-
 	if (_opts)
 		opts = *_opts;
 	options_set_defaults(&opts);
@@ -164,6 +160,10 @@ int reftable_writer_new(struct reftable_writer **out,
 	if (!hash_id)
 		hash_id = REFTABLE_HASH_SHA1;
 
+	wp = reftable_calloc(1, sizeof(*wp));
+	if (!wp)
+		return REFTABLE_OUT_OF_MEMORY_ERROR;
+
 	reftable_buf_init(&wp->block_writer_data.last_key);
 	reftable_buf_init(&wp->last_key);
 	reftable_buf_init(&wp->scratch);
diff --git a/remote-curl.c b/remote-curl.c
index 9e614c5..2c35dd5 100644
--- a/remote-curl.c
+++ b/remote-curl.c
@@ -1340,10 +1340,9 @@ static void parse_get(const char *arg)
 	fflush(stdout);
 }
 
-static int push_dav(int nr_spec, const char **specs)
+static int push_dav(const char **specs)
 {
 	struct child_process child = CHILD_PROCESS_INIT;
-	size_t i;
 
 	child.git_cmd = 1;
 	strvec_push(&child.args, "http-push");
@@ -1353,15 +1352,14 @@ static int push_dav(int nr_spec, const char **specs)
 	if (options.verbosity > 1)
 		strvec_push(&child.args, "--verbose");
 	strvec_push(&child.args, url.buf);
-	for (i = 0; i < nr_spec; i++)
-		strvec_push(&child.args, specs[i]);
+	strvec_pushv(&child.args, specs);
 
 	if (run_command(&child))
 		die(_("git-http-push failed"));
 	return 0;
 }
 
-static int push_git(struct discovery *heads, int nr_spec, const char **specs)
+static int push_git(struct discovery *heads, const char **specs)
 {
 	struct rpc_state rpc = RPC_STATE_INIT;
 	int i, err;
@@ -1400,8 +1398,8 @@ static int push_git(struct discovery *heads, int nr_spec, const char **specs)
 		strvec_push(&args, "--force-if-includes");
 
 	strvec_push(&args, "--stdin");
-	for (i = 0; i < nr_spec; i++)
-		packet_buf_write(&preamble, "%s\n", specs[i]);
+	for (; *specs; specs++)
+		packet_buf_write(&preamble, "%s\n", *specs);
 	packet_buf_flush(&preamble);
 
 	memset(&rpc, 0, sizeof(rpc));
@@ -1416,15 +1414,15 @@ static int push_git(struct discovery *heads, int nr_spec, const char **specs)
 	return err;
 }
 
-static int push(int nr_spec, const char **specs)
+static int push(const char **specs)
 {
 	struct discovery *heads = discover_refs("git-receive-pack", 1);
 	int ret;
 
 	if (heads->proto_git)
-		ret = push_git(heads, nr_spec, specs);
+		ret = push_git(heads, specs);
 	else
-		ret = push_dav(nr_spec, specs);
+		ret = push_dav(specs);
 	free_discovery(heads);
 	return ret;
 }
@@ -1448,7 +1446,7 @@ static void parse_push(struct strbuf *buf)
 			break;
 	} while (1);
 
-	ret = push(specs.nr, specs.v);
+	ret = push(specs.v);
 	printf("\n");
 	fflush(stdout);
 
diff --git a/remote.c b/remote.c
index e6c52c8..fe62068 100644
--- a/remote.c
+++ b/remote.c
@@ -150,8 +150,8 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->prune = -1;  /* unspecified */
 	ret->prune_tags = -1;  /* unspecified */
 	ret->name = xstrndup(name, len);
-	refspec_init_push(&ret->push);
-	refspec_init_fetch(&ret->fetch);
+	refspec_init_push(&ret->push, the_hash_algo);
+	refspec_init_fetch(&ret->fetch, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -309,8 +309,15 @@ static struct rewrite *make_rewrite(struct rewrites *r,
 
 static void rewrites_release(struct rewrites *r)
 {
-	for (int i = 0; i < r->rewrite_nr; i++)
-		free((char *)r->rewrite[i]->base);
+	for (int i = 0; i < r->rewrite_nr; i++) {
+		struct rewrite *rewrite = r->rewrite[i];
+
+		free((char *)rewrite->base);
+		for (int j = 0; j < rewrite->instead_of_nr; j++)
+			free((char *)rewrite->instead_of[j].s);
+		free(rewrite->instead_of);
+		free(rewrite);
+	}
 	free(r->rewrite);
 	memset(r, 0, sizeof(*r));
 }
@@ -954,6 +961,17 @@ struct strvec *push_url_of_remote(struct remote *remote)
 	return remote->pushurl.nr ? &remote->pushurl : &remote->url;
 }
 
+static bool remote_has_push_url(struct remote *remote, const char *url)
+{
+	const struct strvec *push_urls = push_url_of_remote(remote);
+
+	for (size_t i = 0; i < push_urls->nr; i++) {
+		if (!strcmp(push_urls->v[i], url))
+			return true;
+	}
+	return false;
+}
+
 void ref_push_report_free(struct ref_push_report *report)
 {
 	while (report) {
@@ -1821,17 +1839,25 @@ const char *repo_default_remote(struct repository *repo)
 
 const char *repo_remote_from_url(struct repository *repo, const char *url)
 {
+	char *rewritten_url;
+	const char *remote_name = NULL;
+
 	read_config(repo, 0);
+	if ((rewritten_url = alias_url(url, &repo->remote_state->rewrites)))
+		url = rewritten_url;
 
 	for (int i = 0; i < repo->remote_state->remotes_nr; i++) {
 		struct remote *remote = repo->remote_state->remotes[i];
 		if (!remote)
 			continue;
 
-		if (remote_has_url(remote, url))
-			return remote->name;
+		if (remote_has_url(remote, url)) {
+			remote_name = remote->name;
+			break;
+		}
 	}
-	return NULL;
+	free(rewritten_url);
+	return remote_name;
 }
 
 int branch_has_merge_config(struct branch *branch)
@@ -1887,12 +1913,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)
 	return branch->merge[0]->dst;
 }
 
-static char *tracking_for_push_dest(struct remote *remote,
+struct remote *repo_remote_for_push_tracking(struct repository *repo,
+					     struct remote *remote)
+{
+	const struct strvec *push_urls;
+	struct remote *first_match = NULL;
+	struct remote_state *remote_state = repo->remote_state;
+	const char *check_url;
+
+	if (remote->origin != REMOTE_UNCONFIGURED)
+		return remote;
+
+	push_urls = push_url_of_remote(remote);
+	if (push_urls->nr != 1)
+		return remote;
+	check_url = push_urls->v[0];
+
+	for (int i = 0; i < remote_state->remotes_nr; i++) {
+		struct remote *candidate = remote_state->remotes[i];
+
+		if (!candidate || candidate == remote ||
+		    !remote_is_configured(candidate, 0) ||
+		    !remote_has_push_url(candidate, check_url))
+			continue;
+		if (first_match)
+			return remote;
+		first_match = candidate;
+	}
+
+	return first_match ? first_match : remote;
+}
+
+static char *tracking_for_push_dest(struct repository *repo,
+				    struct remote *remote,
 				    const char *refname,
 				    struct strbuf *err)
 {
 	char *ret;
 
+	remote = repo_remote_for_push_tracking(repo, remote);
 	ret = apply_refspecs(&remote->fetch, refname);
 	if (!ret)
 		return error_buf(err,
@@ -1925,21 +1984,21 @@ static char *branch_get_push_1(struct repository *repo,
 					 _("push refspecs for '%s' do not include '%s'"),
 					 remote->name, branch->name);
 
-		ret = tracking_for_push_dest(remote, dst, err);
+		ret = tracking_for_push_dest(repo, remote, dst, err);
 		free(dst);
 		return ret;
 	}
 
 	if (remote->mirror)
-		return tracking_for_push_dest(remote, branch->refname, err);
+		return tracking_for_push_dest(repo, remote, branch->refname, err);
 
-	switch (push_default) {
+	switch (repo_config_values(repo)->push_default) {
 	case PUSH_DEFAULT_NOTHING:
 		return error_buf(err, _("push has no destination (push.default is 'nothing')"));
 
 	case PUSH_DEFAULT_MATCHING:
 	case PUSH_DEFAULT_CURRENT:
-		return tracking_for_push_dest(remote, branch->refname, err);
+		return tracking_for_push_dest(repo, remote, branch->refname, err);
 
 	case PUSH_DEFAULT_UPSTREAM:
 		return xstrdup_or_null(branch_get_upstream(branch, err));
@@ -1953,7 +2012,7 @@ static char *branch_get_push_1(struct repository *repo,
 			up = branch_get_upstream(branch, err);
 			if (!up)
 				return NULL;
-			cur = tracking_for_push_dest(remote, branch->refname, err);
+			cur = tracking_for_push_dest(repo, remote, branch->refname, err);
 			if (!cur)
 				return NULL;
 			if (strcmp(cur, up)) {
@@ -2713,6 +2772,8 @@ static int remote_tracking(struct remote *remote, const char *refname,
 {
 	char *dst;
 
+	if (!remote)
+		BUG("remote_tracking() called with NULL remote");
 	dst = apply_refspecs(&remote->fetch, refname);
 	if (!dst)
 		return -1; /* no tracking ref for refname at remote */
@@ -3039,7 +3100,7 @@ int valid_remote_name(const char *name)
 	int result;
 	struct strbuf refspec = STRBUF_INIT;
 	strbuf_addf(&refspec, "refs/heads/test:refs/remotes/%s/test", name);
-	result = valid_fetch_refspec(refspec.buf);
+	result = valid_fetch_refspec(refspec.buf, the_hash_algo);
 	strbuf_release(&refspec);
 	return result;
 }
diff --git a/remote.h b/remote.h
index 72a54d8..cca0203 100644
--- a/remote.h
+++ b/remote.h
@@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push);
 
 const char *repo_default_remote(struct repository *repo);
 const char *repo_remote_from_url(struct repository *repo, const char *url);
+struct remote *repo_remote_for_push_tracking(struct repository *repo,
+					     struct remote *remote);
 
 /* returns true if the given branch has merge configuration given. */
 int branch_has_merge_config(struct branch *branch);
diff --git a/repack-midx.c b/repack-midx.c
index 7c7c362..64c7f8d 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -954,7 +954,8 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
 					    lock_name.buf))
 		die_errno(_("unable to create leading directories of %s"),
 			  lock_name.buf);
-	hold_lock_file_for_update(&lf, lock_name.buf, LOCK_DIE_ON_ERROR);
+	repo_hold_lock_file_for_update(opts->existing->repo, &lf, lock_name.buf,
+				       LOCK_DIE_ON_ERROR);
 
 	if (!fdopen_lock_file(&lf, "w")) {
 		ret = error_errno(_("unable to open multi-pack-index chain file"));
diff --git a/replay.c b/replay.c
index da531d5..463c900 100644
--- a/replay.c
+++ b/replay.c
@@ -36,12 +36,16 @@ static struct commit *peel_committish(struct repository *repo,
 {
 	struct object *obj;
 	struct object_id oid;
+	struct commit *commit;
 
 	if (repo_get_oid(repo, name, &oid))
 		die(_("'%s' is not a valid commit-ish for %s"), name, mode);
 	obj = parse_object_or_die(repo, &oid, name);
-	return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
-						  OBJ_COMMIT);
+	commit = (struct commit *)repo_peel_to_type(repo, name, 0, obj,
+						    OBJ_COMMIT);
+	if (!commit)
+		die(_("'%s' does not point to a commit for %s"), name, mode);
+	return commit;
 }
 
 static char *get_author(const char *message)
@@ -351,10 +355,10 @@ void replay_result_release(struct replay_result *result)
 	free(result->updates);
 }
 
-static void replay_result_queue_update(struct replay_result *result,
-				       const char *refname,
-				       const struct object_id *old_oid,
-				       const struct object_id *new_oid)
+void replay_result_queue_update(struct replay_result *result,
+				const char *refname,
+				const struct object_id *old_oid,
+				const struct object_id *new_oid)
 {
 	ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
 	result->updates[result->updates_nr].refname = xstrdup(refname);
diff --git a/replay.h b/replay.h
index faf95c7..491db14 100644
--- a/replay.h
+++ b/replay.h
@@ -80,6 +80,11 @@ struct replay_result {
 
 void replay_result_release(struct replay_result *result);
 
+void replay_result_queue_update(struct replay_result *result,
+				const char *refname,
+				const struct object_id *old_oid,
+				const struct object_id *new_oid);
+
 /*
  * Replay a set of commits onto a new location. Leaves both the working tree,
  * index and references untouched. Reference updates caused by the replay will
diff --git a/repository.c b/repository.c
index 73d80bc..651b0f6 100644
--- a/repository.c
+++ b/repository.c
@@ -376,6 +376,7 @@ void repo_clear(struct repository *repo)
 
 	FREE_AND_NULL(repo->gitdir);
 	FREE_AND_NULL(repo->commondir);
+	FREE_AND_NULL(repo->prefix);
 	FREE_AND_NULL(repo->graft_file);
 	FREE_AND_NULL(repo->index_file);
 	FREE_AND_NULL(repo->worktree);
@@ -389,6 +390,7 @@ void repo_clear(struct repository *repo)
 	FREE_AND_NULL(repo->parsed_objects);
 
 	repo_settings_clear(repo);
+	repo_config_values_clear(&repo->config_values_private_);
 
 	if (repo->config) {
 		git_configset_clear(repo->config);
@@ -472,5 +474,5 @@ int repo_hold_locked_index(struct repository *repo,
 {
 	if (!repo->index_file)
 		BUG("the repo hasn't been setup");
-	return hold_lock_file_for_update(lf, repo->index_file, flags);
+	return repo_hold_lock_file_for_update(repo, lf, repo->index_file, flags);
 }
diff --git a/repository.h b/repository.h
index 7d649e3..3b467a2 100644
--- a/repository.h
+++ b/repository.h
@@ -53,6 +53,14 @@ struct repository {
 	char *commondir;
 
 	/*
+	 * The "prefix", a path to the current working directory relative to
+	 * the work tree root, or NULL, if the current working directory is not
+	 * a strict subdirectory of the work tree root. The prefix always ends
+	 * with a '/' character.
+	 */
+	char *prefix;
+
+	/*
 	 * Holds any information related to accessing the raw object content.
 	 */
 	struct object_database *objects;
@@ -159,7 +167,7 @@ struct repository {
 	/* Repository's compatibility hash algorithm. */
 	const struct git_hash_algo *compat_hash_algo;
 
-	/* Repository's config values parsed by git_default_config() */
+	/* Repository-specific configuration values. */
 	struct repo_config_values config_values_private_;
 
 	/* Repository's reference storage format, as serialized on disk. */
diff --git a/rerere.c b/rerere.c
index 8232542..0015e25 100644
--- a/rerere.c
+++ b/rerere.c
@@ -439,7 +439,7 @@ static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_siz
 	struct strbuf buf = STRBUF_INIT, out = STRBUF_INIT;
 	int has_conflicts = 0;
 	if (hash)
-		the_hash_algo->init_fn(&ctx);
+		git_hash_init(&ctx, the_hash_algo);
 
 	while (!io->getline(&buf, io)) {
 		if (is_cmarker(buf.buf, '<', marker_size)) {
@@ -756,7 +756,7 @@ static void do_rerere_one_path(struct index_state *istate,
 	/* Has the user resolved it already? */
 	if (variant >= 0) {
 		if (!handle_file(istate, path, NULL, NULL)) {
-			copy_file(rerere_path(&buf, id, "postimage"), path, 0666);
+			copy_file(the_repository, rerere_path(&buf, id, "postimage"), path, 0666);
 			id->collection->status[variant] |= RR_HAS_POSTIMAGE;
 			fprintf_ln(stderr, _("Recorded resolution for '%s'."), path);
 			free_rerere_id(rr_item);
@@ -911,9 +911,9 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)
 	if (flags & RERERE_READONLY)
 		fd = 0;
 	else
-		fd = hold_lock_file_for_update(&write_lock,
-					       git_path_merge_rr(r),
-					       LOCK_DIE_ON_ERROR);
+		fd = repo_hold_lock_file_for_update(r, &write_lock,
+						    git_path_merge_rr(r),
+						    LOCK_DIE_ON_ERROR);
 	read_rr(r, merge_rr);
 	return fd;
 }
diff --git a/reset.c b/reset.c
index 46e30e6..71254bd 100644
--- a/reset.c
+++ b/reset.c
@@ -1,5 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
-
 #include "git-compat-util.h"
 #include "cache-tree.h"
 #include "gettext.h"
@@ -13,13 +11,14 @@
 #include "unpack-trees.h"
 #include "hook.h"
 
-static int update_refs(const struct reset_head_opts *opts,
+static int update_refs(struct repository *repo,
+		       const struct reset_working_tree_options *opts,
 		       const struct object_id *oid,
 		       const struct object_id *head)
 {
-	unsigned detach_head = opts->flags & RESET_HEAD_DETACH;
-	unsigned run_hook = opts->flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
-	unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD;
+	unsigned detach_head = opts->flags & RESET_WORKING_TREE_DETACH;
+	unsigned run_hook = opts->flags & RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK;
+	unsigned update_orig_head = opts->flags & RESET_WORKING_TREE_UPDATE_ORIG_HEAD;
 	const struct object_id *orig_head = opts->orig_head;
 	const char *switch_to_branch = opts->branch;
 	const char *reflog_branch = opts->branch_msg;
@@ -42,19 +41,19 @@ static int update_refs(const struct reset_head_opts *opts,
 	prefix_len = msg.len;
 
 	if (update_orig_head) {
-		if (!repo_get_oid(the_repository, "ORIG_HEAD", &oid_old_orig))
+		if (!repo_get_oid(repo, "ORIG_HEAD", &oid_old_orig))
 			old_orig = &oid_old_orig;
 		if (head) {
 			if (!reflog_orig_head) {
 				strbuf_addstr(&msg, "updating ORIG_HEAD");
 				reflog_orig_head = msg.buf;
 			}
-			refs_update_ref(get_main_ref_store(the_repository),
+			refs_update_ref(get_main_ref_store(repo),
 					reflog_orig_head, "ORIG_HEAD",
 					orig_head ? orig_head : head,
 					old_orig, 0, UPDATE_REFS_MSG_ON_ERR);
 		} else if (old_orig)
-			refs_delete_ref(get_main_ref_store(the_repository),
+			refs_delete_ref(get_main_ref_store(repo),
 					NULL, "ORIG_HEAD", old_orig, 0);
 	}
 
@@ -64,40 +63,45 @@ static int update_refs(const struct reset_head_opts *opts,
 		reflog_head = msg.buf;
 	}
 	if (!switch_to_branch)
-		ret = refs_update_ref(get_main_ref_store(the_repository),
+		ret = refs_update_ref(get_main_ref_store(repo),
 				      reflog_head, "HEAD", oid, head,
 				      detach_head ? REF_NO_DEREF : 0,
 				      UPDATE_REFS_MSG_ON_ERR);
 	else {
-		ret = refs_update_ref(get_main_ref_store(the_repository),
+		ret = refs_update_ref(get_main_ref_store(repo),
 				      reflog_branch ? reflog_branch : reflog_head,
 				      switch_to_branch, oid, NULL, 0,
 				      UPDATE_REFS_MSG_ON_ERR);
 		if (!ret)
-			ret = refs_update_symref(get_main_ref_store(the_repository),
+			ret = refs_update_symref(get_main_ref_store(repo),
 						 "HEAD", switch_to_branch,
 						 reflog_head);
 	}
 	if (!ret && run_hook)
-		run_hooks_l(the_repository, "post-checkout",
-			    oid_to_hex(head ? head : null_oid(the_hash_algo)),
+		run_hooks_l(repo, "post-checkout",
+			    oid_to_hex(head ? head : null_oid(repo->hash_algo)),
 			    oid_to_hex(oid), "1", NULL);
 	strbuf_release(&msg);
 	return ret;
 }
 
-int reset_head(struct repository *r, const struct reset_head_opts *opts)
+int reset_working_tree(struct repository *r,
+		       const struct reset_working_tree_options *opts)
 {
 	const struct object_id *oid = opts->oid;
 	const char *switch_to_branch = opts->branch;
-	unsigned reset_hard = opts->flags & RESET_HEAD_HARD;
-	unsigned refs_only = opts->flags & RESET_HEAD_REFS_ONLY;
-	unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD;
+	unsigned reset_hard = opts->flags & RESET_WORKING_TREE_HARD;
+	unsigned refs_only = opts->flags & RESET_WORKING_TREE_REFS_ONLY;
+	unsigned update_head = opts->flags & RESET_WORKING_TREE_UPDATE_HEAD;
+	unsigned update_orig_head = opts->flags & RESET_WORKING_TREE_UPDATE_ORIG_HEAD;
+	unsigned dry_run = opts->flags & RESET_WORKING_TREE_DRY_RUN;
 	struct object_id *head = NULL, head_oid;
 	struct tree_desc desc[2] = { { NULL }, { NULL } };
 	struct lock_file lock = LOCK_INIT;
 	struct unpack_trees_options unpack_tree_opts = { 0 };
 	struct tree *tree;
+	struct index_state scratch_index = INDEX_STATE_INIT(r);
+	struct index_state *istate;
 	const char *action;
 	int ret = 0, nr = 0;
 
@@ -110,12 +114,18 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
 	if (opts->branch_msg && !opts->branch)
 		BUG("branch reflog message given without a branch");
 
-	if (!refs_only && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) {
+	if (update_orig_head && !update_head)
+		BUG("cannot update ORIG_HEAD without updating HEAD");
+
+	if (!refs_only && !dry_run && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) {
 		ret = -1;
 		goto leave_reset_head;
 	}
 
-	if (!repo_get_oid(r, "HEAD", &head_oid)) {
+	if (opts->oid_from) {
+		oidcpy(&head_oid, opts->oid_from);
+		head = &head_oid;
+	} else if (!repo_get_oid(r, "HEAD", &head_oid)) {
 		head = &head_oid;
 	} else if (!oid || !reset_hard) {
 		ret = error(_("could not determine HEAD revision"));
@@ -125,26 +135,42 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
 	if (!oid)
 		oid = &head_oid;
 
-	if (refs_only)
-		return update_refs(opts, oid, head);
+	if (refs_only) {
+		if (!dry_run && update_head)
+			return update_refs(r, opts, oid, head);
+		return 0;
+	}
+
+	if (dry_run) {
+		if (read_index_from(&scratch_index, r->index_file, r->gitdir) < 0 ||
+		    index_state_unmerged_to_stage0(&scratch_index) < 0) {
+			ret = error(_("could not read index"));
+			goto leave_reset_head;
+		}
+
+		istate = &scratch_index;
+	} else {
+		if (repo_read_index_unmerged(r) < 0) {
+			ret = error(_("could not read index"));
+			goto leave_reset_head;
+		}
+		istate = r->index;
+	}
 
 	action = reset_hard ? "reset" : "checkout";
 	setup_unpack_trees_porcelain(&unpack_tree_opts, action);
 	unpack_tree_opts.head_idx = 1;
-	unpack_tree_opts.src_index = r->index;
-	unpack_tree_opts.dst_index = r->index;
+	unpack_tree_opts.src_index = istate;
+	unpack_tree_opts.dst_index = istate;
 	unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge;
-	unpack_tree_opts.update = 1;
+	unpack_tree_opts.update = !dry_run;
+	unpack_tree_opts.dry_run = dry_run;
 	unpack_tree_opts.merge = 1;
 	unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
-	unpack_tree_opts.skip_cache_tree_update = 1;
 	init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL);
-	if (reset_hard)
+	if (reset_hard) {
+		unpack_tree_opts.skip_cache_tree_update = 1;
 		unpack_tree_opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
-
-	if (repo_read_index_unmerged(r) < 0) {
-		ret = error(_("could not read index"));
-		goto leave_reset_head;
 	}
 
 	if (!reset_hard && !fill_tree_descriptor(r, &desc[nr++], &head_oid)) {
@@ -163,25 +189,31 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
 		goto leave_reset_head;
 	}
 
-	tree = repo_parse_tree_indirect(the_repository, oid);
+	if (dry_run)
+		goto leave_reset_head;
+
+	tree = repo_parse_tree_indirect(r, oid);
 	if (!tree) {
 		ret = error(_("unable to read tree (%s)"), oid_to_hex(oid));
 		goto leave_reset_head;
 	}
 
-	prime_cache_tree(r, r->index, tree);
+	if (reset_hard)
+		prime_cache_tree(r, r->index, tree);
 
 	if (write_locked_index(r->index, &lock, COMMIT_LOCK) < 0) {
 		ret = error(_("could not write index"));
 		goto leave_reset_head;
 	}
 
-	if (oid != &head_oid || update_orig_head || switch_to_branch)
-		ret = update_refs(opts, oid, head);
+	if (update_head &&
+	    (oid != &head_oid || update_orig_head || switch_to_branch))
+		ret = update_refs(r, opts, oid, head);
 
 leave_reset_head:
 	rollback_lock_file(&lock);
 	clear_unpack_trees_porcelain(&unpack_tree_opts);
+	release_index(&scratch_index);
 	while (nr)
 		free((void *)desc[--nr].buffer);
 	return ret;
diff --git a/reset.h b/reset.h
index a28f818..4c992ba 100644
--- a/reset.h
+++ b/reset.h
@@ -6,23 +6,43 @@
 
 #define GIT_REFLOG_ACTION_ENVIRONMENT "GIT_REFLOG_ACTION"
 
-/* Request a detached checkout */
-#define RESET_HEAD_DETACH (1<<0)
-/* Request a reset rather than a checkout */
-#define RESET_HEAD_HARD (1<<1)
-/* Run the post-checkout hook */
-#define RESET_HEAD_RUN_POST_CHECKOUT_HOOK (1<<2)
-/* Only update refs, do not touch the worktree */
-#define RESET_HEAD_REFS_ONLY (1<<3)
-/* Update ORIG_HEAD as well as HEAD */
-#define RESET_ORIG_HEAD (1<<4)
+enum reset_working_tree_flags {
+	/* Request a detached checkout */
+	RESET_WORKING_TREE_DETACH = (1 << 0),
 
-struct reset_head_opts {
+	/* Request a reset rather than a checkout */
+	RESET_WORKING_TREE_HARD = (1 << 1),
+
+	/* Run the post-checkout hook */
+	RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK = (1 << 2),
+
+	/* Only update refs, do not touch the worktree */
+	RESET_WORKING_TREE_REFS_ONLY = (1 << 3),
+
+	/* Update HEAD */
+	RESET_WORKING_TREE_UPDATE_HEAD = (1 << 4),
+
+	/* Update ORIG_HEAD */
+	RESET_WORKING_TREE_UPDATE_ORIG_HEAD = (1 << 5),
+
+	/*
+	 * Perform a dry-run by performing the operation without updating
+	 * any user-visible state.
+	 */
+	RESET_WORKING_TREE_DRY_RUN = (1 << 6),
+};
+
+struct reset_working_tree_options {
 	/*
 	 * The commit to checkout/reset to. Defaults to HEAD.
 	 */
 	const struct object_id *oid;
 	/*
+	 * The commit to checkout/reset from when doing a two-way merge. This
+	 * is used as one of the sides to merge.
+	 */
+	const struct object_id *oid_from;
+	/*
 	 * Optional value to set ORIG_HEAD. Defaults to HEAD.
 	 */
 	const struct object_id *orig_head;
@@ -33,7 +53,7 @@ struct reset_head_opts {
 	/*
 	 * Flags defined above.
 	 */
-	unsigned flags;
+	enum reset_working_tree_flags flags;
 	/*
 	 * Optional reflog message for branch, defaults to head_msg.
 	 */
@@ -45,7 +65,8 @@ struct reset_head_opts {
 	const char *head_msg;
 	/*
 	 * Optional reflog message for ORIG_HEAD, if this omitted and flags
-	 * contains RESET_ORIG_HEAD then default_reflog_action must be given.
+	 * contains RESET_WORKING_TREE_UPDATE_ORIG_HEAD then
+	 * default_reflog_action must be given.
 	 */
 	const char *orig_head_msg;
 	/*
@@ -55,6 +76,6 @@ struct reset_head_opts {
 	const char *default_reflog_action;
 };
 
-int reset_head(struct repository *r, const struct reset_head_opts *opts);
+int reset_working_tree(struct repository *r, const struct reset_working_tree_options *opts);
 
 #endif
diff --git a/revision.c b/revision.c
index 0c95ede..526bcf3 100644
--- a/revision.c
+++ b/revision.c
@@ -477,16 +477,15 @@ static struct commit *handle_commit(struct rev_info *revs,
 static int everybody_uninteresting(struct prio_queue *orig,
 				   struct commit **interesting_cache)
 {
-	size_t i;
+	struct commit *commit;
 
 	if (*interesting_cache) {
-		struct commit *commit = *interesting_cache;
+		commit = *interesting_cache;
 		if (!(commit->object.flags & UNINTERESTING))
 			return 0;
 	}
 
-	for (i = 0; i < orig->nr; i++) {
-		struct commit *commit = orig->array[i].data;
+	prio_queue_for_each(orig, commit) {
 		if (commit->object.flags & UNINTERESTING)
 			continue;
 
@@ -708,6 +707,8 @@ static int convert_pathspec_to_bloom_keyvec(struct bloom_keyvec **out,
 
 static void prepare_to_use_bloom_filter(struct rev_info *revs)
 {
+	release_revisions_bloom_keyvecs(revs);
+
 	if (!revs->commits)
 		return;
 
@@ -1152,12 +1153,18 @@ static int process_parents(struct rev_info *revs, struct commit *commit,
 			if (p)
 				p->object.flags |= UNINTERESTING |
 						   CHILD_VISITED;
-			if (repo_parse_commit_gently(revs->repo, p, 1) < 0)
+			if (repo_parse_commit_gently(revs->repo, p, 1) < 0) {
+				if (revs->exclude_first_parent_only)
+					break;
 				continue;
+			}
 			if (p->parents)
 				mark_parents_uninteresting(revs, p);
-			if (p->object.flags & SEEN)
+			if (p->object.flags & SEEN) {
+				if (revs->exclude_first_parent_only)
+					break;
 				continue;
+			}
 			p->object.flags |= (SEEN | NOT_USER_GIVEN);
 			if (queue)
 				prio_queue_put(queue, p);
@@ -1443,7 +1450,7 @@ static int limit_list(struct rev_info *revs)
 	struct commit_list *original_list = revs->commits;
 	struct commit_list *newlist = NULL;
 	struct commit_list **p = &newlist;
-	struct commit *interesting_cache = NULL;
+	struct commit *commit, *interesting_cache = NULL;
 	struct prio_queue queue = { .compare = compare_commits_by_commit_date };
 
 	if (revs->ancestry_path_implicit_bottoms) {
@@ -1458,8 +1465,7 @@ static int limit_list(struct rev_info *revs)
 		prio_queue_put(&queue, commit);
 	}
 
-	while (queue.nr) {
-		struct commit *commit = prio_queue_get(&queue);
+	while ((commit = prio_queue_get(&queue))) {
 		struct object *obj = &commit->object;
 
 		if (commit == interesting_cache)
@@ -1711,7 +1717,7 @@ static void add_other_reflogs_to_pending(struct all_refs_cb *cb)
 {
 	struct worktree **worktrees, **p;
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	for (p = worktrees; *p; p++) {
 		struct worktree *wt = *p;
 
@@ -1837,7 +1843,7 @@ void add_index_objects_to_pending(struct rev_info *revs, unsigned int flags)
 	if (revs->single_worktree)
 		return;
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(the_repository);
 	for (p = worktrees; *p; p++) {
 		struct worktree *wt = *p;
 		struct index_state istate = INDEX_STATE_INIT(revs->repo);
@@ -1904,8 +1910,13 @@ static int add_parents_only(struct rev_info *revs, const char *arg_, int flags,
 		return 0;
 	while (1) {
 		it = get_reference(revs, arg, &oid, 0);
-		if (!it && revs->ignore_missing)
-			return 0;
+		if (!it) {
+			if (revs->ignore_missing)
+				return 0;
+			if (revs->do_not_die_on_missing_objects)
+				return 0;
+			return -1;
+		}
 		if (it->type != OBJ_TAG)
 			break;
 		if (!((struct tag*)it)->tagged)
@@ -2627,6 +2638,12 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 		revs->graph = NULL;
 	} else if (skip_prefix(arg, "--graph-lane-limit=", &optarg)) {
 		revs->graph_max_lanes = parse_count(optarg);
+	} else if (!strcmp(arg, "--graph-indent")) {
+		revs->no_graph_indent = 0;
+		revs->graph_indent_set = 1;
+	} else if (!strcmp(arg, "--no-graph-indent")) {
+		revs->no_graph_indent = 1;
+		revs->graph_indent_set = 1;
 	} else if (!strcmp(arg, "--encode-email-headers")) {
 		revs->encode_email_headers = 1;
 	} else if (!strcmp(arg, "--no-encode-email-headers")) {
@@ -2813,7 +2830,7 @@ static int handle_revision_pseudo_opt(struct rev_info *revs,
 			struct all_refs_cb cb;
 
 			init_all_refs_cb(&cb, revs, *flags);
-			other_head_refs(handle_one_ref, &cb);
+			other_head_refs(the_repository, handle_one_ref, &cb);
 		}
 		clear_ref_exclusions(&revs->ref_excludes);
 	} else if (!strcmp(arg, "--branches")) {
@@ -3201,6 +3218,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 	if (revs->graph_max_lanes > 0 && !revs->graph)
 		die(_("the option '%s' requires '%s'"), "--graph-lane-limit", "--graph");
 
+	if (revs->graph_indent_set && !revs->graph)
+		die(_("the option '%s' requires '%s'"), "--[no-]graph-indent", "--graph");
+
 	if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)
 		die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs");
 
@@ -4058,8 +4078,9 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs,
 static void merge_queue_into_prio_queue(struct prio_queue *from,
 					struct prio_queue *to)
 {
-	while (from->nr)
-		prio_queue_put(to, prio_queue_get(from));
+	struct commit *item;
+	while ((item = prio_queue_get(from)))
+		prio_queue_put(to, item);
 }
 
 static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp)
@@ -4418,6 +4439,7 @@ static struct commit *get_revision_1(struct rev_info *revs)
 
 		switch (mode) {
 		case REV_WALK_REFLOG:
+		case REV_WALK_NO_WALK:
 			try_to_simplify_commit(revs, commit);
 			break;
 		case REV_WALK_TOPO:
@@ -4431,7 +4453,6 @@ static struct commit *get_revision_1(struct rev_info *revs)
 					    oid_to_hex(&commit->object.oid));
 			}
 			break;
-		case REV_WALK_NO_WALK:
 		case REV_WALK_LIMITED:
 			break;
 		}
@@ -4658,12 +4679,34 @@ static void retrieve_oldest_commits(struct rev_info *revs,
 		commit_list_insert(c, queue);
 }
 
+/*
+ * Returns the next commit that will be shown, regardless of whether it comes
+ * directly from the revision walk or from the list saved by the staged output
+ * of --max-count-oldest.
+ */
+static struct commit *next_commit_to_show(struct rev_info *revs)
+{
+	struct commit *c;
+	struct commit_list *p;
+
+	if (!revs->max_count_stage)
+		return get_revision_internal(revs);
+
+	c = pop_commit(&revs->commits);
+	if (c) {
+		c->object.flags |= SHOWN;
+		if (!(c->object.flags & BOUNDARY))
+			for (p = c->parents; p; p = p->next)
+				p->item->object.flags |= CHILD_SHOWN;
+	}
+	return c;
+}
+
 struct commit *get_revision(struct rev_info *revs)
 {
 	struct commit *c;
 	struct commit_list *reversed;
 	struct commit_list *queue = NULL;
-	struct commit_list *p;
 
 	if (revs->max_count_type == 1 && !revs->max_count_stage) {
 		retrieve_oldest_commits(revs, &queue);
@@ -4693,20 +4736,24 @@ struct commit *get_revision(struct rev_info *revs)
 		return c;
 	}
 
-	if (revs->max_count_stage) {
-		c = pop_commit(&revs->commits);
-		if (c) {
-			c->object.flags |= SHOWN;
-			if (!(c->object.flags & BOUNDARY))
-				for (p = c->parents; p; p = p->next)
-					p->item->object.flags |= CHILD_SHOWN;
-		}
+	if (revs->graph) {
+		c = graph_pop_lookahead(revs->graph);
+		if (!c)
+			c = next_commit_to_show(revs);
 	} else {
-		c = get_revision_internal(revs);
+		c = next_commit_to_show(revs);
 	}
 
-	if (c && revs->graph)
+	if (c && revs->graph) {
+		while (graph_get_lookahead_room(revs->graph)) {
+			struct commit *next = next_commit_to_show(revs);
+			if (!next)
+				break;
+			graph_push_lookahead(revs->graph, next);
+		}
 		graph_update(revs->graph, c);
+	}
+
 	if (!c) {
 		free_saved_parents(revs);
 		commit_list_free(revs->previous_parents);
diff --git a/revision.h b/revision.h
index 569b3fa..acf6d06 100644
--- a/revision.h
+++ b/revision.h
@@ -314,6 +314,8 @@ struct rev_info {
 	/* Display history graph */
 	struct git_graph *graph;
 	int graph_max_lanes;
+	unsigned int no_graph_indent:1;
+	unsigned int graph_indent_set:1;
 
 	/* special limits */
 	int skip_count;
diff --git a/run-command.c b/run-command.c
index e70a8a3..ce84db8 100644
--- a/run-command.c
+++ b/run-command.c
@@ -706,7 +706,7 @@ int start_command(struct child_process *cmd)
 			failed_errno = errno;
 			if (need_in)
 				close_pair(fdin);
-			else if (cmd->in)
+			else if (cmd->in > 0)
 				close(cmd->in);
 			str = "standard output";
 			goto fail_pipe;
@@ -720,11 +720,11 @@ int start_command(struct child_process *cmd)
 			failed_errno = errno;
 			if (need_in)
 				close_pair(fdin);
-			else if (cmd->in)
+			else if (cmd->in > 0)
 				close(cmd->in);
 			if (need_out)
 				close_pair(fdout);
-			else if (cmd->out)
+			else if (cmd->out > 0)
 				close(cmd->out);
 			str = "standard error";
 fail_pipe:
diff --git a/sequencer.c b/sequencer.c
index 0fe8fed..83c3849 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1926,6 +1926,13 @@ static int seen_squash(struct replay_ctx *ctx)
 		strstr(ctx->current_fixups.buf, "\nsquash");
 }
 
+/* Does the current fixup chain contain a "fixup -c" command? */
+static int seen_fixup_edit_msg(struct replay_ctx *ctx)
+{
+	return starts_with(ctx->current_fixups.buf, "fixup -c") ||
+		strstr(ctx->current_fixups.buf, "\nfixup -c");
+}
+
 static void update_comment_bufs(struct strbuf *buf1, struct strbuf *buf2, int n)
 {
 	strbuf_setlen(buf1, strlen(comment_line_str) + 1);
@@ -2148,9 +2155,14 @@ static int update_squash_messages(struct repository *r,
 	strbuf_release(&buf);
 
 	if (!res) {
-		strbuf_addf(&ctx->current_fixups, "%s%s %s",
+		const char *fixup_flag = "";
+
+		if (is_fixup_flag(command, flag) && (flag & TODO_EDIT_FIXUP_MSG))
+			fixup_flag = " -c";
+
+		strbuf_addf(&ctx->current_fixups, "%s%s%s %s",
 			    ctx->current_fixups.len ? "\n" : "",
-			    command_to_string(command),
+			    command_to_string(command), fixup_flag,
 			    oid_to_hex(&commit->object.oid));
 		res = write_message(ctx->current_fixups.buf,
 				    ctx->current_fixups.len,
@@ -2260,10 +2272,17 @@ static const char *reflog_message(struct replay_opts *opts,
 	return buf.buf;
 }
 
-static int do_pick_commit(struct repository *r,
-			  struct todo_item *item,
-			  struct replay_opts *opts,
-			  int final_fixup, int *check_todo)
+enum pick_result {
+	PICK_RESULT_ERROR = -1,
+	PICK_RESULT_OK,
+	PICK_RESULT_CONFLICTS,
+	PICK_RESULT_DROPPED,
+};
+
+static enum pick_result do_pick_commit(struct repository *r,
+				       struct todo_item *item,
+				       struct replay_opts *opts,
+				       int final_fixup, int *check_todo)
 {
 	struct replay_ctx *ctx = opts->ctx;
 	unsigned int flags = should_edit(opts) ? EDIT_MSG : 0;
@@ -2273,7 +2292,7 @@ static int do_pick_commit(struct repository *r,
 	const char *base_label, *next_label, *reflog_action;
 	char *author = NULL;
 	struct commit_message msg = { NULL, NULL, NULL, NULL };
-	int res, unborn = 0, reword = 0, allow, drop_commit;
+	int res, unborn = 0, reword = 0, allow, drop_commit = 0;
 	enum todo_command command = item->command;
 	struct commit *commit = item->commit;
 
@@ -2419,7 +2438,7 @@ static int do_pick_commit(struct repository *r,
 		} else {
 			const char *dest = git_path_squash_msg(r);
 			unlink(dest);
-			if (copy_file(dest, rebase_path_squash_msg(), 0666)) {
+			if (copy_file(r, dest, rebase_path_squash_msg(), 0666)) {
 				res = error(_("could not copy '%s' to '%s'"),
 					    rebase_path_squash_msg(), dest);
 				goto leave;
@@ -2453,14 +2472,25 @@ static int do_pick_commit(struct repository *r,
 		struct commit_list *common = NULL;
 		struct commit_list *remotes = NULL;
 
-		res = write_message(ctx->message.buf, ctx->message.len,
-				    git_path_merge_msg(r), 0);
+		if (write_message(ctx->message.buf, ctx->message.len,
+				  git_path_merge_msg(r), 0)) {
+			res = -1;
+			goto leave;
+		}
 
 		commit_list_insert(base, &common);
 		commit_list_insert(next, &remotes);
-		res |= try_merge_command(r, opts->strategy,
-					 opts->xopts.nr, opts->xopts.v,
+		res = try_merge_command(r, opts->strategy,
+					opts->xopts.nr, opts->xopts.v,
 					common, oid_to_hex(&head), remotes);
+		/*
+		 * If there were conflicts, try_merge_command() returns 1,
+		 * any other no-zero return code means that either the merge
+		 * command could not be run, or it failed to merge.
+		 */
+		if (res && res != 1)
+			res = -1;
+
 		commit_list_free(common);
 		commit_list_free(remotes);
 	}
@@ -2492,7 +2522,6 @@ static int do_pick_commit(struct repository *r,
 		goto leave;
 	}
 
-	drop_commit = 0;
 	allow = allow_empty(r, opts, commit);
 	if (allow < 0) {
 		res = allow;
@@ -2531,6 +2560,12 @@ static int do_pick_commit(struct repository *r,
 			res = run_git_commit(NULL, reflog_action, opts, flags);
 			*check_todo = 1;
 		}
+		/*
+		 * If "git commit" failed to run then res == -1, but we don't
+		 * want reschedule the last command because the picking the
+		 * commit was successful.
+		 */
+		res = !!res;
 	}
 
 
@@ -2547,7 +2582,14 @@ static int do_pick_commit(struct repository *r,
 	free(author);
 	update_abort_safety_file();
 
-	return res;
+	if (res < 0)
+		return PICK_RESULT_ERROR;
+	else if (res > 0)
+		return PICK_RESULT_CONFLICTS;
+	else if (drop_commit)
+		return PICK_RESULT_DROPPED;
+	else
+		return PICK_RESULT_OK;
 }
 
 static int prepare_revs(struct replay_opts *opts)
@@ -3281,7 +3323,13 @@ static int read_populate_opts(struct replay_opts *opts)
 			const char *p = ctx->current_fixups.buf;
 			ctx->current_fixup_count = 1;
 			while ((p = strchr(p, '\n'))) {
-				ctx->current_fixup_count++;
+				/*
+				 * Older versions of git accidentally
+				 * inserted blank lines when a fixup
+				 * was skipped.
+				 */
+				if (p[1] && p[1] != '\n')
+					ctx->current_fixup_count++;
 				p++;
 			}
 		}
@@ -3864,15 +3912,15 @@ static int error_failed_squash(struct repository *r,
 			       int subject_len,
 			       const char *subject)
 {
-	if (copy_file(rebase_path_message(), rebase_path_squash_msg(), 0666))
+	if (copy_file(r, rebase_path_message(), rebase_path_squash_msg(), 0666))
 		return error(_("could not copy '%s' to '%s'"),
 			rebase_path_squash_msg(), rebase_path_message());
 	unlink(git_path_merge_msg(r));
-	if (copy_file(git_path_merge_msg(r), rebase_path_message(), 0666))
+	if (copy_file(r, git_path_merge_msg(r), rebase_path_message(), 0666))
 		return error(_("could not copy '%s' to '%s'"),
 			     rebase_path_message(),
 			     git_path_merge_msg(r));
-	return error_with_patch(r, commit, subject, subject_len, opts, 1, 0);
+	return error_with_patch(r, commit, subject, subject_len, opts, 1, 1);
 }
 
 static int do_exec(struct repository *r, const char *command_line, int quiet)
@@ -4692,7 +4740,10 @@ static void create_autostash_internal(struct repository *r,
 	if (has_unstaged_changes(r, 1) ||
 	    has_uncommitted_changes(r, 1)) {
 		struct child_process stash = CHILD_PROCESS_INIT;
-		struct reset_head_opts ropts = { .flags = RESET_HEAD_HARD };
+		struct reset_working_tree_options ropts = {
+			.flags = RESET_WORKING_TREE_HARD |
+				 RESET_WORKING_TREE_UPDATE_HEAD,
+		};
 		struct object_id oid;
 
 		strvec_pushl(&stash.args,
@@ -4722,7 +4773,7 @@ static void create_autostash_internal(struct repository *r,
 
 		if (!silent)
 			printf(_("Created autostash: %s\n"), buf.buf);
-		if (reset_head(r, &ropts) < 0)
+		if (reset_working_tree(r, &ropts) < 0)
 			die(_("could not reset --hard"));
 		discard_index(r->index);
 		if (repo_read_index(r) < 0)
@@ -4882,16 +4933,18 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
 			 const char *onto_name, const struct object_id *onto,
 			 const struct object_id *orig_head)
 {
-	struct reset_head_opts ropts = {
+	struct reset_working_tree_options ropts = {
 		.oid = onto,
 		.orig_head = orig_head,
-		.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD |
-				RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
+		.flags = RESET_WORKING_TREE_DETACH |
+			 RESET_WORKING_TREE_UPDATE_HEAD |
+			 RESET_WORKING_TREE_UPDATE_ORIG_HEAD |
+			 RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK,
 		.head_msg = reflog_message(opts, "start", "checkout %s",
 					   onto_name),
 		.default_reflog_action = sequencer_reflog_action(opts)
 	};
-	if (reset_head(r, &ropts)) {
+	if (reset_working_tree(r, &ropts)) {
 		apply_autostash(rebase_path_autostash());
 		sequencer_remove_state(opts);
 		return error(_("could not detach HEAD"));
@@ -4958,37 +5011,59 @@ static int pick_one_commit(struct repository *r,
 			   struct replay_opts *opts,
 			   int *check_todo, int* reschedule)
 {
-	int res;
+	enum pick_result pick_res;
 	struct todo_item *item = todo_list->items + todo_list->current;
 	const char *arg = todo_item_get_arg(todo_list, item);
 
-	res = do_pick_commit(r, item, opts, is_final_fixup(todo_list),
-			     check_todo);
-	if (is_rebase_i(opts) && res < 0) {
+	pick_res = do_pick_commit(r, item, opts, is_final_fixup(todo_list),
+				  check_todo);
+	if (!is_rebase_i(opts))
+		switch (pick_res) {
+		case PICK_RESULT_ERROR:
+			return -1;
+		case PICK_RESULT_CONFLICTS:
+			return 1;
+		default:
+			return 0;
+		}
+
+	if (pick_res == PICK_RESULT_ERROR) {
 		/* Reschedule */
 		*reschedule = 1;
 		return -1;
-	}
-	if (item->command == TODO_EDIT) {
+	} else if (item->command == TODO_EDIT) {
 		struct commit *commit = item->commit;
-		if (!res) {
+		int res = pick_res == PICK_RESULT_CONFLICTS;
+		int to_amend = pick_res != PICK_RESULT_CONFLICTS &&
+				pick_res != PICK_RESULT_DROPPED;
+
+		/*
+		 * NEEDSWORK: Do not record the commit as rewritten when
+		 * continuing if it was dropped. Does it even make sense
+		 * to stop if the commit was dropped?
+		 */
+		if (pick_res == PICK_RESULT_OK ||
+		    pick_res == PICK_RESULT_DROPPED) {
 			if (!opts->verbose)
 				term_clear_line();
 			fprintf(stderr, _("Stopped at %s...  %.*s\n"),
 				short_commit_name(r, commit), item->arg_len, arg);
 		}
-		return error_with_patch(r, commit,
-					arg, item->arg_len, opts, res, !res);
-	}
-	if (is_rebase_i(opts) && !res)
+		return error_with_patch(r, commit, arg, item->arg_len, opts,
+					res, to_amend);
+	} else if (pick_res == PICK_RESULT_OK) {
 		record_in_rewritten(&item->commit->object.oid,
 				    peek_command(todo_list, 1));
-	if (res && is_fixup(item->command)) {
-		if (res == 1)
-			intend_to_amend();
+		return 0;
+	} else if (pick_res == PICK_RESULT_DROPPED) {
+		if (is_final_fixup(todo_list))
+			flush_rewritten_pending();
+		return 0;
+	} else if (pick_res == PICK_RESULT_CONFLICTS &&
+		   is_fixup(item->command)) {
 		return error_failed_squash(r, item->commit, opts,
 					   item->arg_len, arg);
-	} else if (res && is_rebase_i(opts) && item->commit) {
+	} else if (pick_res == PICK_RESULT_CONFLICTS) {
 		int to_amend = 0;
 		struct object_id oid;
 
@@ -5005,11 +5080,11 @@ static int pick_one_commit(struct repository *r,
 		      oideq(&opts->squash_onto, &oid))))
 			to_amend = 1;
 
-		return res | error_with_patch(r, item->commit,
-					      arg, item->arg_len, opts,
-					      res, to_amend);
+		return error_with_patch(r, item->commit, arg, item->arg_len,
+					opts, 1, to_amend);
 	}
-	return res;
+
+	BUG("Unhandled return value from do_pick_commit()");
 }
 
 static int pick_commits(struct repository *r,
@@ -5349,6 +5424,9 @@ static int commit_staged_changes(struct repository *r,
 				BUG("Incorrect current_fixups:\n%s", p);
 			while (len && p[len - 1] != '\n')
 				len--;
+			/* Remove trailing newline */
+			if (len)
+				len--;
 			strbuf_setlen(&ctx->current_fixups, len);
 			if (write_message(p, len, rebase_path_current_fixups(),
 					  0) < 0) {
@@ -5377,8 +5455,8 @@ static int commit_staged_changes(struct repository *r,
 				 * message, no need to bother the user with
 				 * opening the commit message in the editor.
 				 */
-				if (!starts_with(p, "squash ") &&
-				    !strstr(p, "\nsquash "))
+				if (!seen_squash(ctx) &&
+				    !seen_fixup_edit_msg(ctx))
 					flags = (flags & ~EDIT_MSG) | CLEANUP_MSG;
 			} else if (is_fixup(peek_command(todo_list, 0))) {
 				/*
@@ -5545,7 +5623,15 @@ static int single_pick(struct repository *r,
 			TODO_PICK : TODO_REVERT;
 	item.commit = cmit;
 
-	return do_pick_commit(r, &item, opts, 0, &check_todo);
+	switch (do_pick_commit(r, &item, opts, 0, &check_todo)) {
+	case PICK_RESULT_ERROR:
+		return -1;
+	case PICK_RESULT_CONFLICTS:
+		return 1;
+	default:
+		return 0;
+	}
+
 }
 
 int sequencer_pick_revisions(struct repository *r,
diff --git a/serve.c b/serve.c
index 49a6e39..2b07d92 100644
--- a/serve.c
+++ b/serve.c
@@ -89,7 +89,7 @@ static void session_id_receive(struct repository *r UNUSED,
 	trace2_data_string("transfer", NULL, "client-sid", client_sid);
 }
 
-static int object_info_advertise(struct repository *r, struct strbuf *value UNUSED)
+static int object_info_advertise(struct repository *r, struct strbuf *value)
 {
 	if (advertise_object_info == -1 &&
 	    repo_config_get_bool(r, "transfer.advertiseobjectinfo",
@@ -97,6 +97,9 @@ static int object_info_advertise(struct repository *r, struct strbuf *value UNUS
 		/* disabled by default */
 		advertise_object_info = 0;
 	}
+	/* Currently only size is supported */
+	if (value && advertise_object_info)
+		strbuf_addstr(value, "size");
 	return advertise_object_info;
 }
 
diff --git a/setup.c b/setup.c
index 0de56a0..95909e9 100644
--- a/setup.c
+++ b/setup.c
@@ -10,7 +10,6 @@
 #include "object-file.h"
 #include "object-name.h"
 #include "refs.h"
-#include "replace-object.h"
 #include "repository.h"
 #include "config.h"
 #include "dir.h"
@@ -749,9 +748,9 @@ static int check_repo_format(const char *var, const char *value,
 	return read_worktree_config(var, value, ctx, vdata);
 }
 
-static int check_repository_format_gently(const char *gitdir,
-					  struct repository_format *candidate,
-					  int *nongit_ok)
+static int read_and_verify_repository_format(struct repository_format *format,
+					     const char *gitdir,
+					     int *nongit_ok)
 {
 	struct strbuf sb = STRBUF_INIT;
 	struct strbuf err = STRBUF_INIT;
@@ -759,7 +758,7 @@ static int check_repository_format_gently(const char *gitdir,
 
 	has_common = get_common_dir(&sb, gitdir);
 	strbuf_addstr(&sb, "/config");
-	read_repository_format(candidate, sb.buf);
+	read_repository_format(format, sb.buf);
 	strbuf_release(&sb);
 
 	/*
@@ -767,10 +766,10 @@ static int check_repository_format_gently(const char *gitdir,
 	 * we treat a missing config as a silent "ok", even when nongit_ok
 	 * is unset.
 	 */
-	if (candidate->version < 0)
+	if (format->version < 0)
 		return 0;
 
-	if (verify_repository_format(candidate, &err) < 0) {
+	if (verify_repository_format(format, &err) < 0) {
 		if (nongit_ok) {
 			warning("%s", err.buf);
 			strbuf_release(&err);
@@ -780,37 +779,37 @@ static int check_repository_format_gently(const char *gitdir,
 		die("%s", err.buf);
 	}
 
-	string_list_clear(&candidate->unknown_extensions, 0);
-	string_list_clear(&candidate->v1_only_extensions, 0);
+	string_list_clear(&format->unknown_extensions, 0);
+	string_list_clear(&format->v1_only_extensions, 0);
 
-	if (candidate->worktree_config) {
+	if (format->worktree_config) {
 		/*
 		 * pick up core.bare and core.worktree from per-worktree
 		 * config if present
 		 */
 		strbuf_addf(&sb, "%s/config.worktree", gitdir);
-		git_config_from_file(read_worktree_config, sb.buf, candidate);
+		git_config_from_file(read_worktree_config, sb.buf, format);
 		strbuf_release(&sb);
 		has_common = 0;
 	}
 
 	if (startup_info->force_bare_repository) {
-		candidate->is_bare = 1;
-		FREE_AND_NULL(candidate->work_tree);
+		format->is_bare = 1;
+		FREE_AND_NULL(format->work_tree);
 	} else if (has_common) {
 		/*
 		 * When sharing a common dir with another repository (e.g. a
 		 * linked worktree), do not let this repository's config
 		 * dictate bareness; it is inherited from the main worktree.
 		 */
-		candidate->is_bare = -1;
+		format->is_bare = -1;
 
 		/*
 		 * Furthermore, "core.worktree" is supposed to be ignored when
 		 * we have a commondir configured, unless it comes from the
 		 * per-worktree configuration.
 		 */
-		FREE_AND_NULL(candidate->work_tree);
+		FREE_AND_NULL(format->work_tree);
 	}
 
 	return 0;
@@ -1043,43 +1042,19 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
 	return error_code ? NULL : path;
 }
 
-static void setup_git_env_internal(struct repository *repo,
-				   const char *git_dir)
+static void apply_gitdir_and_environment(struct repository *repo, const char *path)
 {
-	char *git_replace_ref_base;
-	const char *shallow_file;
-	const char *replace_ref_base;
-	struct set_gitdir_args args = { NULL };
 	struct strvec to_free = STRVEC_INIT;
+	struct set_gitdir_args args = {
+		.commondir = getenv_safe(&to_free, GIT_COMMON_DIR_ENVIRONMENT),
+		.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT),
+		.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT),
+		.disable_ref_updates = !!getenv(GIT_QUARANTINE_ENVIRONMENT),
+	};
 
-	args.commondir = getenv_safe(&to_free, GIT_COMMON_DIR_ENVIRONMENT);
-	args.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT);
-	args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT);
-	if (getenv(GIT_QUARANTINE_ENVIRONMENT))
-		args.disable_ref_updates = true;
+	repo_set_gitdir(repo, path, &args);
 
-	repo_set_gitdir(repo, git_dir, &args);
 	strvec_clear(&to_free);
-
-	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
-		disable_replace_refs();
-	replace_ref_base = getenv(GIT_REPLACE_REF_BASE_ENVIRONMENT);
-	git_replace_ref_base = xstrdup(replace_ref_base ? replace_ref_base
-							  : "refs/replace/");
-	update_ref_namespace(NAMESPACE_REPLACE, git_replace_ref_base);
-
-	shallow_file = getenv(GIT_SHALLOW_FILE_ENVIRONMENT);
-	if (shallow_file)
-		set_alternate_shallow_file(repo, shallow_file, 0);
-
-	if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0))
-		fetch_if_missing = 0;
-}
-
-static void set_git_dir_1(struct repository *repo, const char *path)
-{
-	xsetenv(GIT_DIR_ENVIRONMENT, path, 1);
-	setup_git_env_internal(repo, path);
 }
 
 static void update_relative_gitdir(const char *name UNUSED,
@@ -1093,11 +1068,12 @@ static void update_relative_gitdir(const char *name UNUSED,
 	trace_printf_key(&trace_setup_key,
 			 "setup: move $GIT_DIR to '%s'",
 			 path);
-	set_git_dir_1(repo, path);
+	apply_gitdir_and_environment(repo, path);
+	xsetenv(GIT_DIR_ENVIRONMENT, path, 1);
 	free(path);
 }
 
-static void set_git_dir(struct repository *repo, const char *path, int make_realpath)
+static void apply_and_export_relative_gitdir(struct repository *repo, const char *path, int make_realpath)
 {
 	struct strbuf realpath = STRBUF_INIT;
 
@@ -1106,21 +1082,61 @@ static void set_git_dir(struct repository *repo, const char *path, int make_real
 		path = realpath.buf;
 	}
 
-	set_git_dir_1(repo, path);
+	apply_gitdir_and_environment(repo, path);
+	xsetenv(GIT_DIR_ENVIRONMENT, path, 1);
+
 	if (!is_absolute_path(path))
 		chdir_notify_register(NULL, update_relative_gitdir, repo);
 
 	strbuf_release(&realpath);
 }
 
-static const char *setup_explicit_git_dir(struct repository *repo,
+struct repo_discovery {
+	struct repository_format format;
+	char *gitdir;
+	char *worktree;
+	char *prefix;
+};
+
+#define REPO_DISCOVERY_INIT { \
+	.format = REPOSITORY_FORMAT_INIT, \
+}
+
+static void repo_discovery_release(struct repo_discovery *r)
+{
+	clear_repository_format(&r->format);
+	free(r->gitdir);
+	free(r->worktree);
+	free(r->prefix);
+}
+
+static void repo_discovery_set_gitdir(struct repo_discovery *r,
+				      const char *gitdir,
+				      int make_realpath)
+{
+	free(r->gitdir);
+	if (make_realpath) {
+		struct strbuf realpath = STRBUF_INIT;
+		strbuf_realpath(&realpath, gitdir, 1);
+		r->gitdir = strbuf_detach(&realpath, NULL);
+	} else {
+		r->gitdir = xstrdup(gitdir);
+	}
+}
+
+static void repo_discovery_set_worktree(struct repo_discovery *r,
+					const char *worktree)
+{
+	free(r->worktree);
+	r->worktree = real_pathdup(worktree, 1);
+}
+
+static void repo_discover_explicit_gitdir(struct repo_discovery *discovery,
 					  const char *gitdirenv,
 					  struct strbuf *cwd,
-					  struct repository_format *repo_fmt,
 					  int *nongit_ok)
 {
 	const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
-	const char *worktree;
 	char *gitfile;
 	int offset;
 
@@ -1136,150 +1152,136 @@ static const char *setup_explicit_git_dir(struct repository *repo,
 	if (!is_git_directory(gitdirenv)) {
 		if (nongit_ok) {
 			*nongit_ok = 1;
-			free(gitfile);
-			return NULL;
+			goto out;
 		}
 		die(_("not a git repository: '%s'"), gitdirenv);
 	}
 
-	if (check_repository_format_gently(gitdirenv, repo_fmt, nongit_ok)) {
-		free(gitfile);
-		return NULL;
-	}
+	if (read_and_verify_repository_format(&discovery->format, gitdirenv, nongit_ok))
+		goto out;
 
 	/* #3, #7, #11, #15, #19, #23, #27, #31 (see t1510) */
-	if (work_tree_env)
-		set_git_work_tree(repo, work_tree_env);
-	else if (repo_fmt->is_bare > 0) {
-		if (repo_fmt->work_tree) {
-			/* #22.2, #30 */
-			warning("core.bare and core.worktree do not make sense");
-			repo->worktree_config_is_bogus = true;
-		}
-
+	if (work_tree_env) {
+		/*
+		 * The environment variable overrides "core.worktree". This
+		 * also has the consequence that we don't want to flag cases as
+		 * bogus where we have both "core.worktree" and "core.bare", so
+		 * we have to explicitly unset the configuration.
+		 */
+		FREE_AND_NULL(discovery->format.work_tree);
+		repo_discovery_set_worktree(discovery, work_tree_env);
+	} else if (discovery->format.is_bare > 0) {
 		/* #18, #26 */
-		set_git_dir(repo, gitdirenv, 0);
-		free(gitfile);
-		return NULL;
-	}
-	else if (repo_fmt->work_tree) { /* #6, #14 */
-		if (is_absolute_path(repo_fmt->work_tree))
-			set_git_work_tree(repo, repo_fmt->work_tree);
-		else {
+		repo_discovery_set_gitdir(discovery, gitdirenv, 0);
+		goto out;
+	} else if (discovery->format.work_tree) { /* #6, #14 */
+		if (is_absolute_path(discovery->format.work_tree)) {
+			repo_discovery_set_worktree(discovery, discovery->format.work_tree);
+		} else {
 			char *core_worktree;
 			if (chdir(gitdirenv))
 				die_errno(_("cannot chdir to '%s'"), gitdirenv);
-			if (chdir(repo_fmt->work_tree))
-				die_errno(_("cannot chdir to '%s'"), repo_fmt->work_tree);
+			if (chdir(discovery->format.work_tree))
+				die_errno(_("cannot chdir to '%s'"), discovery->format.work_tree);
 			core_worktree = xgetcwd();
 			if (chdir(cwd->buf))
 				die_errno(_("cannot come back to cwd"));
-			set_git_work_tree(repo, core_worktree);
+			repo_discovery_set_worktree(discovery, core_worktree);
 			free(core_worktree);
 		}
-	}
-	else if (!git_env_bool(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, 1)) {
+	} else if (!git_env_bool(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, 1)) {
 		/* #16d */
-		set_git_dir(repo, gitdirenv, 0);
-		free(gitfile);
-		return NULL;
-	}
-	else /* #2, #10 */
-		set_git_work_tree(repo, ".");
-
-	/* set_git_work_tree() must have been called by now */
-	worktree = repo_get_work_tree(repo);
-
-	/* both repo_get_work_tree() and cwd are already normalized */
-	if (!strcmp(cwd->buf, worktree)) { /* cwd == worktree */
-		set_git_dir(repo, gitdirenv, 0);
-		free(gitfile);
-		return NULL;
+		repo_discovery_set_gitdir(discovery, gitdirenv, 0);
+		goto out;
+	} else { /* #2, #10 */
+		repo_discovery_set_worktree(discovery, ".");
 	}
 
-	offset = dir_inside_of(cwd->buf, worktree);
-	if (offset >= 0) {	/* cwd inside worktree? */
-		set_git_dir(repo, gitdirenv, 1);
-		if (chdir(worktree))
-			die_errno(_("cannot chdir to '%s'"), worktree);
-		strbuf_addch(cwd, '/');
-		free(gitfile);
-		return cwd->buf + offset;
+	/* both the worktree and cwd are already normalized */
+	if (!strcmp(cwd->buf, discovery->worktree)) { /* cwd == worktree */
+		repo_discovery_set_gitdir(discovery, gitdirenv, 0);
+		goto out;
+	}
+
+	offset = dir_inside_of(cwd->buf, discovery->worktree);
+	if (offset >= 0) {	/* cwd inside discovery->worktree? */
+		repo_discovery_set_gitdir(discovery, gitdirenv, 1);
+		if (chdir(discovery->worktree))
+			die_errno(_("cannot chdir to '%s'"), discovery->worktree);
+		discovery->prefix = xstrfmt("%s/", cwd->buf + offset);
+		goto out;
 	}
 
 	/* cwd outside worktree */
-	set_git_dir(repo, gitdirenv, 0);
+	repo_discovery_set_gitdir(discovery, gitdirenv, 0);
+
+out:
 	free(gitfile);
-	return NULL;
 }
 
-static const char *setup_discovered_git_dir(struct repository *repo,
-					    const char *gitdir,
-					    struct strbuf *cwd, int offset,
-					    struct repository_format *repo_fmt,
-					    int *nongit_ok)
+static void repo_discover_implicit_gitdir(struct repo_discovery *discovery,
+					  const char *gitdir,
+					  struct strbuf *cwd, int offset,
+					  int *nongit_ok)
 {
-	if (check_repository_format_gently(gitdir, repo_fmt, nongit_ok))
-		return NULL;
+	if (read_and_verify_repository_format(&discovery->format, gitdir, nongit_ok))
+		return;
 
 	/* --work-tree is set without --git-dir; use discovered one */
-	if (getenv(GIT_WORK_TREE_ENVIRONMENT) || repo_fmt->work_tree) {
+	if (getenv(GIT_WORK_TREE_ENVIRONMENT) || discovery->format.work_tree) {
 		char *to_free = NULL;
-		const char *ret;
 
 		if (offset != cwd->len && !is_absolute_path(gitdir))
 			gitdir = to_free = real_pathdup(gitdir, 1);
 		if (chdir(cwd->buf))
 			die_errno(_("cannot come back to cwd"));
-		ret = setup_explicit_git_dir(repo, gitdir, cwd, repo_fmt, nongit_ok);
+		repo_discover_explicit_gitdir(discovery, gitdir, cwd,
+					      nongit_ok);
 		free(to_free);
-		return ret;
+		return;
 	}
 
 	/* #16.2, #17.2, #20.2, #21.2, #24, #25, #28, #29 (see t1510) */
-	if (repo_fmt->is_bare > 0) {
-		set_git_dir(repo, gitdir, (offset != cwd->len));
+	if (discovery->format.is_bare > 0) {
+		repo_discovery_set_gitdir(discovery, gitdir, (offset != cwd->len));
 		if (chdir(cwd->buf))
 			die_errno(_("cannot come back to cwd"));
-		return NULL;
+		return;
 	}
 
 	/* #0, #1, #5, #8, #9, #12, #13 */
-	set_git_work_tree(repo, ".");
+	repo_discovery_set_worktree(discovery, ".");
 	if (strcmp(gitdir, DEFAULT_GIT_DIR_ENVIRONMENT))
-		set_git_dir(repo, gitdir, 0);
+		repo_discovery_set_gitdir(discovery, gitdir, 0);
 	if (offset >= cwd->len)
-		return NULL;
+		return;
 
 	/* Make "offset" point past the '/' (already the case for root dirs) */
 	if (offset != offset_1st_component(cwd->buf))
 		offset++;
-	/* Add a '/' at the end */
-	strbuf_addch(cwd, '/');
-	return cwd->buf + offset;
+	discovery->prefix = xstrfmt("%s/", cwd->buf + offset);
 }
 
 /* #16.1, #17.1, #20.1, #21.1, #22.1 (see t1510) */
-static const char *setup_bare_git_dir(struct repository *repo,
+static void repo_discover_bare_gitdir(struct repo_discovery *discovery,
 				      struct strbuf *cwd, int offset,
-				      struct repository_format *repo_fmt,
 				      int *nongit_ok)
 {
 	int root_len;
 
-	if (check_repository_format_gently(".", repo_fmt, nongit_ok))
-		return NULL;
+	if (read_and_verify_repository_format(&discovery->format, ".", nongit_ok))
+		return;
 
 	setenv(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, "0", 1);
 
 	/* --work-tree is set without --git-dir; use discovered one */
-	if (getenv(GIT_WORK_TREE_ENVIRONMENT) || repo_fmt->work_tree) {
-		static const char *gitdir;
-
-		gitdir = offset == cwd->len ? "." : xmemdupz(cwd->buf, offset);
+	if (getenv(GIT_WORK_TREE_ENVIRONMENT) || discovery->format.work_tree) {
+		char *gitdir = offset == cwd->len ? xstrdup(".") : xmemdupz(cwd->buf, offset);
 		if (chdir(cwd->buf))
 			die_errno(_("cannot come back to cwd"));
-		return setup_explicit_git_dir(repo, gitdir, cwd, repo_fmt, nongit_ok);
+		repo_discover_explicit_gitdir(discovery, gitdir, cwd, nongit_ok);
+		free(gitdir);
+		return;
 	}
 
 	if (offset != cwd->len) {
@@ -1287,11 +1289,10 @@ static const char *setup_bare_git_dir(struct repository *repo,
 			die_errno(_("cannot come back to cwd"));
 		root_len = offset_1st_component(cwd->buf);
 		strbuf_setlen(cwd, offset > root_len ? offset : root_len);
-		set_git_dir(repo, cwd->buf, 0);
+		repo_discovery_set_gitdir(discovery, cwd->buf, 0);
+	} else {
+		repo_discovery_set_gitdir(discovery, ".", 0);
 	}
-	else
-		set_git_dir(repo, ".", 0);
-	return NULL;
 }
 
 static dev_t get_device_or_die(const char *path, const char *prefix, int prefix_len)
@@ -1549,10 +1550,10 @@ static int is_implicit_bare_repo(const char *path)
  * the discovered .git/ directory, if any. If `gitdir` is not absolute, it
  * is relative to `dir` (i.e. *not* necessarily the cwd).
  */
-static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,
-							  struct strbuf *gitdir,
-							  struct strbuf *report,
-							  int die_on_error)
+static enum discovery_result repo_discovery_find_dir(struct strbuf *dir,
+						     struct strbuf *gitdir,
+						     struct strbuf *report,
+						     int die_on_error)
 {
 	const char *env_ceiling_dirs = getenv(CEILING_DIRECTORIES_ENVIRONMENT);
 	struct string_list ceiling_dirs = STRING_LIST_INIT_DUP;
@@ -1719,7 +1720,7 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir,
 		return GIT_DIR_CWD_FAILURE;
 
 	cwd_len = dir.len;
-	result = setup_git_directory_gently_1(&dir, gitdir, NULL, 0);
+	result = repo_discovery_find_dir(&dir, gitdir, NULL, 0);
 	if (result <= 0) {
 		strbuf_release(&dir);
 		return result;
@@ -1769,9 +1770,20 @@ int apply_repository_format(struct repository *repo,
 	if (verify_repository_format(format, err) < 0)
 		return -1;
 
+	if (format->is_bare > 0 && format->work_tree) {
+		/* #22.2, #30 */
+		warning("core.bare and core.worktree do not make sense");
+		repo->worktree_config_is_bogus = true;
+	}
+
 	if (flags & APPLY_REPOSITORY_FORMAT_HONOR_ENV) {
+		const char *shallow_file;
+
 		object_directory = xstrdup_or_null(getenv(DB_ENVIRONMENT));
 		alternate_object_directories = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT));
+		shallow_file = getenv(GIT_SHALLOW_FILE_ENVIRONMENT);
+		if (shallow_file)
+			set_alternate_shallow_file(repo, shallow_file);
 	}
 
 	repo->bare_cfg = format->is_bare;
@@ -1874,8 +1886,8 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags
 		struct repository_format fmt = REPOSITORY_FORMAT_INIT;
 		struct strbuf err = STRBUF_INIT;
 
-		set_git_dir(repo, ".", 0);
-		check_repository_format_gently(".", &fmt, NULL);
+		apply_and_export_relative_gitdir(repo, ".", 0);
+		read_and_verify_repository_format(&fmt, ".", NULL);
 		if (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)
 			die("%s", err.buf);
 		startup_info->have_repository = 1;
@@ -1893,7 +1905,7 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags
  * primarily to support git-clone to work in a new repository it just
  * created, and is not meant to flip between different work trees.
  */
-void set_git_work_tree(struct repository *repo, const char *new_work_tree)
+static void set_git_work_tree(struct repository *repo, const char *new_work_tree)
 {
 	if (repo->worktree_initialized) {
 		struct strbuf realpath = STRBUF_INIT;
@@ -1911,21 +1923,10 @@ void set_git_work_tree(struct repository *repo, const char *new_work_tree)
 	repo_set_worktree(repo, new_work_tree);
 }
 
-const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
+static void repo_discover(struct repo_discovery *discovery, int *nongit_ok)
 {
-	static struct strbuf cwd = STRBUF_INIT;
+	struct strbuf cwd = STRBUF_INIT;
 	struct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;
-	const char *prefix = NULL;
-	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
-
-	/*
-	 * We may have read an incomplete configuration before
-	 * setting-up the git directory. If so, clear the cache so
-	 * that the next queries to the configuration reload complete
-	 * configuration (including the per-repo config file that we
-	 * ignored previously).
-	 */
-	repo_config_clear(repo);
 
 	/*
 	 * Let's assume that we are in a git repository.
@@ -1939,20 +1940,21 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 		die_errno(_("Unable to read current working directory"));
 	strbuf_addbuf(&dir, &cwd);
 
-	switch (setup_git_directory_gently_1(&dir, &gitdir, &report, 1)) {
+	switch (repo_discovery_find_dir(&dir, &gitdir, &report, 1)) {
 	case GIT_DIR_EXPLICIT:
-		prefix = setup_explicit_git_dir(repo, gitdir.buf, &cwd, &repo_fmt, nongit_ok);
+		repo_discover_explicit_gitdir(discovery, gitdir.buf, &cwd,
+					      nongit_ok);
 		break;
 	case GIT_DIR_DISCOVERED:
 		if (dir.len < cwd.len && chdir(dir.buf))
 			die(_("cannot change to '%s'"), dir.buf);
-		prefix = setup_discovered_git_dir(repo, gitdir.buf, &cwd, dir.len,
-						  &repo_fmt, nongit_ok);
+		repo_discover_implicit_gitdir(discovery, gitdir.buf, &cwd, dir.len,
+					      nongit_ok);
 		break;
 	case GIT_DIR_BARE:
 		if (dir.len < cwd.len && chdir(dir.buf))
 			die(_("cannot change to '%s'"), dir.buf);
-		prefix = setup_bare_git_dir(repo, &cwd, dir.len, &repo_fmt, nongit_ok);
+		repo_discover_bare_gitdir(discovery, &cwd, dir.len, nongit_ok);
 		break;
 	case GIT_DIR_HIT_CEILING:
 		if (!nongit_ok)
@@ -1993,22 +1995,43 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 	case GIT_DIR_CWD_FAILURE:
 	case GIT_DIR_INVALID_FORMAT:
 		/*
-		 * As a safeguard against setup_git_directory_gently_1 returning
+		 * As a safeguard against repo_discovery_find_dir returning
 		 * these values, fallthrough to BUG. Otherwise it is possible to
 		 * set startup_info->have_repository to 1 when we did nothing to
 		 * find a repository.
 		 */
 	default:
-		BUG("unhandled setup_git_directory_gently_1() result");
+		BUG("unhandled repo_discovery_find_dir() result");
 	}
 
+	strbuf_release(&dir);
+	strbuf_release(&cwd);
+	strbuf_release(&gitdir);
+	strbuf_release(&report);
+}
+
+const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
+{
+	struct repo_discovery discovery = REPO_DISCOVERY_INIT;
+
+	/*
+	 * We may have read an incomplete configuration before
+	 * setting-up the git directory. If so, clear the cache so
+	 * that the next queries to the configuration reload complete
+	 * configuration (including the per-repo config file that we
+	 * ignored previously).
+	 */
+	repo_config_clear(repo);
+
+	repo_discover(&discovery, nongit_ok);
+
 	/*
 	 * At this point, nongit_ok is stable. If it is non-NULL and points
 	 * to a non-zero value, then this means that we haven't found a
 	 * repository and that the caller expects startup_info to reflect
 	 * this.
 	 *
-	 * Regardless of the state of nongit_ok, startup_info->prefix and
+	 * Regardless of the state of nongit_ok, the_repository->prefix and
 	 * the GIT_PREFIX environment variable must always match. For details
 	 * see Documentation/config/alias.adoc.
 	 */
@@ -2018,10 +2041,10 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 		startup_info->have_repository = 1;
 
 	/*
-	 * Not all paths through the setup code will call 'set_git_dir()' (which
-	 * directly sets up the environment) so in order to guarantee that the
-	 * environment is in a consistent state after setup, explicitly setup
-	 * the environment if we have a repository.
+	 * Not all paths through the setup code will have recorded a gitdir
+	 * above, so in order to guarantee that the environment is in a
+	 * consistent state after setup, explicitly set up the gitdir and
+	 * environment if we have a repository.
 	 *
 	 * NEEDSWORK: currently we allow bogus GIT_DIR values to be set in some
 	 * code paths so we also need to explicitly setup the environment if
@@ -2032,11 +2055,16 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 	    startup_info->have_repository ||
 	    /* GIT_DIR_EXPLICIT */
 	    getenv(GIT_DIR_ENVIRONMENT)) {
-		if (!repo->gitdir) {
+		if (discovery.worktree)
+			set_git_work_tree(repo, discovery.worktree);
+
+		if (discovery.gitdir) {
+			apply_and_export_relative_gitdir(repo, discovery.gitdir, 0);
+		} else {
 			const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
 			if (!gitdir)
 				gitdir = DEFAULT_GIT_DIR_ENVIRONMENT;
-			setup_git_env_internal(repo, gitdir);
+			apply_gitdir_and_environment(repo, gitdir);
 		}
 
 		if (startup_info->have_repository) {
@@ -2051,21 +2079,21 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 			if (ref_backend_uri) {
 				char *format;
 
-				free(repo_fmt.ref_storage_payload);
+				free(discovery.format.ref_storage_payload);
 
-				parse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);
-				repo_fmt.ref_storage_format = ref_storage_format_by_name(format);
-				if (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)
+				parse_reference_uri(ref_backend_uri, &format, &discovery.format.ref_storage_payload);
+				discovery.format.ref_storage_format = ref_storage_format_by_name(format);
+				if (discovery.format.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)
 					die(_("unknown ref storage format: '%s'"), format);
 
 				free(format);
 			}
 
-			if (apply_repository_format(repo, &repo_fmt,
+			if (apply_repository_format(repo, &discovery.format,
 						    APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)
 				die("%s", err.buf);
 
-			clear_repository_format(&repo_fmt);
+			clear_repository_format(&discovery.format);
 			strbuf_release(&err);
 		}
 	}
@@ -2076,23 +2104,19 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 	 * out where the repository is, i.e. a preparation
 	 * for calling repo_config_get_bool().
 	 */
-	if (prefix) {
-		prefix = precompose_string_if_needed(prefix);
-		startup_info->prefix = prefix;
-		setenv(GIT_PREFIX_ENVIRONMENT, prefix, 1);
+	if (discovery.prefix) {
+		const char *prefix = precompose_string_if_needed(discovery.prefix);
+		repo->prefix = xstrdup(prefix);
+		setenv(GIT_PREFIX_ENVIRONMENT, repo->prefix, 1);
 	} else {
-		startup_info->prefix = NULL;
+		FREE_AND_NULL(repo->prefix);
 		setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
 	}
 
 	setup_original_cwd(repo);
 
-	strbuf_release(&dir);
-	strbuf_release(&gitdir);
-	strbuf_release(&report);
-	clear_repository_format(&repo_fmt);
-
-	return prefix;
+	repo_discovery_release(&discovery);
+	return repo->prefix;
 }
 
 int git_config_perm(const char *var, const char *value)
@@ -2331,7 +2355,7 @@ static void copy_templates_1(struct repository *repo,
 			strbuf_release(&lnk);
 		}
 		else if (S_ISREG(st_template.st_mode)) {
-			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
+			if (copy_file(repo, path->buf, template_path->buf, st_template.st_mode))
 				die_errno(_("cannot copy '%s' to '%s'"),
 					  template_path->buf, path->buf);
 		}
@@ -2650,7 +2674,8 @@ static void create_object_directory(struct repository *repo)
 	strbuf_release(&path);
 }
 
-static void separate_git_dir(const char *git_dir, const char *git_link)
+static void separate_git_dir(struct repository *repo,
+			     const char *git_dir, const char *git_link)
 {
 	struct stat st;
 
@@ -2666,7 +2691,7 @@ static void separate_git_dir(const char *git_dir, const char *git_link)
 
 		if (rename(src, git_dir))
 			die_errno(_("unable to move %s to %s"), src, git_dir);
-		repair_worktrees_after_gitdir_move(src);
+		repair_worktrees_after_gitdir_move(repo, src);
 	}
 
 	write_file(git_link, "gitdir: %s", git_dir);
@@ -2800,7 +2825,9 @@ static void repository_format_configure(struct repository_format *repo_fmt,
 }
 
 int init_db(struct repository *repo,
-	    const char *git_dir, const char *real_git_dir,
+	    const char *git_dir,
+	    const char *real_git_dir,
+	    const char *worktree,
 	    const char *template_dir, int hash,
 	    enum ref_storage_format ref_storage_format,
 	    const char *initial_branch,
@@ -2821,15 +2848,16 @@ int init_db(struct repository *repo,
 		if (!exist_ok && !stat(real_git_dir, &st))
 			die(_("%s already exists"), real_git_dir);
 
-		set_git_dir(repo, real_git_dir, 1);
+		apply_and_export_relative_gitdir(repo, real_git_dir, 1);
 		git_dir = repo_get_git_dir(repo);
-		separate_git_dir(git_dir, original_git_dir);
-	}
-	else {
-		set_git_dir(repo, git_dir, 1);
+		separate_git_dir(repo, git_dir, original_git_dir);
+	} else {
+		apply_and_export_relative_gitdir(repo, git_dir, 1);
 		git_dir = repo_get_git_dir(repo);
 	}
-	startup_info->have_repository = 1;
+
+	if (worktree)
+		set_git_work_tree(repo, worktree);
 
 	/*
 	 * Check to see if the repository version is right.
@@ -2837,7 +2865,7 @@ int init_db(struct repository *repo,
 	 * config file, so this will not fail.  What we are catching
 	 * is an attempt to reinitialize new repository with an old tool.
 	 */
-	check_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);
+	read_and_verify_repository_format(&repo_fmt, repo_get_git_dir(repo), NULL);
 	repository_format_configure(&repo_fmt, hash, ref_storage_format);
 	if (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)
 		die("%s", err.buf);
diff --git a/setup.h b/setup.h
index dcb6545..654f10e 100644
--- a/setup.h
+++ b/setup.h
@@ -96,8 +96,6 @@ static inline int discover_git_directory(struct strbuf *commondir,
 	return 0;
 }
 
-void set_git_work_tree(struct repository *repo, const char *tree);
-
 /* Flags that can be passed to `enter_repo()`. */
 enum {
 	/*
@@ -263,7 +261,9 @@ const char *get_template_dir(const char *option_template);
 #define INIT_DB_SKIP_REFDB (1 << 2)
 
 int init_db(struct repository *repo,
-	    const char *git_dir, const char *real_git_dir,
+	    const char *git_dir,
+	    const char *real_git_dir,
+	    const char *worktree,
 	    const char *template_dir, int hash_algo,
 	    enum ref_storage_format ref_storage_format,
 	    const char *initial_branch, int init_shared_repository,
@@ -299,7 +299,6 @@ struct startup_info {
 	bool force_bare_repository;
 
 	int have_repository;
-	const char *prefix;
 	const char *original_cwd;
 };
 extern struct startup_info *startup_info;
diff --git a/sha1/openssl.h b/sha1/openssl.h
index 1038af4..48deeb7 100644
--- a/sha1/openssl.h
+++ b/sha1/openssl.h
@@ -40,12 +40,18 @@ static inline void openssl_SHA1_Clone(struct openssl_SHA1_CTX *dst,
 	EVP_MD_CTX_copy_ex(dst->ectx, src->ectx);
 }
 
+static inline void openssl_SHA1_Discard(struct openssl_SHA1_CTX *ctx)
+{
+	EVP_MD_CTX_free(ctx->ectx);
+}
+
 #ifndef platform_SHA_CTX
 #define platform_SHA_CTX openssl_SHA1_CTX
 #define platform_SHA1_Init openssl_SHA1_Init
 #define platform_SHA1_Clone openssl_SHA1_Clone
 #define platform_SHA1_Update openssl_SHA1_Update
 #define platform_SHA1_Final openssl_SHA1_Final
+#define platform_SHA1_Discard openssl_SHA1_Discard
 #endif
 
 #endif /* SHA1_OPENSSL_H */
diff --git a/sha256/gcrypt.h b/sha256/gcrypt.h
index 17a90f1..d91ffe7 100644
--- a/sha256/gcrypt.h
+++ b/sha256/gcrypt.h
@@ -27,13 +27,20 @@ static inline void gcrypt_SHA256_Final(unsigned char *digest, gcrypt_SHA256_CTX
 
 static inline void gcrypt_SHA256_Clone(gcrypt_SHA256_CTX *dst, const gcrypt_SHA256_CTX *src)
 {
+	gcry_md_close(*dst);
 	gcry_md_copy(dst, *src);
 }
 
+static inline void gcrypt_SHA256_Discard(gcrypt_SHA256_CTX *ctx)
+{
+	gcry_md_close(*ctx);
+}
+
 #define platform_SHA256_CTX gcrypt_SHA256_CTX
 #define platform_SHA256_Init gcrypt_SHA256_Init
 #define platform_SHA256_Clone gcrypt_SHA256_Clone
 #define platform_SHA256_Update gcrypt_SHA256_Update
 #define platform_SHA256_Final gcrypt_SHA256_Final
+#define platform_SHA256_Discard gcrypt_SHA256_Discard
 
 #endif
diff --git a/sha256/openssl.h b/sha256/openssl.h
index c1083d9..3d457ca 100644
--- a/sha256/openssl.h
+++ b/sha256/openssl.h
@@ -40,10 +40,16 @@ static inline void openssl_SHA256_Clone(struct openssl_SHA256_CTX *dst,
 	EVP_MD_CTX_copy_ex(dst->ectx, src->ectx);
 }
 
+static inline void openssl_SHA256_Discard(struct openssl_SHA256_CTX *ctx)
+{
+	EVP_MD_CTX_free(ctx->ectx);
+}
+
 #define platform_SHA256_CTX openssl_SHA256_CTX
 #define platform_SHA256_Init openssl_SHA256_Init
 #define platform_SHA256_Clone openssl_SHA256_Clone
 #define platform_SHA256_Update openssl_SHA256_Update
 #define platform_SHA256_Final openssl_SHA256_Final
+#define platform_SHA256_Discard openssl_SHA256_Discard
 
 #endif /* SHA256_OPENSSL_H */
diff --git a/shallow.c b/shallow.c
index 07cae44..8e244a5 100644
--- a/shallow.c
+++ b/shallow.c
@@ -21,12 +21,10 @@
 #include "statinfo.h"
 #include "trace.h"
 
-void set_alternate_shallow_file(struct repository *r, const char *path, int override)
+void set_alternate_shallow_file(struct repository *r, const char *path)
 {
 	if (r->parsed_objects->is_shallow != -1)
 		BUG("is_repository_shallow must not be called before set_alternate_shallow_file");
-	if (r->parsed_objects->alternate_shallow_file && !override)
-		return;
 	free(r->parsed_objects->alternate_shallow_file);
 	r->parsed_objects->alternate_shallow_file = xstrdup_or_null(path);
 }
@@ -359,7 +357,9 @@ struct write_shallow_data {
 static int write_one_shallow(const struct commit_graft *graft, void *cb_data)
 {
 	struct write_shallow_data *data = cb_data;
-	const char *hex = oid_to_hex(&graft->oid);
+	char hex[GIT_MAX_HEXSZ + 1];
+
+	oid_to_hex_r(hex, &graft->oid);
 	if (graft->nr_parent != -1)
 		return 0;
 	if (data->flags & QUICK) {
@@ -370,8 +370,7 @@ static int write_one_shallow(const struct commit_graft *graft, void *cb_data)
 		struct commit *c = lookup_commit(the_repository, &graft->oid);
 		if (!c || !(c->object.flags & SEEN)) {
 			if (data->flags & VERBOSE)
-				printf("Removing %s from .git/shallow\n",
-				       oid_to_hex(&c->object.oid));
+				printf("Removing %s from .git/shallow\n", hex);
 			return 0;
 		}
 	}
diff --git a/shallow.h b/shallow.h
index e20ca4c..6a64db4 100644
--- a/shallow.h
+++ b/shallow.h
@@ -10,7 +10,7 @@
 struct oid_array;
 struct strvec;
 
-void set_alternate_shallow_file(struct repository *r, const char *path, int override);
+void set_alternate_shallow_file(struct repository *r, const char *path);
 int register_shallow(struct repository *r, const struct object_id *oid);
 int unregister_shallow(const struct object_id *oid);
 int is_repository_shallow(struct repository *r);
diff --git a/sideband.c b/sideband.c
index 1523a53..4a2c5f8 100644
--- a/sideband.c
+++ b/sideband.c
@@ -163,6 +163,10 @@ static int handle_ansi_sequence(struct strbuf *dest, const char *src, int n)
 	 *
 	 * ESC [ [<n> [; <n>]*] m
 	 *
+	 * where <n> can be either zero-length, or a decimal number, or a
+	 * series of decimal numbers separated by a colon (for 256-color or
+	 * true-color codes).
+	 *
 	 * These are part of the Select Graphic Rendition sequences which
 	 * contain more than just color sequences, for more details see
 	 * https://en.wikipedia.org/wiki/ANSI_escape_code#SGR.
@@ -210,7 +214,7 @@ static int handle_ansi_sequence(struct strbuf *dest, const char *src, int n)
 			strbuf_add(dest, src, i + 1);
 			return i;
 		}
-		if (!isdigit(src[i]) && src[i] != ';')
+		if (!isdigit(src[i]) && src[i] != ':' && src[i] != ';')
 			break;
 	}
 
diff --git a/src/hash.rs b/src/hash.rs
index dea2998..e1f2d31 100644
--- a/src/hash.rs
+++ b/src/hash.rs
@@ -181,7 +181,10 @@
 impl Clone for CryptoHasher {
     fn clone(&self) -> Self {
         let ctx = unsafe { c::git_hash_alloc() };
-        unsafe { c::git_hash_clone(ctx, self.ctx) };
+        unsafe {
+            c::git_hash_init(ctx, self.algo.hash_algo_ptr());
+            c::git_hash_clone(ctx, self.ctx)
+        };
         Self {
             algo: self.algo,
             ctx,
@@ -191,7 +194,10 @@
 
 impl Drop for CryptoHasher {
     fn drop(&mut self) {
-        unsafe { c::git_hash_free(self.ctx) };
+        unsafe {
+            c::git_hash_discard(self.ctx);
+            c::git_hash_free(self.ctx);
+        };
     }
 }
 
@@ -353,6 +359,7 @@
         pub fn git_hash_clone(dst: *mut c_void, src: *const c_void);
         pub fn git_hash_update(ctx: *mut c_void, inp: *const c_void, len: usize);
         pub fn git_hash_final(hash: *mut u8, ctx: *mut c_void);
+        pub fn git_hash_discard(ctx: *mut c_void);
         pub fn git_hash_final_oid(hash: *mut c_void, ctx: *mut c_void);
     }
 }
@@ -447,6 +454,7 @@
                 h.update(&data[2..]);
 
                 let h2 = h.clone();
+                let h3 = h2.clone();
 
                 let actual_oid = h.into_oid();
                 assert_eq!(**oid, actual_oid);
@@ -460,6 +468,7 @@
 
                 let actual_oid = h.into_oid();
                 assert_eq!(**oid, actual_oid);
+                std::mem::drop(h3);
             }
         }
     }
diff --git a/strbuf.c b/strbuf.c
index 764b629..4495566 100644
--- a/strbuf.c
+++ b/strbuf.c
@@ -646,8 +646,6 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)
 	if (feof(fp))
 		return EOF;
 
-	strbuf_reset(sb);
-
 	/* Translate slopbuf to NULL, as we cannot call realloc on it */
 	if (!sb->alloc)
 		sb->buf = NULL;
diff --git a/submodule.c b/submodule.c
index 93d0361..5c92575 100644
--- a/submodule.c
+++ b/submodule.c
@@ -2390,7 +2390,7 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu
 
 	/* Prevent conflicts on case-folding filesystems */
 	repo_config_get_bool(the_repository, "core.ignorecase", &config_ignorecase);
-	if (ignore_case || config_ignorecase) {
+	if (repo_ignore_case(the_repository) || config_ignorecase) {
 		bool suffixes_match = !strcmp(last_submodule_name, submodule_name);
 		return check_casefolding_conflict(git_dir, submodule_name,
 						  suffixes_match);
@@ -2494,7 +2494,7 @@ static void relocate_single_git_dir_into_superproject(const char *path,
 	if (validate_submodule_path(path) < 0)
 		exit(128);
 
-	if (submodule_uses_worktrees(path))
+	if (submodule_uses_worktrees(the_repository, path))
 		die(_("relocate_gitdir for submodule '%s' with "
 		      "more than one worktree not supported"), path);
 
@@ -2627,13 +2627,12 @@ int get_superproject_working_tree(struct strbuf *buf)
 		 * We might have a superproject, but it is harder
 		 * to determine.
 		 */
-		return 0;
+		goto out;
 
 	if (!strbuf_realpath(&one_up, "../", 0))
-		return 0;
+		goto out;
 
 	subpath = relative_path(cwd, one_up.buf, &sb);
-	strbuf_release(&one_up);
 
 	prepare_submodule_repo_env(&cp.env);
 	strvec_pop(&cp.env);
@@ -2678,20 +2677,22 @@ int get_superproject_working_tree(struct strbuf *buf)
 		ret = 1;
 		free(super_wt);
 	}
-	free(cwd);
-	strbuf_release(&sb);
 
 	code = finish_command(&cp);
 
 	if (code == 128)
 		/* '../' is not a git repository */
-		return 0;
-	if (code == 0 && len == 0)
+		ret = 0;
+	else if (code == 0 && len == 0)
 		/* There is an unrelated git repository at '../' */
-		return 0;
-	if (code)
+		ret = 0;
+	else if (code)
 		die(_("ls-tree returned unexpected return code %d"), code);
 
+out:
+	strbuf_release(&sb);
+	strbuf_release(&one_up);
+	free(cwd);
 	return ret;
 }
 
diff --git a/t/.gitattributes b/t/.gitattributes
index 7664c6e..e867f38 100644
--- a/t/.gitattributes
+++ b/t/.gitattributes
@@ -1,5 +1,7 @@
 t[0-9][0-9][0-9][0-9]/* -whitespace
 /chainlint/*.expect eol=lf -whitespace
+/greplint/*.expect eol=lf -whitespace
+/greplint/*.test eol=lf -whitespace
 /t0110/url-* binary
 /t3206/* eol=lf
 /t3900/*.txt eol=lf
diff --git a/t/Makefile b/t/Makefile
index ab8a5b5..e324987 100644
--- a/t/Makefile
+++ b/t/Makefile
@@ -27,9 +27,11 @@
 ifdef TEST_OUTPUT_DIRECTORY
 TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results
 CHAINLINTTMP = $(TEST_OUTPUT_DIRECTORY)/chainlinttmp
+GREPLINTTMP = $(TEST_OUTPUT_DIRECTORY)/greplinttmp
 else
 TEST_RESULTS_DIRECTORY = test-results
 CHAINLINTTMP = chainlinttmp
+GREPLINTTMP = greplinttmp
 endif
 
 # Shell quote;
@@ -38,6 +40,7 @@
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
 TEST_RESULTS_DIRECTORY_SQ = $(subst ','\'',$(TEST_RESULTS_DIRECTORY))
 CHAINLINTTMP_SQ = $(subst ','\'',$(CHAINLINTTMP))
+GREPLINTTMP_SQ = $(subst ','\'',$(GREPLINTTMP))
 
 T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))
 THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh)))
@@ -45,6 +48,7 @@
 TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))
 TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))
 CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))
+GREPLINTTESTS = $(sort $(patsubst greplint/%.test,%,$(wildcard greplint/*.test)))
 CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl
 UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)
 UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))
@@ -63,8 +67,8 @@
 	$(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup
 
 ifneq ($(PERL_PATH),)
-test: check-chainlint
-prove: check-chainlint
+test: check-chainlint check-greplint
+prove: check-chainlint check-greplint
 endif
 
 failed:
@@ -102,7 +106,7 @@
 pre-clean:
 	$(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)'
 
-clean-except-prove-cache: clean-chainlint
+clean-except-prove-cache: clean-chainlint clean-greplint
 	$(RM) -r 'trash directory'.*
 	$(RM) -r valgrind/bin
 
@@ -120,6 +124,17 @@
 	{ $(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests >'$(CHAINLINTTMP_SQ)'/actual || true; } && \
 	diff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual
 
+clean-greplint:
+	$(RM) -r '$(GREPLINTTMP_SQ)'
+
+check-greplint:
+	@mkdir -p '$(GREPLINTTMP_SQ)' && \
+	'$(PERL_PATH_SQ)' greplint-cat.pl '$(GREPLINTTMP_SQ)' $(GREPLINTTESTS) && \
+	{ '$(PERL_PATH_SQ)' greplint.pl \
+		$(patsubst %,greplint/%.test,$(GREPLINTTESTS)) \
+		>'$(GREPLINTTMP_SQ)'/actual 2>&1 || true; } && \
+	diff -u '$(GREPLINTTMP_SQ)'/expect '$(GREPLINTTMP_SQ)'/actual
+
 check-meson:
 	@# awk acts up when trying to match single quotes, so we use \047 instead.
 	@mkdir -p mesontmp && \
@@ -139,7 +154,7 @@
 test-lint: test-lint-duplicates test-lint-executable \
 	test-lint-filenames
 ifneq ($(PERL_PATH),)
-test-lint: test-lint-shell-syntax
+test-lint: test-lint-shell-syntax test-greplint
 else
 GIT_TEST_CHAIN_LINT = 0
 endif
@@ -160,6 +175,9 @@
 test-lint-shell-syntax:
 	@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)
 
+test-greplint:
+	@'$(PERL_PATH_SQ)' greplint.pl $(T) $(THELPERS) $(TPERF)
+
 test-lint-filenames:
 	@# We do *not* pass a glob to ls-files but use grep instead, to catch
 	@# non-ASCII characters (which are quoted within double-quotes)
@@ -185,7 +203,8 @@
 	$(MAKE) -C perf/ all
 
 .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
-	check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)
+	check-chainlint clean-chainlint test-chainlint \
+	check-greplint clean-greplint test-greplint $(UNIT_TESTS)
 
 .PHONY: libgit-sys-test libgit-rs-test
 libgit-sys-test:
diff --git a/t/README b/t/README
index 085921b..4252774 100644
--- a/t/README
+++ b/t/README
@@ -1039,6 +1039,40 @@
 
    Check whether a file has the length it is expected to.
 
+ - test_grep [!] [<grep-options>] <pattern> <file>
+
+   Check whether <file> contains a line matching <pattern>, or
+   with '!' that no line matches.  Use this instead of bare
+   'grep <pattern> <file>' in test assertions.  On failure,
+   test_grep prints the contents of <file> for easier debugging,
+   whereas a bare 'grep' would fail silently.
+
+   For negation, pass '!' as the first argument:
+
+	test_grep ! "^diff --git" actual
+
+   Do not negate by writing '! test_grep', as that suppresses the
+   diagnostic output.
+
+   test_grep should only be used as a test assertion.  When grep
+   is used as a data filter (e.g. 'grep -v "^index" actual >filtered')
+   or inside a command substitution (e.g. '$(grep -c ...)'), plain
+   'grep' is the right choice because the exit code is not the
+   assertion itself.
+
+   test_grep requires <file> to exist and will BUG otherwise, so
+   use it only where the file is guaranteed to exist at that point.
+   When a file's presence is conditional (a backend-specific file,
+   or a path that only exists on some platforms, such as an NTFS
+   8.3 short name), guard the assertion on that condition (a
+   prerequisite, or a 'test -e' on the path) and use test_grep
+   inside the guard:
+
+	if test_have_prereq REFFILES
+	then
+		test_grep ! "$refname" .git/packed-refs
+	fi
+
  - test_path_is_file <path>
    test_path_is_dir <path>
    test_path_is_missing <path>
diff --git a/t/chainlint.pl b/t/chainlint.pl
index 2d07a99..cededf1 100755
--- a/t/chainlint.pl
+++ b/t/chainlint.pl
@@ -23,458 +23,9 @@
 my $show_stats;
 my $emit_all;
 
-# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3
-# "Token Recognition" of POSIX chapter 2 "Shell Command Language". Although
-# similar to lexical analyzers for other languages, this one differs in a few
-# substantial ways due to quirks of the shell command language.
-#
-# For instance, in many languages, newline is just whitespace like space or
-# TAB, but in shell a newline is a command separator, thus a distinct lexical
-# token. A newline is significant and returned as a distinct token even at the
-# end of a shell comment.
-#
-# In other languages, `1+2` would typically be scanned as three tokens
-# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar
-# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.
-# In shell, several characters with special meaning lose that meaning when not
-# surrounded by whitespace. For instance, the negation operator `!` is special
-# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is
-# just a plain character in the longer token "foo!uucp". In many other
-# languages, `"string"/foo:'string'` might be scanned as five tokens ("string",
-# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.
-#
-# The lexical analyzer for the shell command language is also somewhat unusual
-# in that it recursively invokes the parser to handle the body of `$(...)`
-# expressions which can contain arbitrary shell code. Such expressions may be
-# encountered both inside and outside of double-quoted strings.
-#
-# The lexical analyzer is responsible for consuming shell here-doc bodies which
-# extend from the line following a `<<TAG` operator until a line consisting
-# solely of `TAG`. Here-doc consumption begins when a newline is encountered.
-# It is legal for multiple here-doc `<<TAG` operators to be present on a single
-# line, in which case their bodies must be present one following the next, and
-# are consumed in the (left-to-right) order the `<<TAG` operators appear on the
-# line. A special complication is that the bodies of all here-docs must be
-# consumed when the newline is encountered even if the parse context depth has
-# changed. For instance, in `cat <<A && x=$(cat <<B &&\n`, bodies of here-docs
-# "A" and "B" must be consumed even though "A" was introduced outside the
-# recursive parse context in which "B" was introduced and in which the newline
-# is encountered.
-package Lexer;
-
-sub new {
-	my ($class, $parser, $s) = @_;
-	bless {
-		parser => $parser,
-		buff => $s,
-		lineno => 1,
-		heretags => []
-	} => $class;
-}
-
-sub scan_heredoc_tag {
-	my $self = shift @_;
-	${$self->{buff}} =~ /\G(-?)/gc;
-	my $indented = $1;
-	my $token = $self->scan_token();
-	return "<<$indented" unless $token;
-	my $tag = $token->[0];
-	$tag =~ s/['"\\]//g;
-	$$token[0] = $indented ? "\t$tag" : "$tag";
-	push(@{$self->{heretags}}, $token);
-	return "<<$indented$tag";
-}
-
-sub scan_op {
-	my ($self, $c) = @_;
-	my $b = $self->{buff};
-	return $c unless $$b =~ /\G(.)/sgc;
-	my $cc = $c . $1;
-	return scan_heredoc_tag($self) if $cc eq '<<';
-	return $cc if $cc =~ /^(?:&&|\|\||>>|;;|<&|>&|<>|>\|)$/;
-	pos($$b)--;
-	return $c;
-}
-
-sub scan_sqstring {
-	my $self = shift @_;
-	${$self->{buff}} =~ /\G([^']*'|.*\z)/sgc;
-	my $s = $1;
-	$self->{lineno} += () = $s =~ /\n/sg;
-	return "'" . $s;
-}
-
-sub scan_dqstring {
-	my $self = shift @_;
-	my $b = $self->{buff};
-	my $s = '"';
-	while (1) {
-		# slurp up non-special characters
-		$s .= $1 if $$b =~ /\G([^"\$\\]+)/gc;
-		# handle special characters
-		last unless $$b =~ /\G(.)/sgc;
-		my $c = $1;
-		$s .= '"', last if $c eq '"';
-		$s .= '$' . $self->scan_dollar(), next if $c eq '$';
-		if ($c eq '\\') {
-			$s .= '\\', last unless $$b =~ /\G(.)/sgc;
-			$c = $1;
-			$self->{lineno}++, next if $c eq "\n"; # line splice
-			# backslash escapes only $, `, ", \ in dq-string
-			$s .= '\\' unless $c =~ /^[\$`"\\]$/;
-			$s .= $c;
-			next;
-		}
-		die("internal error scanning dq-string '$c'\n");
-	}
-	$self->{lineno} += () = $s =~ /\n/sg;
-	return $s;
-}
-
-sub scan_balanced {
-	my ($self, $c1, $c2) = @_;
-	my $b = $self->{buff};
-	my $depth = 1;
-	my $s = $c1;
-	while ($$b =~ /\G([^\Q$c1$c2\E]*(?:[\Q$c1$c2\E]|\z))/gc) {
-		$s .= $1;
-		$depth++, next if $s =~ /\Q$c1\E$/;
-		$depth--;
-		last if $depth == 0;
-	}
-	$self->{lineno} += () = $s =~ /\n/sg;
-	return $s;
-}
-
-sub scan_subst {
-	my $self = shift @_;
-	my @tokens = $self->{parser}->parse(qr/^\)$/);
-	$self->{parser}->next_token(); # closing ")"
-	return @tokens;
-}
-
-sub scan_dollar {
-	my $self = shift @_;
-	my $b = $self->{buff};
-	return $self->scan_balanced('(', ')') if $$b =~ /\G\((?=\()/gc; # $((...))
-	return '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\G\(/gc; # $(...)
-	return $self->scan_balanced('{', '}') if $$b =~ /\G\{/gc; # ${...}
-	return $1 if $$b =~ /\G(\w+)/gc; # $var
-	return $1 if $$b =~ /\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.
-	return '';
-}
-
-sub swallow_heredocs {
-	my $self = shift @_;
-	my $b = $self->{buff};
-	my $tags = $self->{heretags};
-	while (my $tag = shift @$tags) {
-		my $start = pos($$b);
-		my $indent = $$tag[0] =~ s/^\t// ? '\\s*' : '';
-		$$b =~ /(?:\G|\n)$indent\Q$$tag[0]\E(?:\n|\z)/gc;
-		if (pos($$b) > $start) {
-			my $body = substr($$b, $start, pos($$b) - $start);
-			$self->{parser}->{heredocs}->{$$tag[0]} = {
-				content => substr($body, 0, length($body) - length($&)),
-				start_line => $self->{lineno},
-		        };
-			$self->{lineno} += () = $body =~ /\n/sg;
-			next;
-		}
-		push(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);
-		$$b =~ /(?:\G|\n).*\z/gc; # consume rest of input
-		my $body = substr($$b, $start, pos($$b) - $start);
-		$self->{lineno} += () = $body =~ /\n/sg;
-		last;
-	}
-}
-
-sub scan_token {
-	my $self = shift @_;
-	my $b = $self->{buff};
-	my $token = '';
-	my ($start, $startln);
-RESTART:
-	$startln = $self->{lineno};
-	$$b =~ /\G[ \t]+/gc; # skip whitespace (but not newline)
-	$start = pos($$b) || 0;
-	$self->{lineno}++, return ["\n", $start, pos($$b), $startln, $startln] if $$b =~ /\G#[^\n]*(?:\n|\z)/gc; # comment
-	while (1) {
-		# slurp up non-special characters
-		$token .= $1 if $$b =~ /\G([^\\;&|<>(){}'"\$\s]+)/gc;
-		# handle special characters
-		last unless $$b =~ /\G(.)/sgc;
-		my $c = $1;
-		pos($$b)--, last if $c =~ /^[ \t]$/; # whitespace ends token
-		pos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\n]$/;
-		$token .= $self->scan_sqstring(), next if $c eq "'";
-		$token .= $self->scan_dqstring(), next if $c eq '"';
-		$token .= $c . $self->scan_dollar(), next if $c eq '$';
-		$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq "\n";
-		$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;
-		$token = $c, last if $c =~ /^[(){}]$/;
-		if ($c eq '\\') {
-			$token .= '\\', last unless $$b =~ /\G(.)/sgc;
-			$c = $1;
-			$self->{lineno}++, next if $c eq "\n" && length($token); # line splice
-			$self->{lineno}++, goto RESTART if $c eq "\n"; # line splice
-			$token .= '\\' . $c;
-			next;
-		}
-		die("internal error scanning character '$c'\n");
-	}
-	return length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;
-}
-
-# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It
-# is a recursive descent parser very roughly modeled after section 2.10 "Shell
-# Grammar" of POSIX chapter 2 "Shell Command Language".
-package ShellParser;
-
-sub new {
-	my ($class, $s) = @_;
-	my $self = bless {
-		buff => [],
-		stop => [],
-		output => [],
-		heredocs => {},
-		insubshell => 0,
-	} => $class;
-	$self->{lexer} = Lexer->new($self, $s);
-	return $self;
-}
-
-sub next_token {
-	my $self = shift @_;
-	return pop(@{$self->{buff}}) if @{$self->{buff}};
-	return $self->{lexer}->scan_token();
-}
-
-sub untoken {
-	my $self = shift @_;
-	push(@{$self->{buff}}, @_);
-}
-
-sub peek {
-	my $self = shift @_;
-	my $token = $self->next_token();
-	return undef unless defined($token);
-	$self->untoken($token);
-	return $token;
-}
-
-sub stop_at {
-	my ($self, $token) = @_;
-	return 1 unless defined($token);
-	my $stop = ${$self->{stop}}[-1] if @{$self->{stop}};
-	return defined($stop) && $token->[0] =~ $stop;
-}
-
-sub expect {
-	my ($self, $expect) = @_;
-	my $token = $self->next_token();
-	return $token if defined($token) && $token->[0] eq $expect;
-	push(@{$self->{output}}, "?!ERR?! expected '$expect' but found '" . (defined($token) ? $token->[0] : "<end-of-input>") . "'\n");
-	$self->untoken($token) if defined($token);
-	return ();
-}
-
-sub optional_newlines {
-	my $self = shift @_;
-	my @tokens;
-	while (my $token = $self->peek()) {
-		last unless $token->[0] eq "\n";
-		push(@tokens, $self->next_token());
-	}
-	return @tokens;
-}
-
-sub parse_group {
-	my $self = shift @_;
-	return ($self->parse(qr/^}$/),
-		$self->expect('}'));
-}
-
-sub parse_subshell {
-	my $self = shift @_;
-	$self->{insubshell}++;
-	my @tokens = ($self->parse(qr/^\)$/),
-		      $self->expect(')'));
-	$self->{insubshell}--;
-	return @tokens;
-}
-
-sub parse_case_pattern {
-	my $self = shift @_;
-	my @tokens;
-	while (defined(my $token = $self->next_token())) {
-		push(@tokens, $token);
-		last if $token->[0] eq ')';
-	}
-	return @tokens;
-}
-
-sub parse_case {
-	my $self = shift @_;
-	my @tokens;
-	push(@tokens,
-	     $self->next_token(), # subject
-	     $self->optional_newlines(),
-	     $self->expect('in'),
-	     $self->optional_newlines());
-	while (1) {
-		my $token = $self->peek();
-		last unless defined($token) && $token->[0] ne 'esac';
-		push(@tokens,
-		     $self->parse_case_pattern(),
-		     $self->optional_newlines(),
-		     $self->parse(qr/^(?:;;|esac)$/)); # item body
-		$token = $self->peek();
-		last unless defined($token) && $token->[0] ne 'esac';
-		push(@tokens,
-		     $self->expect(';;'),
-		     $self->optional_newlines());
-	}
-	push(@tokens, $self->expect('esac'));
-	return @tokens;
-}
-
-sub parse_for {
-	my $self = shift @_;
-	my @tokens;
-	push(@tokens,
-	     $self->next_token(), # variable
-	     $self->optional_newlines());
-	my $token = $self->peek();
-	if (defined($token) && $token->[0] eq 'in') {
-		push(@tokens,
-		     $self->expect('in'),
-		     $self->optional_newlines());
-	}
-	push(@tokens,
-	     $self->parse(qr/^do$/), # items
-	     $self->expect('do'),
-	     $self->optional_newlines(),
-	     $self->parse_loop_body(),
-	     $self->expect('done'));
-	return @tokens;
-}
-
-sub parse_if {
-	my $self = shift @_;
-	my @tokens;
-	while (1) {
-		push(@tokens,
-		     $self->parse(qr/^then$/), # if/elif condition
-		     $self->expect('then'),
-		     $self->optional_newlines(),
-		     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body
-		my $token = $self->peek();
-		last unless defined($token) && $token->[0] eq 'elif';
-		push(@tokens, $self->expect('elif'));
-	}
-	my $token = $self->peek();
-	if (defined($token) && $token->[0] eq 'else') {
-		push(@tokens,
-		     $self->expect('else'),
-		     $self->optional_newlines(),
-		     $self->parse(qr/^fi$/)); # else body
-	}
-	push(@tokens, $self->expect('fi'));
-	return @tokens;
-}
-
-sub parse_loop_body {
-	my $self = shift @_;
-	return $self->parse(qr/^done$/);
-}
-
-sub parse_loop {
-	my $self = shift @_;
-	return ($self->parse(qr/^do$/), # condition
-		$self->expect('do'),
-		$self->optional_newlines(),
-		$self->parse_loop_body(),
-		$self->expect('done'));
-}
-
-sub parse_func {
-	my $self = shift @_;
-	return ($self->expect('('),
-		$self->expect(')'),
-		$self->optional_newlines(),
-		$self->parse_cmd()); # body
-}
-
-sub parse_bash_array_assignment {
-	my $self = shift @_;
-	my @tokens = $self->expect('(');
-	while (defined(my $token = $self->next_token())) {
-		push(@tokens, $token);
-		last if $token->[0] eq ')';
-	}
-	return @tokens;
-}
-
-my %compound = (
-	'{' => \&parse_group,
-	'(' => \&parse_subshell,
-	'case' => \&parse_case,
-	'for' => \&parse_for,
-	'if' => \&parse_if,
-	'until' => \&parse_loop,
-	'while' => \&parse_loop);
-
-sub parse_cmd {
-	my $self = shift @_;
-	my $cmd = $self->next_token();
-	return () unless defined($cmd);
-	return $cmd if $cmd->[0] eq "\n";
-
-	my $token;
-	my @tokens = $cmd;
-	if ($cmd->[0] eq '!') {
-		push(@tokens, $self->parse_cmd());
-		return @tokens;
-	} elsif (my $f = $compound{$cmd->[0]}) {
-		push(@tokens, $self->$f());
-	} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {
-		if ($cmd->[0] !~ /\w=$/) {
-			push(@tokens, $self->parse_func());
-			return @tokens;
-		}
-		my @array = $self->parse_bash_array_assignment();
-		$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);
-		$tokens[-1]->[2] = $array[$#array][2] if @array;
-	}
-
-	while (defined(my $token = $self->next_token())) {
-		$self->untoken($token), last if $self->stop_at($token);
-		push(@tokens, $token);
-		last if $token->[0] =~ /^(?:[;&\n|]|&&|\|\|)$/;
-	}
-	push(@tokens, $self->next_token()) if $tokens[-1]->[0] ne "\n" && defined($token = $self->peek()) && $token->[0] eq "\n";
-	return @tokens;
-}
-
-sub accumulate {
-	my ($self, $tokens, $cmd) = @_;
-	push(@$tokens, @$cmd);
-}
-
-sub parse {
-	my ($self, $stop) = @_;
-	push(@{$self->{stop}}, $stop);
-	goto DONE if $self->stop_at($self->peek());
-	my @tokens;
-	while (my @cmd = $self->parse_cmd()) {
-		$self->accumulate(\@tokens, \@cmd);
-		last if $self->stop_at($self->peek());
-	}
-DONE:
-	pop(@{$self->{stop}});
-	return @tokens;
-}
+use File::Basename;
+do(dirname($0) . "/lib-shell-parser.pl")
+	or die "$0: failed to load lib-shell-parser.pl: $@$!\n";
 
 # TestParser is a subclass of ShellParser which, beyond parsing shell script
 # code, is also imbued with semantic knowledge of test construction, and checks
@@ -482,9 +33,10 @@
 # the tests themselves or in behaviors being exercised by the tests. As such,
 # TestParser is only called upon to parse test bodies, not the top-level
 # scripts in which the tests are defined.
+
 package TestParser;
 
-use base 'ShellParser';
+our @ISA = ('ShellParser');
 
 sub new {
 	my $class = shift @_;
@@ -578,51 +130,10 @@
 	$self->SUPER::accumulate($tokens, $cmd);
 }
 
-# ScriptParser is a subclass of ShellParser which identifies individual test
-# definitions within test scripts, and passes each test body through TestParser
-# to identify possible problems. ShellParser detects test definitions not only
-# at the top-level of test scripts but also within compound commands such as
-# loops and function definitions.
-package ScriptParser;
+# ChainlintParser extends ScriptParser with &&-chain checking
+package ChainlintParser;
 
-use base 'ShellParser';
-
-sub new {
-	my $class = shift @_;
-	my $self = $class->SUPER::new(@_);
-	$self->{ntests} = 0;
-	$self->{nerrs} = 0;
-	return $self;
-}
-
-# extract the raw content of a token, which may be a single string or a
-# composition of multiple strings and non-string character runs; for instance,
-# `"test body"` unwraps to `test body`; `word"a b"42'c d'` to `worda b42c d`
-sub unwrap {
-	my $token = (@_ ? shift @_ : $_)->[0];
-	# simple case: 'sqstring' or "dqstring"
-	return $token if $token =~ s/^'([^']*)'$/$1/;
-	return $token if $token =~ s/^"([^"]*)"$/$1/;
-
-	# composite case
-	my ($s, $q, $escaped);
-	while (1) {
-		# slurp up non-special characters
-		$s .= $1 if $token =~ /\G([^\\'"]*)/gc;
-		# handle special characters
-		last unless $token =~ /\G(.)/sgc;
-		my $c = $1;
-		$q = undef, next if defined($q) && $c eq $q;
-		$q = $c, next if !defined($q) && $c =~ /^['"]$/;
-		if ($c eq '\\') {
-			last unless $token =~ /\G(.)/sgc;
-			$c = $1;
-			$s .= '\\' if $c eq "\n"; # preserve line splice
-		}
-		$s .= $c;
-	}
-	return $s
-}
+our @ISA = ('ScriptParser');
 
 sub format_problem {
 	local $_ = shift;
@@ -635,10 +146,10 @@
 
 sub check_test {
 	my $self = shift @_;
-	my $title = unwrap(shift @_);
+	my $title = ScriptParser::unwrap(shift @_);
 	my $body = shift @_;
 	my $lineno = $body->[3];
-	$body = unwrap($body);
+	$body = ScriptParser::unwrap($body);
 	if ($body eq '-') {
 		my $herebody = shift @_;
 		$body = $herebody->{content};
@@ -673,24 +184,8 @@
 	push(@{$self->{output}}, "$c->{blue}# chainlint: $title$c->{reset}\n$checked");
 }
 
-sub parse_cmd {
-	my $self = shift @_;
-	my @tokens = $self->SUPER::parse_cmd();
-	return @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;
-	my $n = $#tokens;
-	$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\n|]|&&|\|\|)$/;
-	my $herebody;
-	if ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {
-		$herebody = $self->{heredocs}->{$1};
-		$n--;
-	}
-	$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body
-	$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body
-	return @tokens;
-}
-
 # main contains high-level functionality for processing command-line switches,
-# feeding input test scripts to ScriptParser, and reporting results.
+# feeding input test scripts to ChainlintParser, and reporting results.
 package main;
 
 my $getnow = sub { return time(); };
@@ -803,7 +298,7 @@
 		}
 		my $s = do { local $/; <$fh> };
 		close($fh);
-		my $parser = ScriptParser->new(\$s);
+		my $parser = ChainlintParser->new(\$s);
 		1 while $parser->parse_cmd();
 		if (@{$parser->{output}}) {
 			my $c = fd_colors(1);
diff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh
index bd2d45c..b1a10dd 100644
--- a/t/for-each-ref-tests.sh
+++ b/t/for-each-ref-tests.sh
@@ -522,8 +522,8 @@
 '
 
 test_expect_success 'Give help even with invalid sort atoms' '
-	test_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&
-	grep "^usage: ${git_for_each_ref}" actual
+	${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&
+	test_grep "^usage: ${git_for_each_ref}" actual
 '
 
 cat >expected <<\EOF
@@ -622,7 +622,7 @@
 for i in "--perl --shell" "-s --python" "--python --tcl" "--tcl --perl"; do
 	test_expect_success "more than one quoting style: $i" "
 		test_must_fail ${git_for_each_ref} $i 2>err &&
-		grep '^error: more than one quoting style' err
+		test_grep '^error: more than one quoting style' err
 	"
 done
 
@@ -1892,7 +1892,7 @@
 	>in &&
 	test_must_fail ${git_for_each_ref} --format="%(refname)" \
 		--stdin refs/heads/extra <in 2>err &&
-	grep "unknown arguments supplied with --stdin" err
+	test_grep "unknown arguments supplied with --stdin" err
 '
 
 test_expect_success "${git_for_each_ref} --stdin: matches" '
@@ -1955,11 +1955,11 @@
 
 test_expect_success 'is-base atom with non-commits' '
 	${git_for_each_ref} --format="%(is-base:HEAD) %(refname)" >out 2>err &&
-	grep "(HEAD) refs/heads/main" out &&
+	test_grep "(HEAD) refs/heads/main" out &&
 
 	test_line_count = 2 err &&
-	grep "error: object .* is a commit, not a blob" err &&
-	grep "error: bad tag pointer to" err
+	test_grep "error: object .* is a commit, not a blob" err &&
+	test_grep "error: bad tag pointer to" err
 '
 
 GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)"
diff --git a/t/greplint-cat.pl b/t/greplint-cat.pl
new file mode 100644
index 0000000..fa9c3b8
--- /dev/null
+++ b/t/greplint-cat.pl
@@ -0,0 +1,27 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+# Assemble expected output for check-greplint target.
+# Usage: greplint-cat.pl <outdir> <test-name> ...
+#
+# For each <test-name>, reads greplint/<test-name>.expect and
+# prepends "greplint/<test-name>.test:" to every non-empty line,
+# matching the output format of greplint.pl.  Writes combined
+# expected output to <outdir>/expect.
+
+my $outdir = shift;
+open(my $expect, '>', "$outdir/expect")
+	or die "unable to open $outdir/expect: $!";
+
+for my $name (@ARGV) {
+	open(my $fh, '<', "greplint/$name.expect")
+		or die "unable to open greplint/$name.expect: $!";
+	while (<$fh>) {
+		print $expect "greplint/$name.test:$_";
+	}
+	close $fh;
+}
+
+close $expect;
diff --git a/t/greplint.pl b/t/greplint.pl
new file mode 100644
index 0000000..23efe27
--- /dev/null
+++ b/t/greplint.pl
@@ -0,0 +1,258 @@
+#!/usr/bin/env perl
+
+# Detect bare 'grep' used as a test assertion where 'test_grep'
+# should be used, and '! test_grep' where 'test_grep !' should
+# be used.
+#
+# The shared shell parser tokenizes test bodies so that 'grep'
+# inside heredocs, command substitutions like $(grep ...), and
+# quoted strings is collapsed into a single token and never seen
+# by our check.  A line-oriented approach would need to track
+# heredoc delimiters, nested $() depth, and cross-line pipe
+# state to avoid false positives on patterns like:
+#
+#   write_script foo.sh <<-\EOF
+#   grep pattern file    # data, not an assertion
+#   EOF
+#
+# The Lexer already handles these.
+
+use warnings;
+use strict;
+use File::Basename;
+do(dirname($0) . "/lib-shell-parser.pl")
+	or die "$0: failed to load lib-shell-parser.pl: $@$!\n";
+
+my $exit_code = 0;
+
+# GrepLintParser inherits ScriptParser's ability to find
+# test_expect_success/failure blocks and call check_test()
+# on each body.  We override check_test() to walk the token
+# stream looking for bare grep assertions.
+package GrepLintParser;
+
+our @ISA = ('ScriptParser');
+
+# After these tokens, the next token is a command word.
+# For example, in 'echo foo && grep bar file', the 'grep'
+# after '&&' is at command position and should be flagged.
+my %cmd_start = map { $_ => 1 } qw(&& || ; ;; do then else elif), "\n", '{', '(';
+
+# Tokens indicating grep's output is piped or redirected.
+my %filter_op = map { $_ => 1 } qw(| > >> <);
+
+# A token is at "command word" position if the shell would
+# interpret it as a program name rather than an argument.
+# Only 'grep' at command position is an assertion we should
+# flag; 'grep' as an argument ('test_must_fail grep') or
+# value ('for cmd in grep sed') is not.
+sub is_command_word {
+	my ($tokens, $pos) = @_;
+	return 1 if $pos == 0;
+	for (my $j = $pos - 1; $j >= 0; $j--) {
+		my $t = $tokens->[$j]->[0];
+		# After a separator or pipe, a new command starts.
+		return 1 if $cmd_start{$t} || $t eq '|';
+		# After '}' or ')', what follows is a separator or
+		# redirect on the compound command, not a new command.
+		return 0 if $t eq '}' || $t eq ')';
+		# '!' is a prefix that does not consume command
+		# position; keep scanning to find what precedes it.
+		next if $t eq '!';
+		# Any other word means we are past the command word.
+		return 0;
+	}
+	return 1;
+}
+
+# lint_ok() reports whether a bare grep carries a trailing
+# '# lint-ok' comment telling this linter to skip it.
+#
+# In practice this is needed for just one case: a grep acting
+# as a data filter whose output is consumed by a redirect or
+# pipe on an enclosing compound command (such as a subshell or
+# brace group) rather than by grep's own pipeline, e.g.
+#
+#	( grep ... && # lint-ok
+#	  sed ... ) >out
+#
+#	{ grep ... || : # lint-ok
+#	} >out
+#
+# is_filter() only scans grep's own pipeline: it stops at the
+# separator before the compound command closes and never sees
+# the outer redirect, so it would flag such a grep as an
+# assertion.  A grep that really is an assertion is better
+# written as test_grep (or a guarded test_grep when the file's
+# presence is conditional) than annotated with lint-ok.
+sub lint_ok {
+	my ($raw_lines, $ln) = @_;
+	if ($ln < 1 || $ln > @$raw_lines) {
+		warn "lint_ok: line number $ln out of range (1.." .
+		    scalar(@$raw_lines) . ")\n";
+		return 0;
+	}
+	return $raw_lines->[$ln - 1] =~ /lint-ok/;
+}
+
+# Grep is a filter (not an assertion) if it receives piped
+# input or sends its output to a pipe or redirect.  Check
+# both directions from grep's position in the token stream.
+sub is_filter {
+	my ($tokens, $pos) = @_;
+	# Backward: is grep receiving piped input?
+	# Newlines don't break pipes ('cmd |\n grep' is one
+	# pipeline), so skip past them.
+	for (my $j = $pos - 1; $j >= 0; $j--) {
+		my $t = $tokens->[$j]->[0];
+		return 1 if $t eq '|';
+		next if $t eq "\n";
+		last if $cmd_start{$t} || $t eq '}' || $t eq ')';
+	}
+	# Forward: is grep piping or redirecting output?
+	# Unlike the backward scan, we do not skip newlines here:
+	# a bare newline is a command boundary, and redirects or
+	# pipes must appear on the same line as grep (or after a
+	# line continuation, which the Lexer consumes).
+	for (my $j = $pos + 1; $j < @$tokens; $j++) {
+		my $t = $tokens->[$j]->[0];
+		return 0 if $cmd_start{$t};
+		return 1 if $filter_op{$t};
+	}
+	return 0;
+}
+
+# Map a body-relative line number to a file line number.
+# For double-quoted bodies, backslash-continuation lines
+# (\<newline>) are consumed by the Lexer without appearing
+# in the body text, so the inner parser sees fewer lines
+# than the source file has.  We walk the source lines to
+# count continuations and adjust accordingly.
+sub body_to_file_line {
+	my ($body_lineno, $body_token, $raw_lines, $body_start) = @_;
+	my $body_text = $body_token->[0];
+	my $body_end_line = $body_token->[4];
+	unless ($body_start && $body_start >= 1) {
+		warn "body_start is not a positive integer\n";
+		return $body_lineno;
+	}
+	my $file_lineno = $body_lineno + $body_start - 1;
+	# Only double-quoted bodies have line splices.
+	return $file_lineno unless $body_text =~ /^"/;
+	my $adj = 0;
+	my $lines_seen = 0;
+	unless ($body_end_line && $body_end_line >= $body_start) {
+		warn "body_end_line is not set for double-quoted body\n";
+		return $file_lineno;
+	}
+	my $end = $body_end_line;
+	if ($end > @$raw_lines) {
+		warn "body_end_line ($end) exceeds file length (" .
+		    scalar(@$raw_lines) . ")\n";
+		return $file_lineno;
+	}
+	my $src_ln = $body_start;
+	while ($src_ln <= $end && $lines_seen < $body_lineno) {
+		my $line = $raw_lines->[$src_ln - 1];
+		# Odd trailing backslashes = continuation (\<nl>).
+		# Even = escaped backslashes (\\), not a continuation.
+		if ($line =~ /(\\*)$/ && length($1) % 2 == 1) {
+			$adj++;
+		} else {
+			$lines_seen++;
+		}
+		$src_ln++;
+	}
+	if ($lines_seen < $body_lineno) {
+		warn "body_lineno ($body_lineno) not found within body range " .
+		    "($body_start..$end)\n";
+	}
+	return $file_lineno + $adj;
+}
+
+# ScriptParser calls this for each test body found in the script.
+sub check_test {
+	my $self = shift @_;
+	my $title = ScriptParser::unwrap(shift @_);
+	my $body_token = shift @_;
+	my $body_start = $body_token->[3];
+	my $body = ScriptParser::unwrap($body_token);
+	# Handle heredoc-style test bodies:
+	#   test_expect_success 'title' - <<\EOF
+	#   grep pattern file
+	#   EOF
+	# The '-' signals that the body follows as a heredoc.
+	if ($body eq '-') {
+		my $herebody = shift @_;
+		if ($herebody) {
+			$body = $herebody->{content};
+			$body_start = $herebody->{start_line};
+		}
+	}
+	return unless $body;
+
+	my $raw_lines = $self->{raw_lines};
+
+	# The outer parser gives us the body as an opaque string.
+	# Parse it to get individual tokens with command boundaries.
+	my $parser = ShellParser->new(\$body);
+	my @tokens = $parser->parse();
+
+	my $file = $self->{file};
+
+	for (my $i = 0; $i < @tokens; $i++) {
+		my $text = $tokens[$i]->[0];
+		next unless is_command_word(\@tokens, $i);
+
+		my $token_lineno = $tokens[$i]->[3];
+		unless (defined($token_lineno) && $token_lineno >= 1) {
+			warn "token has no line number\n";
+			next;
+		}
+		my $file_lineno = body_to_file_line(
+			$token_lineno,
+			$body_token, $raw_lines, $body_start);
+
+		# '!' negates the exit code without consuming command
+		# position.  '! test_grep' is an anti-pattern because
+		# test_grep only prints diagnostics on grep failure,
+		# and '!' inverts after that decision is already made.
+		if ($text eq '!') {
+			if ($i + 1 < @tokens &&
+			    $tokens[$i + 1]->[0] eq 'test_grep' &&
+			    !lint_ok($raw_lines, $file_lineno)) {
+				print "$file:$file_lineno: error: ",
+				    'use "test_grep !" instead of ',
+				    '"! test_grep"', "\n";
+				$exit_code = 1;
+			}
+			next;
+		}
+
+		# Bare grep as a command (not a filter) is a test
+		# assertion that should use test_grep for better
+		# failure diagnostics.
+		if ($text eq 'grep' &&
+		    !is_filter(\@tokens, $i) &&
+		    !lint_ok($raw_lines, $file_lineno)) {
+			print "$file:$file_lineno: error: ",
+			    "bare grep outside pipeline ",
+			    "(use test_grep)\n";
+			$exit_code = 1;
+		}
+	}
+}
+
+package main;
+
+for my $file (@ARGV) {
+	open(my $fh, '<:unix:crlf', $file) or die "$0: $file: $!\n";
+	my @raw_lines = <$fh>;
+	close $fh;
+	my $s = join('', @raw_lines);
+	my $parser = GrepLintParser->new(\$s);
+	$parser->{file} = $file;
+	$parser->{raw_lines} = \@raw_lines;
+	$parser->parse();
+}
+exit $exit_code;
diff --git a/t/greplint/bare-grep-after-and.expect b/t/greplint/bare-grep-after-and.expect
new file mode 100644
index 0000000..7da1b21
--- /dev/null
+++ b/t/greplint/bare-grep-after-and.expect
@@ -0,0 +1 @@
+3: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-after-and.test b/t/greplint/bare-grep-after-and.test
new file mode 100644
index 0000000..c5b26e4
--- /dev/null
+++ b/t/greplint/bare-grep-after-and.test
@@ -0,0 +1,4 @@
+test_expect_success 'grep after && is flagged' '
+	cmd &&
+	grep pattern file
+'
diff --git a/t/greplint/bare-grep-after-semicolon.expect b/t/greplint/bare-grep-after-semicolon.expect
new file mode 100644
index 0000000..7da1b21
--- /dev/null
+++ b/t/greplint/bare-grep-after-semicolon.expect
@@ -0,0 +1 @@
+3: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-after-semicolon.test b/t/greplint/bare-grep-after-semicolon.test
new file mode 100644
index 0000000..c1e468d
--- /dev/null
+++ b/t/greplint/bare-grep-after-semicolon.test
@@ -0,0 +1,4 @@
+test_expect_success 'grep after semicolon is flagged' '
+	echo hello;
+	grep pattern file
+'
diff --git a/t/greplint/bare-grep-compound-body.expect b/t/greplint/bare-grep-compound-body.expect
new file mode 100644
index 0000000..eb6210f
--- /dev/null
+++ b/t/greplint/bare-grep-compound-body.expect
@@ -0,0 +1,3 @@
+4: error: bare grep outside pipeline (use test_grep)
+8: error: bare grep outside pipeline (use test_grep)
+15: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-compound-body.test b/t/greplint/bare-grep-compound-body.test
new file mode 100644
index 0000000..72472ed
--- /dev/null
+++ b/t/greplint/bare-grep-compound-body.test
@@ -0,0 +1,17 @@
+test_expect_success 'grep after then/do/else is flagged' '
+	if true
+	then
+		grep pattern file
+	fi &&
+	while true
+	do
+		grep pattern file &&
+		break
+	done &&
+	if true
+	then
+		echo yes
+	else
+		grep pattern file
+	fi
+'
diff --git a/t/greplint/bare-grep-count-mode.expect b/t/greplint/bare-grep-count-mode.expect
new file mode 100644
index 0000000..8922d35
--- /dev/null
+++ b/t/greplint/bare-grep-count-mode.expect
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-count-mode.test b/t/greplint/bare-grep-count-mode.test
new file mode 100644
index 0000000..b9c0f18
--- /dev/null
+++ b/t/greplint/bare-grep-count-mode.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep -c is flagged (not special-cased)' '
+	grep -c pattern file
+'
diff --git a/t/greplint/bare-grep-explicit-pattern.expect b/t/greplint/bare-grep-explicit-pattern.expect
new file mode 100644
index 0000000..8922d35
--- /dev/null
+++ b/t/greplint/bare-grep-explicit-pattern.expect
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-explicit-pattern.test b/t/greplint/bare-grep-explicit-pattern.test
new file mode 100644
index 0000000..69d8651
--- /dev/null
+++ b/t/greplint/bare-grep-explicit-pattern.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep -e is flagged' '
+	grep -e pattern file
+'
diff --git a/t/greplint/bare-grep-flags.expect b/t/greplint/bare-grep-flags.expect
new file mode 100644
index 0000000..8922d35
--- /dev/null
+++ b/t/greplint/bare-grep-flags.expect
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-flags.test b/t/greplint/bare-grep-flags.test
new file mode 100644
index 0000000..9ca0e10
--- /dev/null
+++ b/t/greplint/bare-grep-flags.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep -E is flagged' '
+	grep -E "pat+ern" file
+'
diff --git a/t/greplint/bare-grep-lint-ok.expect b/t/greplint/bare-grep-lint-ok.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/bare-grep-lint-ok.expect
diff --git a/t/greplint/bare-grep-lint-ok.test b/t/greplint/bare-grep-lint-ok.test
new file mode 100644
index 0000000..335b902
--- /dev/null
+++ b/t/greplint/bare-grep-lint-ok.test
@@ -0,0 +1,4 @@
+test_expect_success 'grep with lint-ok annotation is not flagged' '
+	grep pattern file && # lint-ok
+	echo done
+'
diff --git a/t/greplint/bare-grep-negated.expect b/t/greplint/bare-grep-negated.expect
new file mode 100644
index 0000000..8922d35
--- /dev/null
+++ b/t/greplint/bare-grep-negated.expect
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-negated.test b/t/greplint/bare-grep-negated.test
new file mode 100644
index 0000000..ef789bc
--- /dev/null
+++ b/t/greplint/bare-grep-negated.test
@@ -0,0 +1,3 @@
+test_expect_success 'negated grep is flagged' '
+	! grep pattern file
+'
diff --git a/t/greplint/bare-grep-pattern-file.expect b/t/greplint/bare-grep-pattern-file.expect
new file mode 100644
index 0000000..8922d35
--- /dev/null
+++ b/t/greplint/bare-grep-pattern-file.expect
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-pattern-file.test b/t/greplint/bare-grep-pattern-file.test
new file mode 100644
index 0000000..d70035e
--- /dev/null
+++ b/t/greplint/bare-grep-pattern-file.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep -f is flagged' '
+	grep -f patterns.txt file
+'
diff --git a/t/greplint/bare-grep-simple.expect b/t/greplint/bare-grep-simple.expect
new file mode 100644
index 0000000..8922d35
--- /dev/null
+++ b/t/greplint/bare-grep-simple.expect
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-simple.test b/t/greplint/bare-grep-simple.test
new file mode 100644
index 0000000..3a8d5f9
--- /dev/null
+++ b/t/greplint/bare-grep-simple.test
@@ -0,0 +1,3 @@
+test_expect_success 'bare grep is flagged' '
+	grep pattern file
+'
diff --git a/t/greplint/bare-grep-subshell.expect b/t/greplint/bare-grep-subshell.expect
new file mode 100644
index 0000000..7da1b21
--- /dev/null
+++ b/t/greplint/bare-grep-subshell.expect
@@ -0,0 +1 @@
+3: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/bare-grep-subshell.test b/t/greplint/bare-grep-subshell.test
new file mode 100644
index 0000000..1fab2d3
--- /dev/null
+++ b/t/greplint/bare-grep-subshell.test
@@ -0,0 +1,5 @@
+test_expect_success 'grep in subshell is flagged' '
+	(
+		grep pattern file
+	)
+'
diff --git a/t/greplint/dqstring-continuation-offset.expect b/t/greplint/dqstring-continuation-offset.expect
new file mode 100644
index 0000000..8185bfd
--- /dev/null
+++ b/t/greplint/dqstring-continuation-offset.expect
@@ -0,0 +1 @@
+10: error: bare grep outside pipeline (use test_grep)
diff --git a/t/greplint/dqstring-continuation-offset.test b/t/greplint/dqstring-continuation-offset.test
new file mode 100644
index 0000000..b332bd8
--- /dev/null
+++ b/t/greplint/dqstring-continuation-offset.test
@@ -0,0 +1,11 @@
+# Double-quoted test bodies with backslash-continuation lines:
+# the splice adjustment in check_test compensates for \<newline>
+# lines that the lexer consumes without emitting into the body
+# text, so the reported line number matches the source.
+test_expect_success 'dqstring continuation offset' "
+	x=\$(echo \
+		hello) &&
+	y=\$(echo \
+		world) &&
+	grep pattern file
+"
diff --git a/t/greplint/filter-command-substitution.expect b/t/greplint/filter-command-substitution.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/filter-command-substitution.expect
diff --git a/t/greplint/filter-command-substitution.test b/t/greplint/filter-command-substitution.test
new file mode 100644
index 0000000..bd9a100
--- /dev/null
+++ b/t/greplint/filter-command-substitution.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep in command substitution is not flagged' '
+	x=$(grep pattern file)
+'
diff --git a/t/greplint/filter-pipe-input.expect b/t/greplint/filter-pipe-input.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/filter-pipe-input.expect
diff --git a/t/greplint/filter-pipe-input.test b/t/greplint/filter-pipe-input.test
new file mode 100644
index 0000000..cb5c0d9
--- /dev/null
+++ b/t/greplint/filter-pipe-input.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep receiving pipe input is not flagged' '
+	cmd | grep pattern
+'
diff --git a/t/greplint/filter-pipe-output.expect b/t/greplint/filter-pipe-output.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/filter-pipe-output.expect
diff --git a/t/greplint/filter-pipe-output.test b/t/greplint/filter-pipe-output.test
new file mode 100644
index 0000000..0134008
--- /dev/null
+++ b/t/greplint/filter-pipe-output.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep piping to another command is not flagged' '
+	grep pattern file | wc -l
+'
diff --git a/t/greplint/filter-redirect-output.expect b/t/greplint/filter-redirect-output.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/filter-redirect-output.expect
diff --git a/t/greplint/filter-redirect-output.test b/t/greplint/filter-redirect-output.test
new file mode 100644
index 0000000..ee70ba7
--- /dev/null
+++ b/t/greplint/filter-redirect-output.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep with output redirect is not flagged' '
+	grep pattern file >output
+'
diff --git a/t/greplint/filter-stdin-redirect.expect b/t/greplint/filter-stdin-redirect.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/filter-stdin-redirect.expect
diff --git a/t/greplint/filter-stdin-redirect.test b/t/greplint/filter-stdin-redirect.test
new file mode 100644
index 0000000..292db77
--- /dev/null
+++ b/t/greplint/filter-stdin-redirect.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep reading from stdin redirect is not flagged' '
+	grep pattern <input
+'
diff --git a/t/greplint/grep-as-argument.expect b/t/greplint/grep-as-argument.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/grep-as-argument.expect
diff --git a/t/greplint/grep-as-argument.test b/t/greplint/grep-as-argument.test
new file mode 100644
index 0000000..7157228
--- /dev/null
+++ b/t/greplint/grep-as-argument.test
@@ -0,0 +1,3 @@
+test_expect_success 'grep as argument to another command is not flagged' '
+	test_must_fail grep pattern file
+'
diff --git a/t/greplint/grep-as-value.expect b/t/greplint/grep-as-value.expect
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/t/greplint/grep-as-value.expect
diff --git a/t/greplint/grep-as-value.test b/t/greplint/grep-as-value.test
new file mode 100644
index 0000000..92926f6
--- /dev/null
+++ b/t/greplint/grep-as-value.test
@@ -0,0 +1,6 @@
+test_expect_success 'grep as value in for-loop is not flagged' '
+	for cmd in grep sed awk
+	do
+		echo $cmd
+	done
+'
diff --git a/t/greplint/wrong-negation.expect b/t/greplint/wrong-negation.expect
new file mode 100644
index 0000000..7dc52f6
--- /dev/null
+++ b/t/greplint/wrong-negation.expect
@@ -0,0 +1 @@
+2: error: use "test_grep !" instead of "! test_grep"
diff --git a/t/greplint/wrong-negation.test b/t/greplint/wrong-negation.test
new file mode 100644
index 0000000..542fbd9
--- /dev/null
+++ b/t/greplint/wrong-negation.test
@@ -0,0 +1,3 @@
+test_expect_success 'wrong negation of test_grep is flagged' '
+	! test_grep pattern file
+'
diff --git a/t/helper/test-bitmap.c b/t/helper/test-bitmap.c
index b130832..8547ef6 100644
--- a/t/helper/test-bitmap.c
+++ b/t/helper/test-bitmap.c
@@ -52,7 +52,7 @@ static int add_packed_object(const struct object_id *oid,
 
 	entry = packlist_alloc(packed, oid);
 	entry->idx.offset = nth_packed_object_offset(pack, pos);
-	if (packed_object_info(pack, entry->idx.offset, &oi) < 0)
+	if (packed_object_info(NULL, pack, entry->idx.offset, &oi) < 0)
 		die("could not get type of object %s",
 		    oid_to_hex(oid));
 	oe_set_type(entry, type);
diff --git a/t/helper/test-hash-speed.c b/t/helper/test-hash-speed.c
index fbf67fe..89b0268 100644
--- a/t/helper/test-hash-speed.c
+++ b/t/helper/test-hash-speed.c
@@ -5,7 +5,7 @@
 
 static inline void compute_hash(const struct git_hash_algo *algo, struct git_hash_ctx *ctx, uint8_t *final, const void *p, size_t len)
 {
-	algo->init_fn(ctx);
+	git_hash_init(ctx, algo);
 	git_hash_update(ctx, p, len);
 	git_hash_final(final, ctx);
 }
diff --git a/t/helper/test-hash.c b/t/helper/test-hash.c
index f0ee61c..1f71636 100644
--- a/t/helper/test-hash.c
+++ b/t/helper/test-hash.c
@@ -29,7 +29,7 @@ int cmd_hash_impl(int ac, const char **av, int algo, int unsafe)
 			die("OOPS");
 	}
 
-	algop->init_fn(&ctx);
+	git_hash_init(&ctx, algop);
 
 	while (1) {
 		ssize_t sz, this_sz;
diff --git a/t/helper/test-lazy-init-name-hash.c b/t/helper/test-lazy-init-name-hash.c
index e542985..43cead6 100644
--- a/t/helper/test-lazy-init-name-hash.c
+++ b/t/helper/test-lazy-init-name-hash.c
@@ -218,7 +218,7 @@ int cmd__lazy_init_name_hash(int argc, const char **argv)
 	/*
 	 * istate->dir_hash is only created when ignore_case is set.
 	 */
-	ignore_case = 1;
+	repo_config_values(the_repository)->ignore_case = 1;
 
 	if (dump) {
 		if (perf || analyze > 0)
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 3866d0a..5a9a305 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -84,7 +84,7 @@ static const char **get_store(const char **argv, struct ref_store **refs)
 
 		*refs = repo_get_submodule_ref_store(the_repository, gitdir);
 	} else if (skip_prefix(argv[0], "worktree:", &gitdir)) {
-		struct worktree **p, **worktrees = get_worktrees();
+		struct worktree **p, **worktrees = get_worktrees(the_repository);
 
 		for (p = worktrees; *p; p++) {
 			struct worktree *wt = *p;
diff --git a/t/helper/test-synthesize.c b/t/helper/test-synthesize.c
index 3fa534f..fd116c8 100644
--- a/t/helper/test-synthesize.c
+++ b/t/helper/test-synthesize.c
@@ -25,8 +25,7 @@ static const unsigned char zeros[BLOCK_SIZE];
  * Updates the pack checksum context.
  */
 static void write_uncompressed_zlib(FILE *f, struct git_hash_ctx *pack_ctx,
-				    const void *data, size_t len,
-				    const struct git_hash_algo *algo)
+				    const void *data, size_t len)
 {
 	unsigned char zlib_header[2] = { 0x78, 0x01 }; /* CMF, FLG */
 	unsigned char block_header[5];
@@ -37,7 +36,7 @@ static void write_uncompressed_zlib(FILE *f, struct git_hash_ctx *pack_ctx,
 
 	/* Write zlib header */
 	fwrite_or_die(f, zlib_header, sizeof(zlib_header));
-	algo->update_fn(pack_ctx, zlib_header, 2);
+	git_hash_update(pack_ctx, zlib_header, 2);
 
 	/* Write uncompressed blocks (max 64KB each) */
 	do {
@@ -52,11 +51,11 @@ static void write_uncompressed_zlib(FILE *f, struct git_hash_ctx *pack_ctx,
 		block_header[4] = block_header[2] ^ 0xff;
 
 		fwrite_or_die(f, block_header, sizeof(block_header));
-		algo->update_fn(pack_ctx, block_header, 5);
+		git_hash_update(pack_ctx, block_header, 5);
 
 		if (block_len) {
 			fwrite_or_die(f, block_data, block_len);
-			algo->update_fn(pack_ctx, block_data, block_len);
+			git_hash_update(pack_ctx, block_data, block_len);
 			adler = adler32(adler, block_data, block_len);
 		}
 
@@ -68,7 +67,7 @@ static void write_uncompressed_zlib(FILE *f, struct git_hash_ctx *pack_ctx,
 	/* Write adler32 checksum */
 	put_be32(adler_buf, adler);
 	fwrite_or_die(f, adler_buf, sizeof(adler_buf));
-	algo->update_fn(pack_ctx, adler_buf, 4);
+	git_hash_update(pack_ctx, adler_buf, 4);
 }
 
 /*
@@ -92,24 +91,24 @@ static void write_pack_object(FILE *f, struct git_hash_ctx *pack_ctx,
 						       sizeof(pack_header),
 						       type, len);
 	fwrite_or_die(f, pack_header, pack_header_len);
-	algo->update_fn(pack_ctx, pack_header, pack_header_len);
+	git_hash_update(pack_ctx, pack_header, pack_header_len);
 
 	/* Write the data as uncompressed zlib */
-	write_uncompressed_zlib(f, pack_ctx, data, len, algo);
+	write_uncompressed_zlib(f, pack_ctx, data, len);
 
-	algo->init_fn(&ctx);
+	git_hash_init(&ctx, algo);
 	object_header_len = format_object_header(object_header,
 						 sizeof(object_header),
 						 type, len);
-	algo->update_fn(&ctx, object_header, object_header_len);
+	git_hash_update(&ctx, object_header, object_header_len);
 	if (data)
-		algo->update_fn(&ctx, data, len);
+		git_hash_update(&ctx, data, len);
 	else {
 		for (size_t i = len / BLOCK_SIZE; i; i--)
-			algo->update_fn(&ctx, zeros, BLOCK_SIZE);
-		algo->update_fn(&ctx, zeros, len % BLOCK_SIZE);
+			git_hash_update(&ctx, zeros, BLOCK_SIZE);
+		git_hash_update(&ctx, zeros, len % BLOCK_SIZE);
 	}
-	algo->final_oid_fn(oid, &ctx);
+	git_hash_final_oid(oid, &ctx);
 }
 
 /*
@@ -430,11 +429,11 @@ static int generate_pack_with_large_object(const char *path, size_t blob_size,
 
 	f = xfopen(path, "wb");
 
-	algo->init_fn(&pack_ctx);
+	git_hash_init(&pack_ctx, algo);
 
 	/* Write pack header */
 	fwrite_or_die(f, &pack_header, sizeof(pack_header));
-	algo->update_fn(&pack_ctx, &pack_header, sizeof(pack_header));
+	git_hash_update(&pack_ctx, &pack_header, sizeof(pack_header));
 
 	/* 1. Write the large blob */
 	write_pack_object(f, &pack_ctx, OBJ_BLOB, NULL, blob_size, &blob_oid, algo);
@@ -472,7 +471,7 @@ static int generate_pack_with_large_object(const char *path, size_t blob_size,
 	write_pack_object(f, &pack_ctx, OBJ_COMMIT, buf.buf, buf.len, &final_commit_oid, algo);
 
 	/* Write pack trailer (checksum) */
-	algo->final_fn(pack_hash, &pack_ctx);
+	git_hash_final(pack_hash, &pack_ctx);
 	fwrite_or_die(f, pack_hash, algo->rawsz);
 	if (fclose(f))
 		die_errno(_("could not close '%s'"), path);
diff --git a/t/lib-bitmap.sh b/t/lib-bitmap.sh
index 62aa674..2de6646 100644
--- a/t/lib-bitmap.sh
+++ b/t/lib-bitmap.sh
@@ -173,7 +173,7 @@
 
 	test_expect_success "bitmap --objects handles non-commit objects ($state, $branch)" '
 		git rev-list --objects --use-bitmap-index $branch tagged-blob >actual &&
-		grep $blob actual
+		test_grep $blob actual
 	'
 }
 
@@ -242,16 +242,16 @@
 		GIT_PROGRESS_DELAY=0 \
 			git pack-objects --all --stdout --progress \
 			</dev/null >/dev/null 2>stderr &&
-		grep "Enumerating objects: $count, done" stderr &&
-		grep "pack-reused $count" stderr &&
+		test_grep "Enumerating objects: $count, done" stderr &&
+		test_grep "pack-reused $count" stderr &&
 
 		# now the same but with one non-reused object
 		git commit --allow-empty -m "an extra commit object" &&
 		GIT_PROGRESS_DELAY=0 \
 			git pack-objects --all --stdout --progress \
 			</dev/null >/dev/null 2>stderr &&
-		grep "Enumerating objects: $((count+1)), done" stderr &&
-		grep "pack-reused $count" stderr
+		test_grep "Enumerating objects: $((count+1)), done" stderr &&
+		test_grep "pack-reused $count" stderr
 	'
 }
 
@@ -283,7 +283,7 @@
 		then
 			test_path_is_file $midx-$(midx_checksum $objdir).rev
 		fi &&
-		grep "\"category\":\"load_midx_revindex\",\"key\":\"source\",\"value\":\"$kind\"" event.trace
+		test_grep "\"category\":\"load_midx_revindex\",\"key\":\"source\",\"value\":\"$kind\"" event.trace
 	'
 }
 
diff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh
index de09b6b..794478a 100644
--- a/t/lib-bundle-uri-protocol.sh
+++ b/t/lib-bundle-uri-protocol.sh
@@ -63,9 +63,9 @@
 		>actual 2>err &&
 
 	# Server responded using protocol v2
-	grep "< version 2" log &&
+	test_grep "< version 2" log &&
 
-	! grep bundle-uri log
+	test_grep ! bundle-uri log
 '
 
 test_expect_success "connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: have bundle-uri" '
@@ -78,10 +78,10 @@
 		>actual 2>err &&
 
 	# Server responded using protocol v2
-	grep "< version 2" log &&
+	test_grep "< version 2" log &&
 
 	# Server advertised bundle-uri capability
-	grep "< bundle-uri" log
+	test_grep "< bundle-uri" log
 '
 
 test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: request bundle-uris" '
@@ -95,13 +95,13 @@
 		>actual 2>err &&
 
 	# Server responded using protocol v2
-	grep "< version 2" log &&
+	test_grep "< version 2" log &&
 
 	# Server advertised bundle-uri capability
-	grep "< bundle-uri" log &&
+	test_grep "< bundle-uri" log &&
 
 	# Client did not issue bundle-uri command
-	! grep "> command=bundle-uri" log &&
+	test_grep ! "> command=bundle-uri" log &&
 
 	GIT_TRACE_PACKET="$PWD/log" \
 	git \
@@ -111,13 +111,13 @@
 		>actual 2>err &&
 
 	# Server responded using protocol v2
-	grep "< version 2" log &&
+	test_grep "< version 2" log &&
 
 	# Server advertised bundle-uri capability
-	grep "< bundle-uri" log &&
+	test_grep "< bundle-uri" log &&
 
 	# Client issued bundle-uri command
-	grep "> command=bundle-uri" log &&
+	test_grep "> command=bundle-uri" log &&
 
 	GIT_TRACE_PACKET="$PWD/log3" \
 	git \
@@ -128,13 +128,13 @@
 		>actual 2>err &&
 
 	# Server responded using protocol v2
-	grep "< version 2" log3 &&
+	test_grep "< version 2" log3 &&
 
 	# Server advertised bundle-uri capability
-	grep "< bundle-uri" log3 &&
+	test_grep "< bundle-uri" log3 &&
 
 	# Client did not issue bundle-uri command (--bundle-uri override)
-	! grep "> command=bundle-uri" log3
+	test_grep ! "> command=bundle-uri" log3
 '
 
 # The remaining tests will all assume transfer.bundleURI=true
@@ -214,3 +214,26 @@
 		>actual &&
 	test_cmp_config_output expect actual
 '
+
+test_expect_success "test bundle-uri with $BUNDLE_URI_PROTOCOL:// using protocol v2 with empty value" '
+	test_config -C "$BUNDLE_URI_PARENT" \
+		bundle.bundle1.uri "$BUNDLE_URI_BUNDLE_URI_ESCAPED-1.bdl" &&
+	test_config -C "$BUNDLE_URI_PARENT" \
+		bundle.bundle2.uri "" &&
+
+	# The empty bundle.bundle2.uri value is invalid configuration and the
+	# server must not advertise it to the client.
+	cat >expect <<-EOF &&
+	[bundle]
+		version = 1
+		mode = all
+	[bundle "bundle1"]
+		uri = $BUNDLE_URI_BUNDLE_URI_ESCAPED-1.bdl
+	EOF
+
+	test-tool bundle-uri \
+		ls-remote \
+		"$BUNDLE_URI_REPO_URI" \
+		>actual &&
+	test_cmp_config_output expect actual
+'
diff --git a/t/lib-cat-file.sh b/t/lib-cat-file.sh
new file mode 100644
index 0000000..7c2e877
--- /dev/null
+++ b/t/lib-cat-file.sh
@@ -0,0 +1,16 @@
+# Library of git-cat-file related test functions.
+
+# Print a string without a trailing newline.
+echo_without_newline () {
+	printf '%s' "$*"
+}
+
+# Print a string without newlines and replace them with a NUL character (\0).
+echo_without_newline_nul () {
+	echo_without_newline "$@" | tr '\n' '\0'
+}
+
+# Calculate the length of a string.
+strlen () {
+	echo_without_newline "$1" | wc -c | sed -e 's/^ *//'
+}
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
index fc64644..a216e53 100644
--- a/t/lib-httpd.sh
+++ b/t/lib-httpd.sh
@@ -275,7 +275,7 @@
 	'
 
 	test_expect_success 'non-fast-forward push show ref status' '
-		grep "^ ! \[rejected\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" output
+		test_grep "^ ! \[rejected\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" output
 	'
 
 	test_expect_success 'non-fast-forward push shows help message' '
diff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh
index bf952ef..1eae8f6 100644
--- a/t/lib-log-graph.sh
+++ b/t/lib-log-graph.sh
@@ -26,3 +26,8 @@
 	test_decode_color <output.colors.raw | sed "s/ *\$//" >output.colors &&
 	test_cmp expect.colors output.colors
 }
+
+lib_test_check_graph () {
+	cat >expect &&
+	lib_test_cmp_graph --format=%s "$@"
+}
diff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl
new file mode 100644
index 0000000..d78e185
--- /dev/null
+++ b/t/lib-shell-parser.pl
@@ -0,0 +1,534 @@
+# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>
+#
+# Shared shell script parser for test lint tools. Provides Lexer,
+# ShellParser, and ScriptParser. Subclass ScriptParser and override
+# check_test() to implement lint checks.
+
+use strict;
+use warnings;
+
+# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3
+# "Token Recognition" of POSIX chapter 2 "Shell Command Language". Although
+# similar to lexical analyzers for other languages, this one differs in a few
+# substantial ways due to quirks of the shell command language.
+#
+# For instance, in many languages, newline is just whitespace like space or
+# TAB, but in shell a newline is a command separator, thus a distinct lexical
+# token. A newline is significant and returned as a distinct token even at the
+# end of a shell comment.
+#
+# In other languages, `1+2` would typically be scanned as three tokens
+# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar
+# `1 + 2`, which embeds whitepaces, is scanned as three token in shell, as well.
+# In shell, several characters with special meaning lose that meaning when not
+# surrounded by whitespace. For instance, the negation operator `!` is special
+# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is
+# just a plain character in the longer token "foo!uucp". In many other
+# languages, `"string"/foo:'string'` might be scanned as five tokens ("string",
+# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.
+#
+# The lexical analyzer for the shell command language is also somewhat unusual
+# in that it recursively invokes the parser to handle the body of `$(...)`
+# expressions which can contain arbitrary shell code. Such expressions may be
+# encountered both inside and outside of double-quoted strings.
+#
+# The lexical analyzer is responsible for consuming shell here-doc bodies which
+# extend from the line following a `<<TAG` operator until a line consisting
+# solely of `TAG`. Here-doc consumption begins when a newline is encountered.
+# It is legal for multiple here-doc `<<TAG` operators to be present on a single
+# line, in which case their bodies must be present one following the next, and
+# are consumed in the (left-to-right) order the `<<TAG` operators appear on the
+# line. A special complication is that the bodies of all here-docs must be
+# consumed when the newline is encountered even if the parse context depth has
+# changed. For instance, in `cat <<A && x=$(cat <<B &&\n`, bodies of here-docs
+# "A" and "B" must be consumed even though "A" was introduced outside the
+# recursive parse context in which "B" was introduced and in which the newline
+# is encountered.
+package Lexer;
+
+sub new {
+	my ($class, $parser, $s) = @_;
+	bless {
+		parser => $parser,
+		buff => $s,
+		lineno => 1,
+		heretags => []
+	} => $class;
+}
+
+sub scan_heredoc_tag {
+	my $self = shift @_;
+	${$self->{buff}} =~ /\G(-?)/gc;
+	my $indented = $1;
+	my $token = $self->scan_token();
+	return "<<$indented" unless $token;
+	my $tag = $token->[0];
+	$tag =~ s/['"\\]//g;
+	$$token[0] = $indented ? "\t$tag" : "$tag";
+	push(@{$self->{heretags}}, $token);
+	return "<<$indented$tag";
+}
+
+sub scan_op {
+	my ($self, $c) = @_;
+	my $b = $self->{buff};
+	return $c unless $$b =~ /\G(.)/sgc;
+	my $cc = $c . $1;
+	return scan_heredoc_tag($self) if $cc eq '<<';
+	return $cc if $cc =~ /^(?:&&|\|\||>>|;;|<&|>&|<>|>\|)$/;
+	pos($$b)--;
+	return $c;
+}
+
+sub scan_sqstring {
+	my $self = shift @_;
+	${$self->{buff}} =~ /\G([^']*'|.*\z)/sgc;
+	my $s = $1;
+	$self->{lineno} += () = $s =~ /\n/sg;
+	return "'" . $s;
+}
+
+sub scan_dqstring {
+	my $self = shift @_;
+	my $b = $self->{buff};
+	my $s = '"';
+	while (1) {
+		# Slurp non-special characters; count newlines here because
+		# newlines inside $() are already counted by the recursive parse.
+		if ($$b =~ /\G([^"\$\\]+)/gc) {
+			$s .= $1;
+			$self->{lineno} += $1 =~ tr/\n//;
+		}
+		# handle special characters
+		last unless $$b =~ /\G(.)/sgc;
+		my $c = $1;
+		$s .= '"', last if $c eq '"';
+		$s .= '$' . $self->scan_dollar(), next if $c eq '$';
+		if ($c eq '\\') {
+			$s .= '\\', last unless $$b =~ /\G(.)/sgc;
+			$c = $1;
+			$self->{lineno}++, next if $c eq "\n"; # line splice
+			# backslash escapes only $, `, ", \ in dq-string
+			$s .= '\\' unless $c =~ /^[\$`"\\]$/;
+			$s .= $c;
+			next;
+		}
+		die("internal error scanning dq-string '$c'\n");
+	}
+	return $s;
+}
+
+sub scan_balanced {
+	my ($self, $c1, $c2) = @_;
+	my $b = $self->{buff};
+	my $depth = 1;
+	my $s = $c1;
+	while ($$b =~ /\G([^\Q$c1$c2\E]*(?:[\Q$c1$c2\E]|\z))/gc) {
+		$s .= $1;
+		$depth++, next if $s =~ /\Q$c1\E$/;
+		$depth--;
+		last if $depth == 0;
+	}
+	$self->{lineno} += () = $s =~ /\n/sg;
+	return $s;
+}
+
+sub scan_subst {
+	my $self = shift @_;
+	my @tokens = $self->{parser}->parse(qr/^\)$/);
+	$self->{parser}->next_token(); # closing ")"
+	return @tokens;
+}
+
+sub scan_dollar {
+	my $self = shift @_;
+	my $b = $self->{buff};
+	return $self->scan_balanced('(', ')') if $$b =~ /\G\((?=\()/gc; # $((...))
+	return '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\G\(/gc; # $(...)
+	return $self->scan_balanced('{', '}') if $$b =~ /\G\{/gc; # ${...}
+	return $1 if $$b =~ /\G(\w+)/gc; # $var
+	return $1 if $$b =~ /\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.
+	return '';
+}
+
+sub swallow_heredocs {
+	my $self = shift @_;
+	my $b = $self->{buff};
+	my $tags = $self->{heretags};
+	while (my $tag = shift @$tags) {
+		my $start = pos($$b);
+		my $indent = $$tag[0] =~ s/^\t// ? '\\s*' : '';
+		$$b =~ /(?:\G|\n)$indent\Q$$tag[0]\E(?:\n|\z)/gc;
+		if (pos($$b) > $start) {
+			my $body = substr($$b, $start, pos($$b) - $start);
+			$self->{parser}->{heredocs}->{$$tag[0]} = {
+				content => substr($body, 0, length($body) - length($&)),
+				start_line => $self->{lineno},
+		        };
+			$self->{lineno} += () = $body =~ /\n/sg;
+			next;
+		}
+		push(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);
+		$$b =~ /(?:\G|\n).*\z/gc; # consume rest of input
+		my $body = substr($$b, $start, pos($$b) - $start);
+		$self->{lineno} += () = $body =~ /\n/sg;
+		last;
+	}
+}
+
+sub scan_token {
+	my $self = shift @_;
+	my $b = $self->{buff};
+	my $token = '';
+	my ($start, $startln);
+RESTART:
+	$startln = $self->{lineno};
+	$$b =~ /\G[ \t]+/gc; # skip whitespace (but not newline)
+	$start = pos($$b) || 0;
+	$self->{lineno}++, return ["\n", $start, pos($$b), $startln, $startln] if $$b =~ /\G#[^\n]*(?:\n|\z)/gc; # comment
+	while (1) {
+		# slurp up non-special characters
+		$token .= $1 if $$b =~ /\G([^\\;&|<>(){}'"\$\s]+)/gc;
+		# handle special characters
+		last unless $$b =~ /\G(.)/sgc;
+		my $c = $1;
+		pos($$b)--, last if $c =~ /^[ \t]$/; # whitespace ends token
+		pos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\n]$/;
+		$token .= $self->scan_sqstring(), next if $c eq "'";
+		$token .= $self->scan_dqstring(), next if $c eq '"';
+		$token .= $c . $self->scan_dollar(), next if $c eq '$';
+		$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq "\n";
+		$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;
+		$token = $c, last if $c =~ /^[(){}]$/;
+		if ($c eq '\\') {
+			$token .= '\\', last unless $$b =~ /\G(.)/sgc;
+			$c = $1;
+			$self->{lineno}++, next if $c eq "\n" && length($token); # line splice
+			$self->{lineno}++, goto RESTART if $c eq "\n"; # line splice
+			$token .= '\\' . $c;
+			next;
+		}
+		die("internal error scanning character '$c'\n");
+	}
+	return length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;
+}
+
+# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It
+# is a recursive descent parser very roughly modeled after section 2.10 "Shell
+# Grammar" of POSIX chapter 2 "Shell Command Language".
+
+package ShellParser;
+
+sub new {
+	my ($class, $s) = @_;
+	my $self = bless {
+		buff => [],
+		stop => [],
+		output => [],
+		heredocs => {},
+		insubshell => 0,
+	} => $class;
+	$self->{lexer} = Lexer->new($self, $s);
+	return $self;
+}
+
+sub next_token {
+	my $self = shift @_;
+	return pop(@{$self->{buff}}) if @{$self->{buff}};
+	return $self->{lexer}->scan_token();
+}
+
+sub untoken {
+	my $self = shift @_;
+	push(@{$self->{buff}}, @_);
+}
+
+sub peek {
+	my $self = shift @_;
+	my $token = $self->next_token();
+	return undef unless defined($token);
+	$self->untoken($token);
+	return $token;
+}
+
+sub stop_at {
+	my ($self, $token) = @_;
+	return 1 unless defined($token);
+	my $stop = ${$self->{stop}}[-1] if @{$self->{stop}};
+	return defined($stop) && $token->[0] =~ $stop;
+}
+
+sub expect {
+	my ($self, $expect) = @_;
+	my $token = $self->next_token();
+	return $token if defined($token) && $token->[0] eq $expect;
+	push(@{$self->{output}}, "?!ERR?! expected '$expect' but found '" . (defined($token) ? $token->[0] : "<end-of-input>") . "'\n");
+	$self->untoken($token) if defined($token);
+	return ();
+}
+
+sub optional_newlines {
+	my $self = shift @_;
+	my @tokens;
+	while (my $token = $self->peek()) {
+		last unless $token->[0] eq "\n";
+		push(@tokens, $self->next_token());
+	}
+	return @tokens;
+}
+
+sub parse_group {
+	my $self = shift @_;
+	return ($self->parse(qr/^}$/),
+		$self->expect('}'));
+}
+
+sub parse_subshell {
+	my $self = shift @_;
+	$self->{insubshell}++;
+	my @tokens = ($self->parse(qr/^\)$/),
+		      $self->expect(')'));
+	$self->{insubshell}--;
+	return @tokens;
+}
+
+sub parse_case_pattern {
+	my $self = shift @_;
+	my @tokens;
+	while (defined(my $token = $self->next_token())) {
+		push(@tokens, $token);
+		last if $token->[0] eq ')';
+	}
+	return @tokens;
+}
+
+sub parse_case {
+	my $self = shift @_;
+	my @tokens;
+	push(@tokens,
+	     $self->next_token(), # subject
+	     $self->optional_newlines(),
+	     $self->expect('in'),
+	     $self->optional_newlines());
+	while (1) {
+		my $token = $self->peek();
+		last unless defined($token) && $token->[0] ne 'esac';
+		push(@tokens,
+		     $self->parse_case_pattern(),
+		     $self->optional_newlines(),
+		     $self->parse(qr/^(?:;;|esac)$/)); # item body
+		$token = $self->peek();
+		last unless defined($token) && $token->[0] ne 'esac';
+		push(@tokens,
+		     $self->expect(';;'),
+		     $self->optional_newlines());
+	}
+	push(@tokens, $self->expect('esac'));
+	return @tokens;
+}
+
+sub parse_for {
+	my $self = shift @_;
+	my @tokens;
+	push(@tokens,
+	     $self->next_token(), # variable
+	     $self->optional_newlines());
+	my $token = $self->peek();
+	if (defined($token) && $token->[0] eq 'in') {
+		push(@tokens,
+		     $self->expect('in'),
+		     $self->optional_newlines());
+	}
+	push(@tokens,
+	     $self->parse(qr/^do$/), # items
+	     $self->expect('do'),
+	     $self->optional_newlines(),
+	     $self->parse_loop_body(),
+	     $self->expect('done'));
+	return @tokens;
+}
+
+sub parse_if {
+	my $self = shift @_;
+	my @tokens;
+	while (1) {
+		push(@tokens,
+		     $self->parse(qr/^then$/), # if/elif condition
+		     $self->expect('then'),
+		     $self->optional_newlines(),
+		     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body
+		my $token = $self->peek();
+		last unless defined($token) && $token->[0] eq 'elif';
+		push(@tokens, $self->expect('elif'));
+	}
+	my $token = $self->peek();
+	if (defined($token) && $token->[0] eq 'else') {
+		push(@tokens,
+		     $self->expect('else'),
+		     $self->optional_newlines(),
+		     $self->parse(qr/^fi$/)); # else body
+	}
+	push(@tokens, $self->expect('fi'));
+	return @tokens;
+}
+
+sub parse_loop_body {
+	my $self = shift @_;
+	return $self->parse(qr/^done$/);
+}
+
+sub parse_loop {
+	my $self = shift @_;
+	return ($self->parse(qr/^do$/), # condition
+		$self->expect('do'),
+		$self->optional_newlines(),
+		$self->parse_loop_body(),
+		$self->expect('done'));
+}
+
+sub parse_func {
+	my $self = shift @_;
+	return ($self->expect('('),
+		$self->expect(')'),
+		$self->optional_newlines(),
+		$self->parse_cmd()); # body
+}
+
+sub parse_bash_array_assignment {
+	my $self = shift @_;
+	my @tokens = $self->expect('(');
+	while (defined(my $token = $self->next_token())) {
+		push(@tokens, $token);
+		last if $token->[0] eq ')';
+	}
+	return @tokens;
+}
+
+my %compound = (
+	'{' => \&parse_group,
+	'(' => \&parse_subshell,
+	'case' => \&parse_case,
+	'for' => \&parse_for,
+	'if' => \&parse_if,
+	'until' => \&parse_loop,
+	'while' => \&parse_loop);
+
+sub parse_cmd {
+	my $self = shift @_;
+	my $cmd = $self->next_token();
+	return () unless defined($cmd);
+	return $cmd if $cmd->[0] eq "\n";
+
+	my $token;
+	my @tokens = $cmd;
+	if ($cmd->[0] eq '!') {
+		push(@tokens, $self->parse_cmd());
+		return @tokens;
+	} elsif (my $f = $compound{$cmd->[0]}) {
+		push(@tokens, $self->$f());
+	} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {
+		if ($cmd->[0] !~ /\w=$/) {
+			push(@tokens, $self->parse_func());
+			return @tokens;
+		}
+		my @array = $self->parse_bash_array_assignment();
+		$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);
+		$tokens[-1]->[2] = $array[$#array][2] if @array;
+	}
+
+	while (defined(my $token = $self->next_token())) {
+		$self->untoken($token), last if $self->stop_at($token);
+		push(@tokens, $token);
+		last if $token->[0] =~ /^(?:[;&\n|]|&&|\|\|)$/;
+	}
+	push(@tokens, $self->next_token()) if $tokens[-1]->[0] ne "\n" && defined($token = $self->peek()) && $token->[0] eq "\n";
+	return @tokens;
+}
+
+sub accumulate {
+	my ($self, $tokens, $cmd) = @_;
+	push(@$tokens, @$cmd);
+}
+
+sub parse {
+	my ($self, $stop) = @_;
+	push(@{$self->{stop}}, $stop);
+	goto DONE if $self->stop_at($self->peek());
+	my @tokens;
+	while (my @cmd = $self->parse_cmd()) {
+		$self->accumulate(\@tokens, \@cmd);
+		last if $self->stop_at($self->peek());
+	}
+DONE:
+	pop(@{$self->{stop}});
+	return @tokens;
+}
+
+# ScriptParser is a subclass of ShellParser which identifies individual test
+# definitions within test scripts and passes each test body to check_test().
+# ScriptParser detects test definitions not only at the top-level of test
+# scripts but also within compound commands such as loops and function
+# definitions.
+
+package ScriptParser;
+
+our @ISA = ('ShellParser');
+
+sub new {
+	my $class = shift @_;
+	my $self = $class->SUPER::new(@_);
+	$self->{ntests} = 0;
+	$self->{nerrs} = 0;
+	return $self;
+}
+
+# extract the raw content of a token, which may be a single string or a
+# composition of multiple strings and non-string character runs; for instance,
+# `"test body"` unwraps to `test body`; `word"a b"42'c d'` to `worda b42c d`
+sub unwrap {
+	my $token = (@_ ? shift @_ : $_)->[0];
+	# simple case: 'sqstring' or "dqstring"
+	return $token if $token =~ s/^'([^']*)'$/$1/;
+	return $token if $token =~ s/^"([^"]*)"$/$1/;
+
+	# composite case
+	my ($s, $q, $escaped);
+	while (1) {
+		# slurp up non-special characters
+		$s .= $1 if $token =~ /\G([^\\'"]*)/gc;
+		# handle special characters
+		last unless $token =~ /\G(.)/sgc;
+		my $c = $1;
+		$q = undef, next if defined($q) && $c eq $q;
+		$q = $c, next if !defined($q) && $c =~ /^['"]$/;
+		if ($c eq '\\') {
+			last unless $token =~ /\G(.)/sgc;
+			$c = $1;
+			$s .= '\\' if $c eq "\n"; # preserve line splice
+		}
+		$s .= $c;
+	}
+	return $s
+}
+
+sub check_test {
+	# no-op; subclass and override to implement lint checks
+}
+
+sub parse_cmd {
+	my $self = shift @_;
+	my @tokens = $self->SUPER::parse_cmd();
+	return @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;
+	my $n = $#tokens;
+	$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\n|]|&&|\|\|)$/;
+	my $herebody;
+	if ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {
+		$herebody = $self->{heredocs}->{$1};
+		$n--;
+	}
+	$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body
+	$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body
+	return @tokens;
+}
+
+1;
diff --git a/t/meson.build b/t/meson.build
index 7c3c070..a25f37d 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -171,6 +171,7 @@
   't1014-read-tree-confusing.sh',
   't1015-read-index-unmerged.sh',
   't1016-compatObjectFormat.sh',
+  't1017-cat-file-remote-object-info.sh',
   't1020-subdirectory.sh',
   't1022-read-tree-partial-clone.sh',
   't1050-large.sh',
@@ -224,6 +225,10 @@
   't1461-refs-list.sh',
   't1462-refs-exists.sh',
   't1463-refs-optimize.sh',
+  't1464-refs-delete.sh',
+  't1465-refs-update.sh',
+  't1466-refs-create.sh',
+  't1467-refs-rename.sh',
   't1500-rev-parse.sh',
   't1501-work-tree.sh',
   't1502-rev-parse-parseopt.sh',
@@ -400,6 +405,7 @@
   't3451-history-reword.sh',
   't3452-history-split.sh',
   't3453-history-fixup.sh',
+  't3454-history-drop.sh',
   't3500-cherry.sh',
   't3501-revert-cherry-pick.sh',
   't3502-cherry-pick-merge.sh',
@@ -577,6 +583,7 @@
   't4215-log-skewed-merges.sh',
   't4216-log-bloom.sh',
   't4217-log-limit.sh',
+  't4218-log-graph-indentation.sh',
   't4219-log-follow-merge.sh',
   't4252-am-options.sh',
   't4253-am-keep-cr-dos.sh',
diff --git a/t/pack-refs-tests.sh b/t/pack-refs-tests.sh
index d76b087..a800177 100644
--- a/t/pack-refs-tests.sh
+++ b/t/pack-refs-tests.sh
@@ -195,7 +195,7 @@
 test_expect_success 'pack ref directly below refs/' '
 	git update-ref refs/top HEAD &&
 	git ${pack_refs} --all --prune &&
-	grep refs/top .git/packed-refs &&
+	test_grep refs/top .git/packed-refs &&
 	test_path_is_missing .git/refs/top
 '
 
diff --git a/t/perf/p0009-diff-pathspec.sh b/t/perf/p0009-diff-pathspec.sh
new file mode 100755
index 0000000..6079db5
--- /dev/null
+++ b/t/perf/p0009-diff-pathspec.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+test_description='Tests performance of diffing the working tree with a pathspec'
+
+. ./perf-lib.sh
+
+test_perf_fresh_repo
+
+count=10000
+if test_have_prereq EXPENSIVE
+then
+	count=100000
+fi
+
+# The entries exist only in the index, which is enough to
+# exercise the index scan.
+test_expect_success 'setup' '
+	blob=$(echo content | git hash-object -w --stdin) &&
+	{
+		printf "100644 $blob\taaa/file\n" &&
+		printf "100644 $blob\tf%s\n" $(test_seq $count)
+	} | git update-index --index-info &&
+	git commit -q -m initial &&
+	mkdir -p aaa &&
+	echo content >aaa/file
+'
+
+test_perf 'diff pathspec subtree' '
+	git diff HEAD -- aaa/file
+'
+
+test_done
diff --git a/t/perf/p1401-ref-store-tombstones.sh b/t/perf/p1401-ref-store-tombstones.sh
new file mode 100755
index 0000000..9e3d803
--- /dev/null
+++ b/t/perf/p1401-ref-store-tombstones.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+test_description="Tests performance of ref operations with many tombstones"
+
+. ./perf-lib.sh
+
+test_expect_success "setup" '
+	git init --ref-format=reftable repo &&
+	blob=$(echo foo | git -C repo hash-object -w --stdin) &&
+	for i in $(test_seq 8000)
+	do
+		printf "create refs/tags/tag-%d %s\n" "$i" "$blob" ||
+		return 1
+	done >repo/input &&
+	git -C repo update-ref --stdin <repo/input &&
+	git -C repo for-each-ref --format="delete %(refname)" |
+	git -C repo update-ref --stdin
+'
+
+test_perf "recreate refs after mass delete" '
+	git -C repo update-ref --stdin <repo/input &&
+	git -C repo for-each-ref --format="delete %(refname)" |
+	git -C repo update-ref --stdin
+'
+
+test_expect_success "setup asymmetric" '
+	git init --ref-format=reftable repo2 &&
+	blob=$(echo foo | git -C repo2 hash-object -w --stdin) &&
+	for i in $(test_seq 8000)
+	do
+		printf "create refs/tags/old-%d %s\n" "$i" "$blob" ||
+		return 1
+	done >repo2/input-old &&
+	sed "s/old-/new-/" <repo2/input-old >repo2/input-new &&
+	git -C repo2 update-ref --stdin <repo2/input-old &&
+	git -C repo2 for-each-ref --format="delete %(refname)" |
+	git -C repo2 update-ref --stdin
+'
+
+test_perf "create new refs after deleting differently-named refs" '
+	git -C repo2 update-ref --stdin <repo2/input-new &&
+	git -C repo2 for-each-ref --format="delete %(refname)" refs/tags/ |
+	git -C repo2 update-ref --stdin
+'
+
+test_done
diff --git a/t/perf/p1500-graph-walks.sh b/t/perf/p1500-graph-walks.sh
index 5b23ce5..d167b4f 100755
--- a/t/perf/p1500-graph-walks.sh
+++ b/t/perf/p1500-graph-walks.sh
@@ -32,7 +32,16 @@
 		echo "X:$line" >>test-tool-tags || return 1
 	done &&
 
-	commit=$(git commit-tree $(git rev-parse HEAD^{tree})) &&
+	git rev-list --first-parent --max-count=8192 HEAD >contains-commits &&
+	test_file_not_empty contains-commits &&
+	git update-ref refs/contains-perf-base "$(tail -n 1 contains-commits)" &&
+	awk "{
+		printf \"update refs/contains-perf/%04d %s\\n\", NR, \$1
+	}" contains-commits |
+		git update-ref --stdin &&
+	git pack-refs --include "refs/contains-perf/*" &&
+
+	commit=$(git commit-tree HEAD^{tree}) &&
 	git update-ref refs/heads/disjoint-base $commit &&
 
 	git commit-graph write --reachable
@@ -62,6 +71,23 @@
 	xargs git tag --merged=HEAD <tags
 '
 
+test_perf 'contains: git for-each-ref' '
+	git for-each-ref --contains=refs/contains-perf-base --stdin <refs
+'
+
+test_perf 'contains: git branch' '
+	xargs git branch --contains=refs/contains-perf-base <branches
+'
+
+test_perf 'contains: git tag' '
+	xargs git tag --contains=refs/contains-perf-base <tags
+'
+
+test_perf 'contains: synthetic shared history' '
+	git for-each-ref --contains=refs/contains-perf-base \
+		refs/contains-perf/ >/dev/null
+'
+
 test_perf 'is-base check: test-tool reach (refs)' '
 	test-tool reach get_branch_base_for_tip <test-tool-refs
 '
diff --git a/t/perf/p2000-sparse-operations.sh b/t/perf/p2000-sparse-operations.sh
index aadf22b..548a61c 100755
--- a/t/perf/p2000-sparse-operations.sh
+++ b/t/perf/p2000-sparse-operations.sh
@@ -108,6 +108,7 @@
 
 test_perf_on_all git status
 test_perf_on_all 'git stash && git stash pop'
+test_perf_on_all "git stash push -- $SPARSE_CONE/a && git stash pop"
 test_perf_on_all 'echo >>new && git stash -u && git stash pop'
 test_perf_on_all git add -A
 test_perf_on_all git add .
diff --git a/t/show-ref-exists-tests.sh b/t/show-ref-exists-tests.sh
index 36e8e9d..1351f69 100644
--- a/t/show-ref-exists-tests.sh
+++ b/t/show-ref-exists-tests.sh
@@ -19,7 +19,7 @@
 
 test_expect_success '--exists does not use DWIM' '
 	test_expect_code 2 ${git_show_ref_exists} $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME 2>err &&
-	grep "reference does not exist" err
+	test_grep "reference does not exist" err
 '
 
 test_expect_success '--exists with HEAD' '
diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
index 2b63e1c..7bd1f4e 100755
--- a/t/t0000-basic.sh
+++ b/t/t0000-basic.sh
@@ -743,7 +743,7 @@
 	test_done
 	EOF
 
-	grep "echo trace" lazy-prereq-and-tracing/err
+	test_grep "echo trace" lazy-prereq-and-tracing/err
 '
 
 test_expect_success 'subtest: tests clean up after themselves' '
@@ -815,7 +815,7 @@
 
 test_expect_success 'test_oid provides sane info by default' '
 	test_oid zero >actual &&
-	grep "^00*\$" actual &&
+	test_grep "^00*\$" actual &&
 	rawsz="$(test_oid rawsz)" &&
 	hexsz="$(test_oid hexsz)" &&
 	# +1 accounts for the trailing newline
@@ -827,7 +827,7 @@
 	test_when_finished "test_detect_hash" &&
 	test_set_hash sha1 &&
 	test_oid zero >actual &&
-	grep "^00*\$" actual &&
+	test_grep "^00*\$" actual &&
 	rawsz="$(test_oid rawsz)" &&
 	hexsz="$(test_oid hexsz)" &&
 	test $(wc -c <actual) -eq 41 &&
@@ -839,7 +839,7 @@
 	test_when_finished "test_detect_hash" &&
 	test_set_hash sha256 &&
 	test_oid zero >actual &&
-	grep "^00*\$" actual &&
+	test_grep "^00*\$" actual &&
 	rawsz="$(test_oid rawsz)" &&
 	hexsz="$(test_oid hexsz)" &&
 	test $(wc -c <actual) -eq 65 &&
@@ -884,11 +884,11 @@
 		# test script, hence the redirection of fd 7, and aborts
 		# with "exit 1", hence the subshell.
 		! ( test_bool_env envvar true ) 7>err &&
-		grep "error: test_bool_env requires bool values" err &&
+		test_grep "error: test_bool_env requires bool values" err &&
 
 		envvar=true &&
 		! ( test_bool_env envvar invalid ) 7>err &&
-		grep "error: test_bool_env requires bool values" err
+		test_grep "error: test_bool_env requires bool values" err
 	)
 '
 
@@ -1242,12 +1242,12 @@
 
 test_expect_success 'test_must_fail rejects a non-git command' '
 	! test_must_fail grep ^$ notafile 2>err &&
-	grep -F "test_must_fail: only '"'"'git'"'"' is allowed" err
+	test_grep -F "test_must_fail: only '"'"'git'"'"' is allowed" err
 '
 
 test_expect_success 'test_must_fail rejects a non-git command with env' '
 	! test_must_fail env var1=a var2=b grep ^$ notafile 2>err &&
-	grep -F "test_must_fail: only '"'"'git'"'"' is allowed" err
+	test_grep -F "test_must_fail: only '"'"'git'"'"' is allowed" err
 '
 
 test_done
diff --git a/t/t0001-init.sh b/t/t0001-init.sh
index e89feca..5cf2e5a 100755
--- a/t/t0001-init.sh
+++ b/t/t0001-init.sh
@@ -278,9 +278,9 @@
 		git init --bare --shared=0660 newdir/a/b/c &&
 		test_path_is_dir newdir/a/b/c/refs &&
 		ls -ld newdir/a newdir/a/b > lsab.out &&
-		! grep -v "^drwxrw[sx]r-x" lsab.out &&
+		test_grep ! -v "^drwxrw[sx]r-x" lsab.out &&
 		ls -ld newdir/a/b/c > lsc.out &&
-		! grep -v "^drwxrw[sx]---" lsc.out
+		test_grep ! -v "^drwxrw[sx]---" lsc.out
 	)
 '
 
@@ -619,7 +619,7 @@
 	git init refstorage &&
 	git -C refstorage config extensions.refStorage files &&
 	test_must_fail git -C refstorage rev-parse 2>err &&
-	grep "repo version is 0, but v1-only extension found" err
+	test_grep "repo version is 0, but v1-only extension found" err
 '
 
 test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with files backend' '
@@ -637,7 +637,7 @@
 	git -C refstorage config core.repositoryformatversion 1 &&
 	git -C refstorage config extensions.refStorage garbage &&
 	test_must_fail git -C refstorage rev-parse 2>err &&
-	grep "invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}" err
+	test_grep "invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}" err
 '
 
 test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' '
@@ -848,8 +848,8 @@
 		GIT_REDIRECT_STDOUT=output.txt \
 		GIT_REDIRECT_STDERR="2>&1" \
 		git rev-parse --git-dir --verify refs/invalid &&
-	grep "^\\.git\$" output.txt &&
-	grep "Needed a single revision" output.txt
+	test_grep "^\\.git\$" output.txt &&
+	test_grep "Needed a single revision" output.txt
 '
 
 test_expect_success '--initial-branch' '
@@ -862,14 +862,14 @@
 	git init --initial-branch=ignore initial-branch-option 2>err &&
 	test_grep "ignored --initial-branch" err &&
 	git -C initial-branch-option symbolic-ref HEAD >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'overridden default initial branch name (config)' '
 	test_config_global init.defaultBranch nmb &&
 	GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= git init initial-branch-config &&
 	git -C initial-branch-config symbolic-ref HEAD >actual &&
-	grep nmb actual
+	test_grep nmb actual
 '
 
 test_expect_success 'advice on unconfigured init.defaultBranch' '
@@ -907,7 +907,7 @@
 	test_config_global init.defaultBranch nmb &&
 	GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=env git init main-branch-env &&
 	git -C main-branch-env symbolic-ref HEAD >actual &&
-	grep env actual
+	test_grep env actual
 '
 
 test_expect_success 'invalid default branch name' '
diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh
index d77a179..ed95faf 100755
--- a/t/t0008-ignores.sh
+++ b/t/t0008-ignores.sh
@@ -790,8 +790,8 @@
 	>one &&
 	mkdir top-level-dir &&
 	git check-ignore one top-level-dir >actual &&
-	grep one actual &&
-	grep top-level-dir actual
+	test_grep one actual &&
+	test_grep top-level-dir actual
 '
 
 test_expect_success 'existing directory and file' '
@@ -800,8 +800,8 @@
 	>one &&
 	mkdir top-level-dir &&
 	git check-ignore top-level-dir one >actual &&
-	grep one actual &&
-	grep top-level-dir actual
+	test_grep one actual &&
+	test_grep top-level-dir actual
 '
 
 test_expect_success 'exact prefix matching (with root)' '
diff --git a/t/t0009-git-dir-validation.sh b/t/t0009-git-dir-validation.sh
index 33d21ed..4cba478 100755
--- a/t/t0009-git-dir-validation.sh
+++ b/t/t0009-git-dir-validation.sh
@@ -35,7 +35,7 @@
 		cd parent/fifo-trap &&
 		mkfifo .git &&
 		test_must_fail git rev-parse --git-dir 2>stderr &&
-		grep "not a regular file" stderr
+		test_grep "not a regular file" stderr
 	)
 '
 
@@ -47,7 +47,7 @@
 		mkfifo target-fifo &&
 		ln -s target-fifo .git &&
 		test_must_fail git rev-parse --git-dir 2>stderr &&
-		grep "not a regular file" stderr
+		test_grep "not a regular file" stderr
 	)
 '
 
@@ -58,7 +58,7 @@
 		cd parent/garbage-trap &&
 		echo "garbage" >.git &&
 		test_must_fail git rev-parse --git-dir 2>stderr &&
-		grep "invalid gitfile format" stderr
+		test_grep "invalid gitfile format" stderr
 	)
 '
 
diff --git a/t/t0012-help.sh b/t/t0012-help.sh
index c33501b..b0a3fdb 100755
--- a/t/t0012-help.sh
+++ b/t/t0012-help.sh
@@ -131,8 +131,8 @@
 
 test_expect_success 'git help --user-interfaces' '
 	git help --user-interfaces >help.output &&
-	grep "^   attributes   " help.output &&
-	grep "^   mailmap   " help.output
+	test_grep "^   attributes   " help.output &&
+	test_grep "^   mailmap   " help.output
 '
 
 test_expect_success 'git help -c' '
@@ -260,7 +260,7 @@
 		(
 			GIT_CEILING_DIRECTORIES=$(pwd) &&
 			export GIT_CEILING_DIRECTORIES &&
-			test_expect_code 129 git -C sub $builtin -h >output 2>err
+			git -C sub $builtin -h >output 2>err
 		) &&
 		test_must_be_empty err &&
 		test_grep usage output
diff --git a/t/t0013-sha1dc.sh b/t/t0013-sha1dc.sh
index ce3d812..3ea3169 100755
--- a/t/t0013-sha1dc.sh
+++ b/t/t0013-sha1dc.sh
@@ -16,7 +16,7 @@
 test_expect_success 'test-sha1 detects shattered pdf' '
 	test_must_fail test-tool sha1 <"$TEST_DATA/shattered-1.pdf" 2>err &&
 	test_grep collision err &&
-	grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err
+	test_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err
 '
 
 test_done
diff --git a/t/t0017-env-helper.sh b/t/t0017-env-helper.sh
index 32fe848..da02bce 100755
--- a/t/t0017-env-helper.sh
+++ b/t/t0017-env-helper.sh
@@ -88,7 +88,7 @@
 	test_must_fail \
 		env HOME="$(pwd)/home" \
 		git config -l 2>err &&
-	grep "exceeded maximum include depth" err &&
+	test_grep "exceeded maximum include depth" err &&
 
 	# This validates that the assumption that we attempt to
 	# read the configuration and fail very early in the start-up
@@ -100,7 +100,7 @@
 		test-tool -C no-such-directory \
 		env-helper --type=bool --default=0 \
 		--exit-code GIT_TEST_ENV_HELPER 2>err &&
-	grep "exceeded maximum include depth" err
+	test_grep "exceeded maximum include depth" err
 '
 
 test_done
diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh
index 033b00a..6329150 100755
--- a/t/t0021-conversion.sh
+++ b/t/t0021-conversion.sh
@@ -296,7 +296,7 @@
 	test_cmp expect actual
 '
 
-test_expect_success EXPENSIVE 'filter large file' '
+test_expect_success EXPENSIVE,SIZE_T_IS_64BIT 'filter large file' '
 	test_config filter.largefile.smudge cat &&
 	test_config filter.largefile.clean cat &&
 	test_seq -f "%1048576d" 1 2048 >2GB &&
@@ -731,7 +731,7 @@
 		rm -f debug.log &&
 		git checkout --quiet --no-progress . 2>git-stderr.log &&
 
-		grep "smudge write error" git-stderr.log &&
+		test_grep "smudge write error" git-stderr.log &&
 		test_grep "error: external filter" git-stderr.log &&
 
 		cat >expected.log <<-EOF &&
@@ -853,7 +853,7 @@
 
 		cp "$TEST_ROOT/test.o" test.r &&
 		test_must_fail git add . 2>git-stderr.log &&
-		grep "expected git-filter-server" git-stderr.log
+		test_grep "expected git-filter-server" git-stderr.log
 	)
 '
 
@@ -970,7 +970,7 @@
 
 	rm -rf repo-cloned &&
 	test_must_fail git clone repo repo-cloned 2>git-stderr.log &&
-	grep "error: .missing-delay\.a. was not filtered properly" git-stderr.log
+	test_grep "error: .missing-delay\.a. was not filtered properly" git-stderr.log
 '
 
 test_expect_success 'invalid file in delayed checkout' '
@@ -991,7 +991,7 @@
 
 	rm -rf repo-cloned &&
 	test_must_fail git clone repo repo-cloned 2>git-stderr.log &&
-	grep "error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier" git-stderr.log
+	test_grep "error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier" git-stderr.log
 '
 
 for mode in 'case' 'utf-8'
@@ -1032,7 +1032,7 @@
 
 		git clone $mode-collision $mode-collision-cloned &&
 		# Make sure z was really delayed
-		grep "IN: smudge $dir/z .* \\[DELAYED\\]" $mode-collision-cloned/delayed.log &&
+		test_grep "IN: smudge $dir/z .* \\[DELAYED\\]" $mode-collision-cloned/delayed.log &&
 
 		# Should not create $dir/z at $symlink/z
 		test_path_is_missing $mode-collision/target-dir/z
@@ -1070,7 +1070,7 @@
 		git commit -m super &&
 
 		git checkout --recurse-submodules . &&
-		grep "IN: smudge A/B/y .* \\[DELAYED\\]" delayed.log &&
+		test_grep "IN: smudge A/B/y .* \\[DELAYED\\]" delayed.log &&
 		test_path_is_missing target-dir/y
 	)
 '
@@ -1161,9 +1161,9 @@
 
 		rm *.a &&
 		git checkout . 2>err &&
-		grep "IN: smudge test-delay10.a .* \\[DELAYED\\]" delayed.log &&
-		grep "IN: smudge test-delay11.a .* \\[DELAYED\\]" delayed.log &&
-		grep "Updated 2 paths from the index" err
+		test_grep "IN: smudge test-delay10.a .* \\[DELAYED\\]" delayed.log &&
+		test_grep "IN: smudge test-delay11.a .* \\[DELAYED\\]" delayed.log &&
+		test_grep "Updated 2 paths from the index" err
 	)
 '
 
diff --git a/t/t0029-core-unsetenvvars.sh b/t/t0029-core-unsetenvvars.sh
index baa1b7e..975620b 100755
--- a/t/t0029-core-unsetenvvars.sh
+++ b/t/t0029-core-unsetenvvars.sh
@@ -20,10 +20,10 @@
 	HOBBES=Calvin &&
 	export HOBBES &&
 	git commit --allow-empty -m with 2>err &&
-	grep Calvin err &&
+	test_grep Calvin err &&
 	git -c core.unsetenvvars=FINDUS,HOBBES,CALVIN \
 		commit --allow-empty -m without 2>err &&
-	! grep Calvin err
+	test_grep ! Calvin err
 '
 
 test_done
diff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh
index 43155f6..d6e2f63 100755
--- a/t/t0030-stripspace.sh
+++ b/t/t0030-stripspace.sh
@@ -407,12 +407,12 @@
 
 test_expect_success 'newline as commentchar is forbidden' '
 	test_must_fail git -c core.commentChar="$LF" stripspace -s 2>err &&
-	grep "core.commentchar cannot contain newline" err
+	test_grep "core.commentchar cannot contain newline" err
 '
 
 test_expect_success 'empty commentchar is forbidden' '
 	test_must_fail git -c core.commentchar= stripspace -s 2>err &&
-	grep "core.commentchar must have at least one character" err
+	test_grep "core.commentchar must have at least one character" err
 '
 
 test_expect_success '-c with single line' '
diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh
index 8ef87ad..e9e2f04 100755
--- a/t/t0031-lockfile-pid.sh
+++ b/t/t0031-lockfile-pid.sh
@@ -29,7 +29,7 @@
 		test_must_fail git add . 2>err &&
 		# Should not crash, just show normal error without PID
 		test_grep "Unable to create" err &&
-		! test_grep "is held by process" err
+		test_grep ! "is held by process" err
 	)
 '
 
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index ca55ea8..a22533f 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -68,7 +68,7 @@
 EOF
 
 test_expect_success 'test help' '
-	test_must_fail test-tool parse-options -h >output 2>output.err &&
+	test-tool parse-options -h >output 2>output.err &&
 	test_must_be_empty output.err &&
 	test_cmp expect output
 '
@@ -324,13 +324,13 @@
 
 test_expect_success 'Alias options do not contribute to abbreviation' '
 	test-tool parse-options --alias-source 123 >output &&
-	grep "^string: 123" output &&
+	test_grep "^string: 123" output &&
 	test-tool parse-options --alias-target 123 >output &&
-	grep "^string: 123" output &&
+	test_grep "^string: 123" output &&
 	test_must_fail test-tool parse-options --alias &&
 	GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \
 	test-tool parse-options --alias 123 >output &&
-	grep "^string: 123" output
+	test_grep "^string: 123" output
 '
 
 cat >typo.err <<\EOF
@@ -582,16 +582,16 @@
 
 test_expect_success 'NO_INTERNAL_HELP works for -h' '
 	test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&
-	grep "^error: unknown switch \`h$SQ" err &&
-	grep "^usage: " err
+	test_grep "^error: unknown switch \`h$SQ" err &&
+	test_grep "^usage: " err
 '
 
 for help_opt in help help-all
 do
 	test_expect_success "NO_INTERNAL_HELP works for --$help_opt" "
 		test_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&
-		grep '^error: unknown option \`'$help_opt\' err &&
-		grep '^usage: ' err
+		test_grep '^error: unknown option \`'$help_opt\' err &&
+		test_grep '^usage: ' err
 	"
 done
 
@@ -608,38 +608,38 @@
 
 test_expect_success 'subcommand - no subcommand shows error and usage' '
 	test_expect_code 129 test-tool parse-subcommand cmd 2>err &&
-	grep "^error: need a subcommand" err &&
-	grep ^usage: err
+	test_grep "^error: need a subcommand" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - subcommand after -- shows error and usage' '
 	test_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&
-	grep "^error: need a subcommand" err &&
-	grep ^usage: err
+	test_grep "^error: need a subcommand" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '
 	test_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&
-	grep "^error: need a subcommand" err &&
-	grep ^usage: err
+	test_grep "^error: need a subcommand" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - unknown subcommand shows error and usage' '
 	test_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&
-	grep "^error: unknown subcommand: \`nope$SQ" err &&
-	grep ^usage: err
+	test_grep "^error: unknown subcommand: \`nope$SQ" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - subcommands cannot be abbreviated' '
 	test_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&
-	grep "^error: unknown subcommand: \`subcmd-o$SQ$" err &&
-	grep ^usage: err
+	test_grep "^error: unknown subcommand: \`subcmd-o$SQ$" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - no negated subcommands' '
 	test_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&
-	grep "^error: unknown subcommand: \`no-subcmd-one$SQ" err &&
-	grep ^usage: err
+	test_grep "^error: unknown subcommand: \`no-subcmd-one$SQ" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - simple' '
@@ -709,8 +709,8 @@
 
 test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '
 	test_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&
-	grep "^error: unknown option" err &&
-	grep ^usage: err
+	test_grep "^error: unknown option" err &&
+	test_grep ^usage: err
 '
 
 test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '
@@ -778,28 +778,28 @@
 
 test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '
 	test_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&
-	grep ^BUG err
+	test_grep ^BUG err
 '
 
 test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '
 	test_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&
-	grep ^BUG err
+	test_grep ^BUG err
 '
 
 test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '
 	test_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&
-	grep ^BUG err
+	test_grep ^BUG err
 '
 
 test_expect_success 'negative unsigned' '
 	test_must_fail test-tool parse-options --unsigned -1 >out 2>err &&
-	grep "non-negative integer" err &&
+	test_grep "non-negative integer" err &&
 	test_must_be_empty out
 '
 
 test_expect_success 'unsigned with units but no numbers' '
 	test_must_fail test-tool parse-options --unsigned m >out 2>err &&
-	grep "non-negative integer" err &&
+	test_grep "non-negative integer" err &&
 	test_must_be_empty out
 '
 
diff --git a/t/t0041-usage.sh b/t/t0041-usage.sh
index a0f6f13..51af7cc 100755
--- a/t/t0041-usage.sh
+++ b/t/t0041-usage.sh
@@ -13,7 +13,7 @@
 
 test_expect_success 'tag --contains <existent_tag>' '
 	git tag --contains "v1.0" >actual 2>actual.err &&
-	grep "v1.0" actual &&
+	test_grep "v1.0" actual &&
 	test_line_count = 0 actual.err
 '
 
diff --git a/t/t0052-simple-ipc.sh b/t/t0052-simple-ipc.sh
index ff98be3..14cea84 100755
--- a/t/t0052-simple-ipc.sh
+++ b/t/t0052-simple-ipc.sh
@@ -33,19 +33,19 @@
 test_expect_success 'big response' '
 	test-tool simple-ipc send --token=big >actual &&
 	test_line_count -ge 10000 actual &&
-	grep -q "big: [0]*9999\$" actual
+	test_grep -q "big: [0]*9999\$" actual
 '
 
 test_expect_success 'chunk response' '
 	test-tool simple-ipc send --token=chunk >actual &&
 	test_line_count -ge 10000 actual &&
-	grep -q "big: [0]*9999\$" actual
+	test_grep -q "big: [0]*9999\$" actual
 '
 
 test_expect_success 'slow response' '
 	test-tool simple-ipc send --token=slow >actual &&
 	test_line_count -ge 100 actual &&
-	grep -q "big: [0]*99\$" actual
+	test_grep -q "big: [0]*99\$" actual
 '
 
 # Send an IPC with n=100,000 bytes of ballast.  This should be large enough
@@ -54,7 +54,7 @@
 #
 test_expect_success 'sendbytes' '
 	test-tool simple-ipc sendbytes --bytecount=100000 --byte=A >actual &&
-	grep "sent:A00100000 rcvd:A00100000" actual
+	test_grep "sent:A00100000 rcvd:A00100000" actual
 '
 
 # Start a series of <threads> client threads that each make <batchsize>
@@ -93,7 +93,7 @@
 		--batchsize=13 \
 		>actual &&
 	test_line_count = 92 actual &&
-	grep "good 91" actual &&
+	test_grep "good 91" actual &&
 	grep "sent:A" <actual >actual_a &&
 	cat >expect_a <<-EOF &&
 		sent:A00000019 rcvd:A00000019
diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh
index 60cfe65..dc9d77b 100755
--- a/t/t0061-run-command.sh
+++ b/t/t0061-run-command.sh
@@ -97,7 +97,7 @@
 	chmod -x hello.sh &&
 	test_must_fail test-tool run-command run-command ./hello.sh 2>err &&
 
-	grep "fatal: cannot exec.*hello.sh" err
+	test_grep "fatal: cannot exec.*hello.sh" err
 '
 
 test_expect_success POSIXPERM,SANITY 'unreadable directory in PATH' '
diff --git a/t/t0066-dir-iterator.sh b/t/t0066-dir-iterator.sh
index df3e9f5..9fbb41b 100755
--- a/t/t0066-dir-iterator.sh
+++ b/t/t0066-dir-iterator.sh
@@ -127,7 +127,7 @@
 test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' '
 	test_must_fail test-tool dir-iterator ./dir5 >out &&
 
-	grep "ENOTDIR" out
+	test_grep "ENOTDIR" out
 '
 
 test_done
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 80b163e..55b37d8 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -21,23 +21,23 @@
 
 	git for-each-repo --config=run.key commit --allow-empty -m "ran" &&
 	git -C one log -1 --pretty=format:%s >message &&
-	grep ran message &&
+	test_grep ran message &&
 	git -C two log -1 --pretty=format:%s >message &&
-	! grep ran message &&
+	test_grep ! ran message &&
 	git -C three log -1 --pretty=format:%s >message &&
-	grep ran message &&
+	test_grep ran message &&
 	git -C ~/four log -1 --pretty=format:%s >message &&
-	grep ran message &&
+	test_grep ran message &&
 
 	git for-each-repo --config=run.key -- commit --allow-empty -m "ran again" &&
 	git -C one log -1 --pretty=format:%s >message &&
-	grep again message &&
+	test_grep again message &&
 	git -C two log -1 --pretty=format:%s >message &&
-	! grep again message &&
+	test_grep ! again message &&
 	git -C three log -1 --pretty=format:%s >message &&
-	grep again message &&
+	test_grep again message &&
 	git -C ~/four log -1 --pretty=format:%s >message &&
-	grep again message &&
+	test_grep again message &&
 
 	git -C three for-each-repo --config=run.key -- \
 		commit --allow-empty -m "ran from worktree" &&
diff --git a/t/t0070-fundamental.sh b/t/t0070-fundamental.sh
index 6b9dcf9..8f573c2 100755
--- a/t/t0070-fundamental.sh
+++ b/t/t0070-fundamental.sh
@@ -10,7 +10,7 @@
 
 test_expect_success 'mktemp to nonexistent directory prints filename' '
 	test_must_fail test-tool mktemp doesnotexist/testXXXXXX 2>err &&
-	grep "doesnotexist/test" err
+	test_grep "doesnotexist/test" err
 '
 
 test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints filename' '
@@ -18,7 +18,7 @@
 	test_when_finished "chmod +w cannotwrite" &&
 	chmod -w cannotwrite &&
 	test_must_fail test-tool mktemp cannotwrite/testXXXXXX 2>err &&
-	grep "cannotwrite/test" err
+	test_grep "cannotwrite/test" err
 '
 
 test_expect_success 'git_mkstemps_mode does not fail if fd 0 is not open' '
@@ -33,7 +33,7 @@
 test_expect_success 'incomplete sideband messages are reassembled' '
 	test-tool pkt-line send-split-sideband >split-sideband &&
 	test-tool pkt-line receive-sideband <split-sideband 2>err &&
-	grep "Hello, world" err
+	test_grep "Hello, world" err
 '
 
 test_expect_success 'eof on sideband message is reported' '
diff --git a/t/t0081-find-pack.sh b/t/t0081-find-pack.sh
index 26f0174..ff9d56f 100755
--- a/t/t0081-find-pack.sh
+++ b/t/t0081-find-pack.sh
@@ -52,17 +52,17 @@
 
 	# HEAD^{tree} is in 2 packfiles
 	test-tool find-pack HEAD^{tree} >head_tree_packs &&
-	grep "$head_commit_pack" head_tree_packs &&
-	grep mypackname1 head_tree_packs &&
-	! grep mypackname2 head_tree_packs &&
+	test_grep "$head_commit_pack" head_tree_packs &&
+	test_grep mypackname1 head_tree_packs &&
+	test_grep ! mypackname2 head_tree_packs &&
 	test-tool find-pack --check-count 2 HEAD^{tree} &&
 	! test-tool find-pack --check-count 1 HEAD^{tree} &&
 
 	# HEAD:five.t is also in 2 packfiles
 	test-tool find-pack HEAD:five.t >five_packs &&
-	grep "$head_commit_pack" five_packs &&
-	! grep mypackname1 five_packs &&
-	grep mypackname2 five_packs &&
+	test_grep "$head_commit_pack" five_packs &&
+	test_grep ! mypackname1 five_packs &&
+	test_grep mypackname2 five_packs &&
 	test-tool find-pack -c 2 HEAD:five.t &&
 	! test-tool find-pack --check-count=0 HEAD:five.t
 '
diff --git a/t/t0091-bugreport.sh b/t/t0091-bugreport.sh
index e38ca7a..81ed417 100755
--- a/t/t0091-bugreport.sh
+++ b/t/t0091-bugreport.sh
@@ -40,15 +40,15 @@
 
 	# The beginning should match "git version --build-options" verbatim,
 	# but rather than checking bit-for-bit equality, just test some basics.
-	grep "git version " system &&
-	grep "shell-path: ." system &&
+	test_grep "git version " system &&
+	test_grep "shell-path: ." system &&
 
 	# After the version, there should be some more info.
 	# This is bound to differ from environment to environment,
 	# so we just do some rather high-level checks.
-	grep "uname: ." system &&
-	grep "compiler info: ." system &&
-	grep "zlib." system
+	test_grep "uname: ." system &&
+	test_grep "compiler info: ." system &&
+	test_grep "zlib." system
 '
 
 test_expect_success 'dies if file with same name as report already exists' '
@@ -112,7 +112,7 @@
 	git bugreport --diagnose -o report -s test >out &&
 
 	zip_path=report/git-diagnostics-test.zip &&
-	grep "Available space" out &&
+	test_grep "Available space" out &&
 	test_path_is_file "$zip_path" &&
 
 	# Check zipped archive content
@@ -120,10 +120,10 @@
 	test_file_not_empty out &&
 
 	"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
-	grep ".git/objects" out &&
+	test_grep ".git/objects" out &&
 
 	"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
-	grep "^Total: [0-9][0-9]*" out &&
+	test_grep "^Total: [0-9][0-9]*" out &&
 
 	# Should not include .git directory contents by default
 	! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
@@ -136,7 +136,7 @@
 
 	# Includes pack quantity/size info
 	"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
-	grep ".git/objects" out &&
+	test_grep ".git/objects" out &&
 
 	# Does not include .git directory contents
 	! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
diff --git a/t/t0092-diagnose.sh b/t/t0092-diagnose.sh
index 6cabd6e..0bc2b1e 100755
--- a/t/t0092-diagnose.sh
+++ b/t/t0092-diagnose.sh
@@ -8,7 +8,7 @@
 	test_when_finished rm -rf report &&
 
 	git diagnose -o report -s test >out &&
-	grep "Available space" out &&
+	test_grep "Available space" out &&
 
 	zip_path=report/git-diagnostics-test.zip &&
 	test_path_is_file "$zip_path" &&
@@ -18,10 +18,10 @@
 	test_file_not_empty out &&
 
 	"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
-	grep ".git/objects" out &&
+	test_grep ".git/objects" out &&
 
 	"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
-	grep "^Total: [0-9][0-9]*" out &&
+	test_grep "^Total: [0-9][0-9]*" out &&
 
 	# Should not include .git directory contents by default
 	! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
@@ -34,7 +34,7 @@
 	git diagnose -o test-count -s 1 >out &&
 	zip_path=test-count/git-diagnostics-1.zip &&
 	"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
-	grep "^Total: [1-9][0-9]* loose objects" out
+	test_grep "^Total: [1-9][0-9]* loose objects" out
 '
 
 test_expect_success UNZIP '--mode=stats excludes .git dir contents' '
@@ -45,7 +45,7 @@
 	# Includes pack quantity/size info
 	zip_path=report/git-diagnostics-test.zip &&
 	"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
-	grep ".git/objects" out &&
+	test_grep ".git/objects" out &&
 
 	# Does not include .git directory contents
 	! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
@@ -59,7 +59,7 @@
 	# Includes pack quantity/size info
 	zip_path=report/git-diagnostics-test.zip &&
 	"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
-	grep ".git/objects" out &&
+	test_grep ".git/objects" out &&
 
 	# Includes .git directory contents
 	"$GIT_UNZIP" -l "$zip_path" | grep ".git/" &&
diff --git a/t/t0100-previous.sh b/t/t0100-previous.sh
index dd5d9b4..87a9995 100755
--- a/t/t0100-previous.sh
+++ b/t/t0100-previous.sh
@@ -50,7 +50,7 @@
 	git checkout main &&
 	git merge @{-1}~1 &&
 	git cat-file commit HEAD >actual &&
-	grep "Merge branch '\''other'\''" actual
+	test_grep "Merge branch '\''other'\''" actual
 '
 
 test_expect_success 'merge @{-100} before checking out that many branches yet' '
diff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh
index 8853d8a..8db26c1 100755
--- a/t/t0200-gettext-basic.sh
+++ b/t/t0200-gettext-basic.sh
@@ -16,7 +16,7 @@
 '
 
 test_expect_success 'xgettext sanity: Perl _() strings are not extracted' '
-    ! grep "A Perl string xgettext will not get" "$GIT_PO_PATH"/is.po
+    test_grep ! "A Perl string xgettext will not get" "$GIT_PO_PATH"/is.po
 '
 
 test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '
@@ -26,8 +26,8 @@
 '
 
 test_expect_success 'xgettext sanity: Comment extraction with --add-comments stops at statements' '
-    ! grep "This is a phony" "$GIT_PO_PATH"/is.po &&
-    ! grep "the above comment" "$GIT_PO_PATH"/is.po
+    test_grep ! "This is a phony" "$GIT_PO_PATH"/is.po &&
+    test_grep ! "the above comment" "$GIT_PO_PATH"/is.po
 '
 
 test_expect_success GETTEXT 'sanity: $TEXTDOMAINDIR exists without NO_GETTEXT=YesPlease' '
@@ -44,10 +44,10 @@
 test_expect_success GETTEXT_LOCALE 'sanity: gettext("") metadata is OK' '
     # Return value may be non-zero
     LANGUAGE=is LC_ALL="$is_IS_locale" gettext "" >zero-expect &&
-    grep "Project-Id-Version: Git" zero-expect &&
-    grep "Git Mailing List <git@vger.kernel.org>" zero-expect &&
-    grep "Content-Type: text/plain; charset=UTF-8" zero-expect &&
-    grep "Content-Transfer-Encoding: 8bit" zero-expect
+    test_grep "Project-Id-Version: Git" zero-expect &&
+    test_grep "Git Mailing List <git@vger.kernel.org>" zero-expect &&
+    test_grep "Content-Type: text/plain; charset=UTF-8" zero-expect &&
+    test_grep "Content-Transfer-Encoding: 8bit" zero-expect
 '
 
 test_expect_success GETTEXT_LOCALE 'sanity: gettext(unknown) is passed through' '
diff --git a/t/t0203-gettext-setlocale-sanity.sh b/t/t0203-gettext-setlocale-sanity.sh
index 0ce1f22..670082b 100755
--- a/t/t0203-gettext-setlocale-sanity.sh
+++ b/t/t0203-gettext-setlocale-sanity.sh
@@ -12,7 +12,7 @@
 	test_commit "iso-c-commit" iso-under-c &&
 	git show >out 2>err &&
 	test_must_be_empty err &&
-	grep -q "iso-c-commit" out
+	test_grep -q "iso-c-commit" out
 '
 
 test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' '
@@ -20,7 +20,7 @@
 	test_commit "iso-utf8-commit" iso-under-utf8 &&
 	LANGUAGE=is LC_ALL="$is_IS_locale" git show >out 2>err &&
 	test_must_be_empty err &&
-	grep -q "iso-utf8-commit" out
+	test_grep -q "iso-utf8-commit" out
 '
 
 test_done
diff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh
index 28d92bb..48ccdda 100755
--- a/t/t0204-gettext-reencode-sanity.sh
+++ b/t/t0204-gettext-reencode-sanity.sh
@@ -66,22 +66,22 @@
 # eyes.
 test_expect_success GETTEXT_ISO_LOCALE 'gettext: Fetching a UTF-8 msgid -> ISO-8859-1' '
     LANGUAGE=is LC_ALL="$is_IS_iso_locale" gettext "TEST: ‘single’ and “double” quotes" >actual &&
-    grep "einfaldar" actual &&
-    grep "$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)" actual
+    test_grep "einfaldar" actual &&
+    test_grep "$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)" actual
 '
 
 test_expect_success GETTEXT_LOCALE 'gettext.c: git init UTF-8 -> UTF-8' '
     printf "Bjó til tóma Git lind" >expect &&
     LANGUAGE=is LC_ALL="$is_IS_locale" git init repo >actual &&
     test_when_finished "rm -rf repo" &&
-    grep "^$(cat expect) " actual
+    test_grep "^$(cat expect) " actual
 '
 
 test_expect_success GETTEXT_ISO_LOCALE 'gettext.c: git init UTF-8 -> ISO-8859-1' '
     printf "Bjó til tóma Git lind" >expect &&
     LANGUAGE=is LC_ALL="$is_IS_iso_locale" git init repo >actual &&
     test_when_finished "rm -rf repo" &&
-    grep "^$(iconv -f UTF-8 -t ISO8859-1 <expect) " actual
+    test_grep "^$(iconv -f UTF-8 -t ISO8859-1 <expect) " actual
 '
 
 test_done
diff --git a/t/t0210-trace2-normal.sh b/t/t0210-trace2-normal.sh
index 7e1e7af..beed2b2 100755
--- a/t/t0210-trace2-normal.sh
+++ b/t/t0210-trace2-normal.sh
@@ -333,12 +333,12 @@
 
 	GIT_TRACE2="$(pwd)/trace.normal" \
 		git clone https://user:pwd@example.com/ clone &&
-	! grep user:pwd trace.normal &&
+	test_grep ! user:pwd trace.normal &&
 
 	GIT_TRACE2_REDACT=0 GIT_TRACE2="$(pwd)/unredacted.normal" \
 		git clone https://user:pwd@example.com/ clone2 &&
-	grep "start .* clone https://user:pwd@example.com" unredacted.normal &&
-	grep "remote.origin.url=https://user:pwd@example.com" unredacted.normal
+	test_grep "start .* clone https://user:pwd@example.com" unredacted.normal &&
+	test_grep "remote.origin.url=https://user:pwd@example.com" unredacted.normal
 '
 
 test_done
diff --git a/t/t0211-trace2-perf.sh b/t/t0211-trace2-perf.sh
index 760cf69..665c8b3 100755
--- a/t/t0211-trace2-perf.sh
+++ b/t/t0211-trace2-perf.sh
@@ -283,13 +283,13 @@
 
 	GIT_TRACE2_PERF="$(pwd)/trace.perf" \
 		git clone https://user:pwd@example.com/ clone &&
-	! grep user:pwd trace.perf &&
+	test_grep ! user:pwd trace.perf &&
 
 	GIT_TRACE2_REDACT=0 GIT_TRACE2_PERF="$(pwd)/unredacted.perf" \
 		git clone https://user:pwd@example.com/ clone2 &&
 	perl "$TEST_DIRECTORY/t0211/scrub_perf.perl" <unredacted.perf >actual &&
-	grep "d0|main|start|.* clone https://user:pwd@example.com" actual &&
-	grep "d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com" actual
+	test_grep "d0|main|start|.* clone https://user:pwd@example.com" actual &&
+	test_grep "d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com" actual
 '
 
 # Confirm that the requested command produces a "cmd_name" and a
@@ -358,13 +358,13 @@
 
 	perl "$TEST_DIRECTORY/t0211/scrub_perf.perl" <prop.perf >actual &&
 
-	grep "d0|main|cmd_name|.*|_run_dashed_" actual &&
-	grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_dashed_" actual &&
+	test_grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
 
-	grep "d1|main|cmd_name|.*|remote-curl" actual &&
-	grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
+	test_grep "d1|main|cmd_name|.*|remote-curl" actual &&
+	test_grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
 '
 
 # Similarly, `git-http-fetch` is not built from git.c so do a
@@ -389,13 +389,13 @@
 
 	perl "$TEST_DIRECTORY/t0211/scrub_perf.perl" <prop.perf >actual &&
 
-	grep "d0|main|cmd_name|.*|_run_dashed_" actual &&
-	grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_dashed_" actual &&
+	test_grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
 
-	grep "d1|main|cmd_name|.*|http-fetch" actual &&
-	grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
+	test_grep "d1|main|cmd_name|.*|http-fetch" actual &&
+	test_grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
 '
 
 # Historically, alias expansion explicitly emitted the def_param
@@ -421,22 +421,22 @@
 	perl "$TEST_DIRECTORY/t0211/scrub_perf.perl" <prop.perf >actual &&
 
 	# "git xxx" is first mapped to "git-xxx" and the child will fail.
-	grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)" actual &&
 
 	# We unpeel that and substitute "version" into "xxx" (giving
 	# "git version") and update the cmd_name event.
-	grep "d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)" actual &&
 
 	# These def_param events could be associated with either of the
 	# above cmd_name events.  It does not matter.
-	grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
+	test_grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
 
 	# The "git version" child sees a different cmd_name hierarchy.
 	# Also test the def_param (only for completeness).
-	grep "d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)" actual &&
-	grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
+	test_grep "d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)" actual &&
+	test_grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
 '
 
 test_expect_success 'expect def_params during shell alias expansion' '
@@ -456,25 +456,25 @@
 	perl "$TEST_DIRECTORY/t0211/scrub_perf.perl" <prop.perf >actual &&
 
 	# "git xxx" is first mapped to "git-xxx" and the child will fail.
-	grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)" actual &&
 
 	# We unpeel that and substitute "git version" for "git xxx" (as a
 	# shell command.  Another cmd_name event is emitted as we unpeel.
-	grep "d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)" actual &&
 
 	# These def_param events could be associated with either of the
 	# above cmd_name events.  It does not matter.
-	grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
+	test_grep "d0|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
 
 	# We get the following only because we used a git command for the
 	# shell command. In general, it could have been a shell script and
 	# we would see nothing.
 	#
 	# The child knows the cmd_name hierarchy so it includes it.
-	grep "d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)" actual &&
-	grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
+	test_grep "d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)" actual &&
+	test_grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
 '
 
 test_expect_success 'expect def_params during nested git alias expansion' '
@@ -496,33 +496,33 @@
 
 	# "git xxx" is first mapped to "git-xxx" and try to spawn "git-xxx"
 	# and the child will fail.
-	grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)" actual &&
-	grep "d0|main|child_start|.*|.* class:dashed argv:\[git-xxx\]" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)" actual &&
+	test_grep "d0|main|child_start|.*|.* class:dashed argv:\[git-xxx\]" actual &&
 
 	# We unpeel that and substitute "yyy" into "xxx" (giving "git yyy")
 	# and spawn "git-yyy" and the child will fail.
-	grep "d0|main|alias|.*|alias:xxx argv:\[yyy\]" actual &&
-	grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)" actual &&
-	grep "d0|main|child_start|.*|.* class:dashed argv:\[git-yyy\]" actual &&
+	test_grep "d0|main|alias|.*|alias:xxx argv:\[yyy\]" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)" actual &&
+	test_grep "d0|main|child_start|.*|.* class:dashed argv:\[git-yyy\]" actual &&
 
 	# We unpeel that and substitute "version" into "xxx" (giving
 	# "git version") and update the cmd_name event.
-	grep "d0|main|alias|.*|alias:yyy argv:\[version\]" actual &&
-	grep "d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)" actual &&
+	test_grep "d0|main|alias|.*|alias:yyy argv:\[version\]" actual &&
+	test_grep "d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)" actual &&
 
 	# These def_param events could be associated with any of the
 	# above cmd_name events.  It does not matter.
 	grep "d0|main|def_param|.*|cfg.prop.foo:red" actual >actual.matches &&
-	grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
+	test_grep "d0|main|def_param|.*|ENV_PROP_FOO:blue" actual &&
 
 	# However, we do not want them repeated each time we unpeel.
 	test_line_count = 1 actual.matches &&
 
 	# The "git version" child sees a different cmd_name hierarchy.
 	# Also test the def_param (only for completeness).
-	grep "d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)" actual &&
-	grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
-	grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
+	test_grep "d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)" actual &&
+	test_grep "d1|main|def_param|.*|cfg.prop.foo:red" actual &&
+	test_grep "d1|main|def_param|.*|ENV_PROP_FOO:blue" actual
 '
 
 test_done
diff --git a/t/t0212-trace2-event.sh b/t/t0212-trace2-event.sh
index 1211db9..f5358a1 100755
--- a/t/t0212-trace2-event.sh
+++ b/t/t0212-trace2-event.sh
@@ -332,7 +332,7 @@
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 300redact_start git clone https://user:pwd@example.com/ clone2 &&
-	! grep user:pwd trace.event
+	test_grep ! user:pwd trace.event
 '
 
 test_expect_success 'unsafe URLs are redacted by default in child_start events' '
@@ -341,7 +341,7 @@
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 301redact_child_start git clone https://user:pwd@example.com/ clone2 &&
-	! grep user:pwd trace.event
+	test_grep ! user:pwd trace.event
 '
 
 test_expect_success 'unsafe URLs are redacted by default in exec events' '
@@ -350,7 +350,7 @@
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 302redact_exec git clone https://user:pwd@example.com/ clone2 &&
-	! grep user:pwd trace.event
+	test_grep ! user:pwd trace.event
 '
 
 test_expect_success 'unsafe URLs are redacted by default in def_param events' '
@@ -359,7 +359,7 @@
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 303redact_def_param url https://user:pwd@example.com/ &&
-	! grep user:pwd trace.event
+	test_grep ! user:pwd trace.event
 '
 
 test_done
diff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh
index 64ead15..ea11cdb 100755
--- a/t/t0300-credentials.sh
+++ b/t/t0300-credentials.sh
@@ -1014,7 +1014,7 @@
 	do
 		git -c credential.$partial.helper=yep \
 			credential fill <stdin >stdout &&
-		grep yep stdout ||
+		test_grep yep stdout ||
 		return 1
 	done &&
 
@@ -1030,7 +1030,7 @@
 	do
 		git -c credential.$partial.helper=yep \
 			credential fill <stdin >stdout &&
-		! grep yep stdout ||
+		test_grep ! yep stdout ||
 		return 1
 	done &&
 
diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh
index dff442d..788e9a1 100755
--- a/t/t0410-partial-clone.sh
+++ b/t/t0410-partial-clone.sh
@@ -206,7 +206,7 @@
 	git -C repo cat-file -p "$HASH" 2>err &&
 
 	# Ensure that no spurious FETCH_HEAD messages are written
-	! grep FETCH_HEAD err &&
+	test_grep ! FETCH_HEAD err &&
 
 	# Ensure that the .promisor file is written, and check that its
 	# associated packfile contains the object
@@ -214,7 +214,7 @@
 	test_line_count = 1 promisorlist &&
 	IDX=$(sed "s/promisor$/idx/" promisorlist) &&
 	git verify-pack --verbose "$IDX" >out &&
-	grep "$HASH" out
+	test_grep "$HASH" out
 '
 
 test_expect_success 'fetching of a promised object that promisor remote no longer has' '
@@ -228,7 +228,7 @@
 
 	rm -rf unreliable-server/.git/objects/* &&
 	test_must_fail git -C unreliable-client checkout HEAD 2>err &&
-	grep "could not fetch.*from promisor remote" err
+	test_grep "could not fetch.*from promisor remote" err
 '
 
 test_expect_success 'fetching of missing objects works with ref-in-want enabled' '
@@ -240,7 +240,7 @@
 	rm -rf repo/.git/objects/* &&
 	rm -f trace &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C repo cat-file -p "$HASH" &&
-	grep "fetch< fetch=.*ref-in-want" trace
+	test_grep "fetch< fetch=.*ref-in-want" trace
 '
 
 test_expect_success 'fetching from another promisor remote' '
@@ -263,7 +263,7 @@
 	test_line_count = 1 promisorlist &&
 	IDX=$(sed "s/promisor$/idx/" promisorlist) &&
 	git verify-pack --verbose "$IDX" >out &&
-	grep "$HASH2" out
+	test_grep "$HASH2" out
 '
 
 test_expect_success 'fetching with --filter configures a promisor remote' '
@@ -286,7 +286,7 @@
 	test_line_count = 1 promisorlist &&
 	IDX=$(sed "s/promisor$/idx/" promisorlist) &&
 	git verify-pack --verbose "$IDX" >out &&
-	grep "$HASH3" out
+	test_grep "$HASH3" out
 '
 
 test_expect_success 'fetching of missing blobs works' '
@@ -327,8 +327,8 @@
 
 	# Ensure that the tree, but not the blob, is fetched
 	git -C repo rev-list --objects --missing=print $(cat treehash) >objects &&
-	grep "^$(cat treehash)" objects &&
-	grep "^[?]$(cat blobhash)" objects
+	test_grep "^$(cat treehash)" objects &&
+	test_grep "^[?]$(cat blobhash)" objects
 '
 
 test_expect_success 'rev-list stops traversal at missing and promised commit' '
@@ -343,8 +343,8 @@
 	git -C repo config core.repositoryformatversion 1 &&
 	git -C repo config extensions.partialclone "arbitrary string" &&
 	git -C repo rev-list --exclude-promisor-objects --objects bar >out &&
-	grep $(git -C repo rev-parse bar) out &&
-	! grep $FOO out
+	test_grep $(git -C repo rev-parse bar) out &&
+	test_grep ! $FOO out
 '
 
 test_expect_success 'missing tree objects with --missing=allow-promisor and --exclude-promisor-objects' '
@@ -413,10 +413,10 @@
 	git -C repo config core.repositoryformatversion 1 &&
 	git -C repo config extensions.partialclone "arbitrary string" &&
 	git -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&
-	grep $(git -C repo rev-parse foo) out &&
-	! grep $TREE out &&
-	grep $(git -C repo rev-parse HEAD) out &&
-	! grep $TREE2 out
+	test_grep $(git -C repo rev-parse foo) out &&
+	test_grep ! $TREE out &&
+	test_grep $(git -C repo rev-parse HEAD) out &&
+	test_grep ! $TREE2 out
 '
 
 test_expect_success 'rev-list stops traversal at missing and promised blob' '
@@ -432,8 +432,8 @@
 	git -C repo config core.repositoryformatversion 1 &&
 	git -C repo config extensions.partialclone "arbitrary string" &&
 	git -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&
-	grep $(git -C repo rev-parse HEAD) out &&
-	! grep $BLOB out
+	test_grep $(git -C repo rev-parse HEAD) out &&
+	test_grep ! $BLOB out
 '
 
 test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob' '
@@ -451,10 +451,10 @@
 	git -C repo config core.repositoryformatversion 1 &&
 	git -C repo config extensions.partialclone "arbitrary string" &&
 	git -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&
-	! grep $COMMIT out &&
-	! grep $TREE out &&
-	! grep $BLOB out &&
-	grep $(git -C repo rev-parse bar) out  # sanity check that some walking was done
+	test_grep ! $COMMIT out &&
+	test_grep ! $TREE out &&
+	test_grep ! $BLOB out &&
+	test_grep $(git -C repo rev-parse bar) out  # sanity check that some walking was done
 '
 
 test_expect_success 'rev-list dies for missing objects on cmd line' '
@@ -489,6 +489,24 @@
 	done
 '
 
+test_expect_success '--exclude-promisor-objects with ^@ on missing object' '
+	rm -rf repo &&
+	test_create_repo repo &&
+	test_commit -C repo foo &&
+	test_commit -C repo bar &&
+
+	COMMIT=$(git -C repo rev-parse foo) &&
+	promise_and_delete "$COMMIT" &&
+
+	git -C repo config core.repositoryformatversion 1 &&
+	git -C repo config extensions.partialclone "arbitrary string" &&
+
+	# Ensure that "$COMMIT^@" is handled gracefully even though the
+	# actual commits are missing.
+	git -C repo rev-list --exclude-promisor-objects "$COMMIT^@" >out &&
+	test_must_be_empty out
+'
+
 test_expect_success 'single promisor remote can be re-initialized gracefully' '
 	# ensure one promisor is in the promisors list
 	rm -rf repo &&
@@ -523,10 +541,10 @@
 	test_line_count = 1 promisorlist &&
 	PROMISOR_PACKFILE=$(sed "s/.promisor/.pack/" <promisorlist) &&
 	git verify-pack $PROMISOR_PACKFILE -v >out &&
-	grep "$TREE_ONE" out &&
-	grep "$TREE_TWO" out &&
-	! grep "$(git -C repo rev-parse one)" out &&
-	! grep "$(git -C repo rev-parse two)" out &&
+	test_grep "$TREE_ONE" out &&
+	test_grep "$TREE_TWO" out &&
+	test_grep ! "$(git -C repo rev-parse one)" out &&
+	test_grep ! "$(git -C repo rev-parse two)" out &&
 
 	# Remove the promisor packfile and associated files
 	rm $(sed "s/.promisor//" <promisorlist).* &&
@@ -536,10 +554,10 @@
 	ls repo/.git/objects/pack/pack-*.pack >packlist &&
 	test_line_count = 1 packlist &&
 	git verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&
-	grep "$(git -C repo rev-parse one)" out &&
-	grep "$(git -C repo rev-parse two)" out &&
-	! grep "$TREE_ONE" out &&
-	! grep "$TREE_TWO" out
+	test_grep "$(git -C repo rev-parse one)" out &&
+	test_grep "$(git -C repo rev-parse two)" out &&
+	test_grep ! "$TREE_ONE" out &&
+	test_grep ! "$TREE_TWO" out
 '
 
 test_expect_success 'gc does not repack promisor objects if there are none' '
@@ -616,8 +634,8 @@
 	ls repo/.git/objects/pack/pack-*.pack >packlist &&
 	test_line_count = 1 packlist &&
 	git verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&
-	grep "$(git -C repo rev-parse HEAD)" out &&
-	! grep "$TREE_HASH" out
+	test_grep "$(git -C repo rev-parse HEAD)" out &&
+	test_grep ! "$TREE_HASH" out
 '
 
 test_expect_success 'do not fetch when checking existence of tree we construct ourselves' '
@@ -647,10 +665,10 @@
 
 	git clone --filter=blob:none --bare "file://$(pwd)/repo" partial.git &&
 	git -C partial.git rev-list --objects --missing=print HEAD >out &&
-	grep "[?]$FILE_HASH" out &&
+	test_grep "[?]$FILE_HASH" out &&
 	git -C partial.git log --follow -- new-file.txt &&
 	git -C partial.git rev-list --objects --missing=print HEAD >out &&
-	grep "[?]$FILE_HASH" out
+	test_grep "[?]$FILE_HASH" out
 '
 
 test_expect_success 'lazy-fetch when accessing object not in the_repository' '
@@ -665,7 +683,7 @@
 
 	# Sanity check that the file is missing
 	git -C partial.git rev-list --objects --missing=print HEAD >out &&
-	grep "[?]$FILE_HASH" out &&
+	test_grep "[?]$FILE_HASH" out &&
 
 	# The no-lazy-fetch mechanism prevents Git from fetching
 	test_must_fail env GIT_NO_LAZY_FETCH=1 \
@@ -680,7 +698,7 @@
 
 	# Sanity check that the file is still missing
 	git -C partial.git rev-list --objects --missing=print HEAD >out &&
-	grep "[?]$FILE_HASH" out &&
+	test_grep "[?]$FILE_HASH" out &&
 
 	git -C full cat-file -s "$FILE_HASH" >expect &&
 	test-tool partial-clone object-info partial.git "$FILE_HASH" >actual &&
@@ -688,7 +706,7 @@
 
 	# Sanity check that the file is now present
 	git -C partial.git rev-list --objects --missing=print HEAD >out &&
-	! grep "[?]$FILE_HASH" out
+	test_grep ! "[?]$FILE_HASH" out
 '
 
 test_expect_success 'push should not fetch new commit objects' '
@@ -705,9 +723,9 @@
 	COMMIT=$(git -C server rev-parse server2) &&
 
 	test_must_fail git -C client push 2>err &&
-	grep "fetch first" err &&
+	test_grep "fetch first" err &&
 	git -C client rev-list --objects --missing=print "$COMMIT" >objects &&
-	grep "^[?]$COMMIT" objects
+	test_grep "^[?]$COMMIT" objects
 '
 
 test_expect_success 'setup for promisor.quiet tests' '
@@ -750,7 +768,7 @@
 	test_terminal git -C repo cat-file -p foo:foo.t 2>err &&
 
 	# Ensure that progress messages are written
-	grep "Receiving objects" err
+	test_grep "Receiving objects" err
 '
 
 test_expect_success TTY 'promisor.quiet=true does not show progress messages' '
@@ -761,7 +779,7 @@
 	test_terminal git -C repo cat-file -p foo:foo.t 2>err &&
 
 	# Ensure that no progress messages are written
-	! grep "Receiving objects" err
+	test_grep ! "Receiving objects" err
 '
 
 test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '
@@ -771,7 +789,7 @@
 	test_terminal git -C repo cat-file -p foo:foo.t 2>err &&
 
 	# Ensure that progress messages are written
-	grep "Receiving objects" err
+	test_grep "Receiving objects" err
 '
 
 test_expect_success 'promisor.quiet from submodule repo is honored' '
@@ -819,7 +837,7 @@
 	test_line_count = 1 promisorlist &&
 	IDX=$(sed "s/promisor$/idx/" promisorlist) &&
 	git verify-pack --verbose "$IDX" >out &&
-	grep "$HASH" out
+	test_grep "$HASH" out
 '
 
 # DO NOT add non-httpd-specific tests here, because the last part of this
diff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh
index 822b0d5..55c0fb3 100755
--- a/t/t0450-txt-doc-vs-help.sh
+++ b/t/t0450-txt-doc-vs-help.sh
@@ -29,7 +29,7 @@
 		return 0
 	fi &&
 	mkdir -p "$out_dir" &&
-	test_expect_code 129 git $builtin -h >"$out.raw" 2>&1 &&
+	test_might_fail git $builtin -h >"$out.raw" 2>&1 &&
 	sed -n \
 		-e '1,/^$/ {
 			/^$/d;
@@ -87,7 +87,7 @@
 	# -h output assertions
 	test_expect_success "$builtin -h output has no \t" '
 		h2s="$(help_to_synopsis "$builtin")" &&
-		! grep "$HT" "$h2s"
+		test_grep ! "$HT" "$h2s"
 	'
 
 	test_expect_success "$builtin -h output has dashed labels" '
diff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches
index e8d6c13..c4a55ff 100644
--- a/t/t0450/adoc-help-mismatches
+++ b/t/t0450/adoc-help-mismatches
@@ -12,7 +12,6 @@
 credential
 credential-cache
 credential-store
-fast-export
 fast-import
 fetch-pack
 fmt-merge-msg
diff --git a/t/t0500-progress-display.sh b/t/t0500-progress-display.sh
index d1a498a..fe2232c 100755
--- a/t/t0500-progress-display.sh
+++ b/t/t0500-progress-display.sh
@@ -320,8 +320,8 @@
 
 	# t0212/parse_events.perl intentionally omits regions and data.
 	test_region progress "Working hard" trace.event &&
-	grep "\"key\":\"total_objects\",\"value\":\"40\"" trace.event &&
-	grep "\"key\":\"total_bytes\",\"value\":\"409600\"" trace.event
+	test_grep "\"key\":\"total_objects\",\"value\":\"40\"" trace.event &&
+	test_grep "\"key\":\"total_bytes\",\"value\":\"409600\"" trace.event
 '
 
 test_expect_success 'progress generates traces: stop / start' '
@@ -344,8 +344,8 @@
 	LSAN_OPTIONS=detect_leaks=0 \
 	test-tool progress \
 		<in 2>stderr &&
-	grep region_enter.*progress trace-start.event &&
-	! grep region_leave.*progress trace-start.event
+	test_grep region_enter.*progress trace-start.event &&
+	test_grep ! region_leave.*progress trace-start.event
 '
 
 test_expect_success 'progress generates traces: stop without start' '
@@ -355,8 +355,8 @@
 
 	GIT_TRACE2_EVENT="$PWD/trace-stop.event" test-tool progress \
 		<in 2>stderr &&
-	! grep region_enter.*progress trace-stop.event &&
-	! grep region_leave.*progress trace-stop.event
+	test_grep ! region_enter.*progress trace-stop.event &&
+	test_grep ! region_leave.*progress trace-stop.event
 '
 
 test_expect_success 'progress generates traces: start with active progress bar (no stops)' '
@@ -369,9 +369,9 @@
 	LSAN_OPTIONS=detect_leaks=0 \
 	test-tool progress \
 		<in 2>stderr &&
-	grep region_enter.*progress.*One trace-2start.event &&
-	grep region_enter.*progress.*Two trace-2start.event &&
-	! grep region_leave trace-2start.event
+	test_grep region_enter.*progress.*One trace-2start.event &&
+	test_grep region_enter.*progress.*Two trace-2start.event &&
+	test_grep ! region_leave trace-2start.event
 '
 
 test_done
diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh
index e19e036..35e98b4 100755
--- a/t/t0610-reftable-basics.sh
+++ b/t/t0610-reftable-basics.sh
@@ -15,9 +15,9 @@
 INVALID_OID=$(test_oid 001)
 
 test_expect_success 'pack-refs does not crash with -h' '
-	test_expect_code 129 git pack-refs -h >usage &&
+	git pack-refs -h >usage &&
 	test_grep "[Uu]sage: git pack-refs " usage &&
-	test_expect_code 129 nongit git pack-refs -h >usage &&
+	nongit git pack-refs -h >usage &&
 	test_grep "[Uu]sage: git pack-refs " usage
 '
 
@@ -776,11 +776,11 @@
 		test_commit file3 &&
 		test_commit file4 &&
 		git reflog >actual &&
-		grep file3 actual &&
+		test_grep file3 actual &&
 
 		git reflog delete HEAD@{1} &&
 		git reflog >actual &&
-		! grep file3 actual
+		test_grep ! file3 actual
 	)
 '
 
@@ -902,8 +902,8 @@
 		done &&
 		git reflog refs/heads/main >actual &&
 		test_line_count = 10 actual &&
-		grep "commit (initial): number 1" actual &&
-		grep "commit: number 10" actual &&
+		test_grep "commit (initial): number 1" actual &&
+		test_grep "commit: number 10" actual &&
 
 		git gc &&
 		git reflog refs/heads/main >actual &&
diff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh
index 11bf104..70ba3ee 100755
--- a/t/t1004-read-tree-m-u-wf.sh
+++ b/t/t1004-read-tree-m-u-wf.sh
@@ -142,8 +142,8 @@
 
 	echo >>file1 "local changes" &&
 	read_tree_u_must_succeed -m -u branch-point side-a side-b &&
-	grep "new line to be kept" file1 &&
-	grep "local changes" file1
+	test_grep "new line to be kept" file1 &&
+	test_grep "local changes" file1
 
 '
 
@@ -156,8 +156,8 @@
 
 	echo >>file2 "local changes" &&
 	read_tree_u_must_fail -m -u branch-point side-a side-b &&
-	! grep "new line to be kept" file2 &&
-	grep "local changes" file2
+	test_grep ! "new line to be kept" file2 &&
+	test_grep "local changes" file2
 
 '
 
diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh
index 8e2c526..0c1dc0f 100755
--- a/t/t1006-cat-file.sh
+++ b/t/t1006-cat-file.sh
@@ -3,7 +3,8 @@
 test_description='git cat-file'
 
 . ./test-lib.sh
-. "$TEST_DIRECTORY/lib-loose.sh"
+. "$TEST_DIRECTORY"/lib-loose.sh
+. "$TEST_DIRECTORY"/lib-cat-file.sh
 
 test_cmdmode_usage () {
 	test_expect_code 129 "$@" 2>err &&
@@ -99,18 +100,6 @@
 	'
 done
 
-echo_without_newline () {
-    printf '%s' "$*"
-}
-
-echo_without_newline_nul () {
-	echo_without_newline "$@" | tr '\n' '\0'
-}
-
-strlen () {
-    echo_without_newline "$1" | wc -c | sed -e 's/^ *//'
-}
-
 run_tests () {
     type=$1
     object_name="$2"
@@ -696,8 +685,8 @@
 	git repack -ad &&
 	git cat-file --batch-check="%(deltabase)" <blobs >actual &&
 	{
-		grep "$(git rev-parse HEAD:foo)" actual ||
-		grep "$(git rev-parse HEAD:foo-plus)" actual
+		test_grep "$(git rev-parse HEAD:foo)" actual ||
+		test_grep "$(git rev-parse HEAD:foo-plus)" actual
 	}
 '
 
@@ -826,7 +815,7 @@
 		# Swap the two to corrupt the repository
 		mv -f "$other_path" "$empty_path" &&
 		test_must_fail git fsck 2>err.fsck &&
-		grep "hash-path mismatch" err.fsck &&
+		test_grep "hash-path mismatch" err.fsck &&
 
 		# confirm that cat-file is reading the new swapped-in
 		# blob...
@@ -1318,37 +1307,37 @@
 test_expect_success 'batch-command empty command' '
 	echo "" >cmd &&
 	test_expect_code 128 git cat-file --batch-command <cmd 2>err &&
-	grep "^fatal:.*empty command in input.*" err
+	test_grep "^fatal:.*empty command in input.*" err
 '
 
 test_expect_success 'batch-command whitespace before command' '
 	echo " info deadbeef" >cmd &&
 	test_expect_code 128 git cat-file --batch-command <cmd 2>err &&
-	grep "^fatal:.*whitespace before command.*" err
+	test_grep "^fatal:.*whitespace before command.*" err
 '
 
 test_expect_success 'batch-command unknown command' '
 	echo unknown_command >cmd &&
 	test_expect_code 128 git cat-file --batch-command <cmd 2>err &&
-	grep "^fatal:.*unknown command.*" err
+	test_grep "^fatal:.*unknown command.*" err
 '
 
 test_expect_success 'batch-command missing arguments' '
 	echo "info" >cmd &&
 	test_expect_code 128 git cat-file --batch-command <cmd 2>err &&
-	grep "^fatal:.*info requires arguments.*" err
+	test_grep "^fatal:.*info requires arguments.*" err
 '
 
 test_expect_success 'batch-command flush with arguments' '
 	echo "flush arg" >cmd &&
 	test_expect_code 128 git cat-file --batch-command --buffer <cmd 2>err &&
-	grep "^fatal:.*flush takes no arguments.*" err
+	test_grep "^fatal:.*flush takes no arguments.*" err
 '
 
 test_expect_success 'batch-command flush without --buffer' '
 	echo "flush" >cmd &&
 	test_expect_code 128 git cat-file --batch-command <cmd 2>err &&
-	grep "^fatal:.*flush is only for --buffer mode.*" err
+	test_grep "^fatal:.*flush is only for --buffer mode.*" err
 '
 
 perl_script='
diff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh
index 4bc82dd..463b38f 100755
--- a/t/t1007-hash-object.sh
+++ b/t/t1007-hash-object.sh
@@ -205,7 +205,7 @@
 test_expect_success 'too-short tree' '
 	echo abc >malformed-tree &&
 	test_must_fail git hash-object -t tree malformed-tree 2>err &&
-	grep "too-short tree object" err
+	test_grep "too-short tree object" err
 '
 
 test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '
@@ -213,7 +213,7 @@
 	bin_oid=$(echo $hex_oid | hex2oct) &&
 	printf "9100644 \0$bin_oid" >tree-with-malformed-mode &&
 	test_must_fail git hash-object -t tree tree-with-malformed-mode 2>err &&
-	grep "malformed mode in tree entry" err
+	test_grep "malformed mode in tree entry" err
 '
 
 test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '
@@ -221,7 +221,7 @@
 	bin_oid=$(echo $hex_oid | hex2oct) &&
 	printf "100644 \0$bin_oid" >tree-with-empty-filename &&
 	test_must_fail git hash-object -t tree tree-with-empty-filename 2>err &&
-	grep "empty filename in tree entry" err
+	test_grep "empty filename in tree entry" err
 '
 
 test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '
@@ -232,7 +232,7 @@
 		printf "100644 file\0$bin_oid"
 	} >tree-with-duplicate-filename &&
 	test_must_fail git hash-object -t tree tree-with-duplicate-filename 2>err &&
-	grep "duplicateEntries" err
+	test_grep "duplicateEntries" err
 '
 
 test_expect_success 'corrupt commit' '
diff --git a/t/t1011-read-tree-sparse-checkout.sh b/t/t1011-read-tree-sparse-checkout.sh
index 742f0fa..93244ee 100755
--- a/t/t1011-read-tree-sparse-checkout.sh
+++ b/t/t1011-read-tree-sparse-checkout.sh
@@ -196,7 +196,7 @@
 	echo dirty >init.t &&
 	read_tree_u_must_fail -m -u HEAD^ &&
 	test_path_is_file init.t &&
-	grep -q dirty init.t
+	test_grep -q dirty init.t
 '
 
 test_expect_success 'read-tree will not throw away dirty changes, sparse' '
@@ -207,7 +207,7 @@
 	echo sub/added >.git/info/sparse-checkout &&
 	read_tree_u_must_fail -m -u HEAD^ &&
 	test_path_is_file init.t &&
-	grep -q dirty init.t
+	test_grep -q dirty init.t
 '
 
 test_expect_success 'read-tree updates worktree, dirty case' '
@@ -215,7 +215,7 @@
 	git checkout -f top &&
 	echo dirty >init.t &&
 	read_tree_u_must_fail -m -u HEAD^ &&
-	grep -q dirty init.t &&
+	test_grep -q dirty init.t &&
 	rm init.t
 '
 
@@ -224,7 +224,7 @@
 	git checkout -f top &&
 	echo dirty >added &&
 	read_tree_u_must_succeed -m -u HEAD^ &&
-	grep -q dirty added
+	test_grep -q dirty added
 '
 
 test_expect_success 'read-tree adds to worktree, absent case' '
@@ -240,7 +240,7 @@
 	mkdir sub &&
 	echo dirty >sub/added &&
 	read_tree_u_must_succeed -u -m HEAD^ &&
-	grep -q dirty sub/added
+	test_grep -q dirty sub/added
 '
 
 test_expect_success 'index removal and worktree narrowing at the same time' '
diff --git a/t/t1017-cat-file-remote-object-info.sh b/t/t1017-cat-file-remote-object-info.sh
new file mode 100755
index 0000000..116862f
--- /dev/null
+++ b/t/t1017-cat-file-remote-object-info.sh
@@ -0,0 +1,747 @@
+#!/bin/sh
+
+test_description='git cat-file --batch-command with remote-object-info command'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-cat-file.sh
+
+hello_content="Hello World"
+hello_size=$(strlen "$hello_content")
+hello_oid=$(echo_without_newline "$hello_content" | git hash-object --stdin)
+hello_short_oid=$(git rev-parse --short "$hello_oid")
+
+unstored_content="Hello Git"
+unstored_oid=$(echo_without_newline "$unstored_content" | git hash-object --stdin)
+
+# This is how we get 13:
+# 13 = <file mode> + <a_space> + <file name> + <a_null>, where
+# file mode is 100644, which is 6 characters;
+# file name is hello, which is 5 characters
+# a space is 1 character and a null is 1 character
+tree_size=$(($(test_oid rawsz) + 13))
+
+commit_message="Initial commit"
+
+# This is how we get 137:
+# 137 = <tree header> + <a_space> + <a newline> +
+# <Author line> + <a newline> +
+# <Committer line> + <a newline> +
+# <a newline> +
+# <commit message length>
+# An easier way to calculate is: 1. use `git cat-file commit <commit hash> | wc -c`,
+# to get 177, 2. then deduct 40 hex characters to get 137
+commit_size=$(($(test_oid hexsz) + 137))
+
+tag_header_without_oid="type blob
+tag hellotag
+tagger $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>"
+tag_header_without_timestamp="object $hello_oid
+$tag_header_without_oid"
+tag_description="This is a tag"
+tag_content="$tag_header_without_timestamp 0 +0000
+
+$tag_description"
+
+tag_oid=$(echo_without_newline "$tag_content" | git hash-object -t tag --stdin -w)
+tag_size=$(strlen "$tag_content")
+
+set_transport_variables () {
+	hello_oid=$(echo_without_newline "$hello_content" | git hash-object --stdin)
+	tree_oid=$(git -C "$1" write-tree)
+	commit_oid=$(echo_without_newline "$commit_message" | git -C "$1" commit-tree $tree_oid)
+	tag_oid=$(echo_without_newline "$tag_content" | git -C "$1" hash-object -t tag --stdin -w)
+	tag_size=$(strlen "$tag_content")
+}
+
+# This section tests --batch-command with remote-object-info command
+# Since "%(objecttype)" is currently not supported by the command remote-object-info ,
+# the filters are set to "%(objectname) %(objectsize)" in some test cases.
+
+# Test --batch-command remote-object-info with 'git://' transport with
+# transfer.advertiseobjectinfo set to true, i.e. server has object-info capability
+. "$TEST_DIRECTORY"/lib-git-daemon.sh
+start_git_daemon --export-all
+daemon_parent=$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent
+
+test_expect_success 'create repo to be served by git-daemon' '
+	git init "$daemon_parent" &&
+	echo_without_newline "$hello_content" > $daemon_parent/hello &&
+	git -C "$daemon_parent" update-index --add hello &&
+	git -C "$daemon_parent" config transfer.advertiseobjectinfo true &&
+	git clone "$GIT_DAEMON_URL/parent" -n "$daemon_parent/daemon_client_empty"
+'
+
+test_expect_success 'batch-command remote-object-info git://' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid
+		remote-object-info "$GIT_DAEMON_URL/parent" $tree_oid
+		remote-object-info "$GIT_DAEMON_URL/parent" $commit_oid
+		remote-object-info "$GIT_DAEMON_URL/parent" $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command remote-object-info git:// multiple sha1 per line' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid $tree_oid $commit_oid $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command remote-object-info git:// default filter' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		git cat-file --batch-command >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid $tree_oid
+		remote-object-info "$GIT_DAEMON_URL/parent" $commit_oid $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'remote-object-info does not change the default format of info' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		local_content="local object" &&
+		local_oid=$(echo_without_newline "$local_content" | git hash-object -w --stdin) &&
+		local_size=$(strlen "$local_content") &&
+
+		echo "$local_oid blob $local_size" >expect &&
+		echo "$hello_oid $hello_size" >>expect &&
+		echo "$local_oid blob $local_size" >>expect &&
+
+		git cat-file --batch-command >actual <<-EOF &&
+		info $local_oid
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid
+		info $local_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command --buffer remote-object-info git://' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" --buffer >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid $tree_oid
+		remote-object-info "$GIT_DAEMON_URL/parent" $commit_oid $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		flush
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command -Z remote-object-info git:// default filter' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		printf "%s\0" "$hello_oid $hello_size" >expect &&
+		printf "%s\0" "$tree_oid $tree_size" >>expect &&
+		printf "%s\0" "$commit_oid $commit_size" >>expect &&
+		printf "%s\0" "$tag_oid $tag_size" >>expect &&
+
+		printf "%s\0" "$hello_oid missing" >>expect &&
+		printf "%s\0" "$tree_oid missing" >>expect &&
+		printf "%s\0" "$commit_oid missing" >>expect &&
+		printf "%s\0" "$tag_oid missing" >>expect &&
+
+		batch_input="remote-object-info $GIT_DAEMON_URL/parent $hello_oid $tree_oid
+remote-object-info $GIT_DAEMON_URL/parent $commit_oid $tag_oid
+info $hello_oid
+info $tree_oid
+info $commit_oid
+info $tag_oid
+" &&
+		echo_without_newline_nul "$batch_input" >commands_null_delimited &&
+
+		git cat-file --batch-command -Z < commands_null_delimited >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'remote-object-info does not support short oids' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		test_must_fail git cat-file --batch-command 2>err <<-EOF &&
+		remote-object-info $GIT_DAEMON_URL/parent $hello_short_oid
+		EOF
+		test_grep "does not support short oids" err
+	)
+'
+
+test_expect_success 'remote-object-info does not die on missing oid like info' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		git cat-file --batch-command >local <<-EOF &&
+		info $unstored_oid
+		EOF
+		git cat-file --batch-command >remote <<-EOF &&
+		remote-object-info $GIT_DAEMON_URL/parent $unstored_oid
+		EOF
+		test_cmp local remote
+	)
+'
+
+# This tests depends on %(objecttype) not being supported yet, once supported
+# it needs to be updated.
+test_expect_success 'unsupported placeholder on remote returns empty string' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		echo "" >expect &&
+		git cat-file --batch-command="%(objecttype)" >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'requesting only objectname echoes back' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		echo $hello_oid >expect &&
+		git cat-file --batch-command="%(objectname)" >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'objectname goes through existence check' '
+	(
+		set_transport_variables "$daemon_parent" &&
+		cd "$daemon_parent/daemon_client_empty" &&
+
+		echo "$unstored_oid missing" >expect &&
+
+		git cat-file --batch-command="%(objectname)" >actual <<-EOF &&
+		remote-object-info "$GIT_DAEMON_URL/parent" $unstored_oid
+		EOF
+
+		test_cmp expect actual
+	)
+'
+
+# Test --batch-command remote-object-info with 'git://' and
+# transfer.advertiseobjectinfo set to false, i.e. server does not have object-info capability
+test_expect_success 'batch-command remote-object-info git:// fails when transfer.advertiseobjectinfo=false' '
+	(
+		git -C "$daemon_parent" config transfer.advertiseobjectinfo false &&
+		set_transport_variables "$daemon_parent" &&
+
+		test_must_fail git cat-file --batch-command="%(objectname) %(objectsize)" 2>err <<-EOF &&
+		remote-object-info $GIT_DAEMON_URL/parent $hello_oid $tree_oid $commit_oid $tag_oid
+		EOF
+		test_grep "object-info capability is not enabled on the server" err &&
+
+		# revert server state back
+		git -C "$daemon_parent" config transfer.advertiseobjectinfo true
+
+	)
+'
+
+stop_git_daemon
+
+# Test --batch-command remote-object-info with 'file://' transport with
+# transfer.advertiseobjectinfo set to true, i.e. server has object-info capability
+# shellcheck disable=SC2016
+test_expect_success 'create repo to be served by file:// transport' '
+	git init server &&
+	git -C server config protocol.version 2 &&
+	git -C server config transfer.advertiseobjectinfo true &&
+	echo_without_newline "$hello_content" > server/hello &&
+	git -C server update-index --add hello &&
+	git clone -n "file://$(pwd)/server" file_client_empty
+'
+
+test_expect_success 'batch-command remote-object-info file://' '
+	(
+		set_transport_variables "server" &&
+		server_path="$(pwd)/server" &&
+		cd file_client_empty &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" >actual <<-EOF &&
+		remote-object-info "file://${server_path}" $hello_oid
+		remote-object-info "file://${server_path}" $tree_oid
+		remote-object-info "file://${server_path}" $commit_oid
+		remote-object-info "file://${server_path}" $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command remote-object-info file:// multiple sha1 per line' '
+	(
+		set_transport_variables "server" &&
+		server_path="$(pwd)/server" &&
+		cd file_client_empty &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" >actual <<-EOF &&
+		remote-object-info "file://${server_path}" $hello_oid $tree_oid $commit_oid $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command --buffer remote-object-info file://' '
+	(
+		set_transport_variables "server" &&
+		server_path="$(pwd)/server" &&
+		cd file_client_empty &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" --buffer >actual <<-EOF &&
+		remote-object-info "file://${server_path}" $hello_oid $tree_oid
+		remote-object-info "file://${server_path}" $commit_oid $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		flush
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command remote-object-info file:// default filter' '
+	(
+		set_transport_variables "server" &&
+		server_path="$(pwd)/server" &&
+		cd file_client_empty &&
+
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		git cat-file --batch-command >actual <<-EOF &&
+		remote-object-info "file://${server_path}" $hello_oid $tree_oid
+		remote-object-info "file://${server_path}" $commit_oid $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command -Z remote-object-info file:// default filter' '
+	(
+		set_transport_variables "server" &&
+		server_path="$(pwd)/server" &&
+		cd file_client_empty &&
+
+		printf "%s\0" "$hello_oid $hello_size" >expect &&
+		printf "%s\0" "$tree_oid $tree_size" >>expect &&
+		printf "%s\0" "$commit_oid $commit_size" >>expect &&
+		printf "%s\0" "$tag_oid $tag_size" >>expect &&
+
+		printf "%s\0" "$hello_oid missing" >>expect &&
+		printf "%s\0" "$tree_oid missing" >>expect &&
+		printf "%s\0" "$commit_oid missing" >>expect &&
+		printf "%s\0" "$tag_oid missing" >>expect &&
+
+		batch_input="remote-object-info \"file://${server_path}\" $hello_oid $tree_oid
+remote-object-info \"file://${server_path}\" $commit_oid $tag_oid
+info $hello_oid
+info $tree_oid
+info $commit_oid
+info $tag_oid
+" &&
+		echo_without_newline_nul "$batch_input" >commands_null_delimited &&
+
+		git cat-file --batch-command -Z < commands_null_delimited >actual &&
+		test_cmp expect actual
+	)
+'
+
+# Test --batch-command remote-object-info with 'file://' and
+# transfer.advertiseobjectinfo set to false, i.e. server does not have object-info capability
+test_expect_success 'batch-command remote-object-info file:// fails when transfer.advertiseobjectinfo=false' '
+	(
+		set_transport_variables "server" &&
+		server_path="$(pwd)/server" &&
+		git -C "${server_path}" config transfer.advertiseobjectinfo false &&
+
+		test_must_fail git cat-file --batch-command="%(objectname) %(objectsize)" 2>err <<-EOF &&
+		remote-object-info "file://${server_path}" $hello_oid $tree_oid $commit_oid $tag_oid
+		EOF
+		test_grep "object-info capability is not enabled on the server" err &&
+
+		# revert server state back
+		git -C "${server_path}" config transfer.advertiseobjectinfo true
+	)
+'
+
+# Test --batch-command remote-object-info with 'http://' transport with
+# transfer.advertiseobjectinfo set to true, i.e. server has object-info capability
+
+. "$TEST_DIRECTORY"/lib-httpd.sh
+start_httpd
+
+test_expect_success 'create repo to be served by http:// transport' '
+	git init "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config http.receivepack true &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config transfer.advertiseobjectinfo true &&
+	echo_without_newline "$hello_content" > $HTTPD_DOCUMENT_ROOT_PATH/http_parent/hello &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" update-index --add hello &&
+	git clone "$HTTPD_URL/smart/http_parent" -n "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty"
+'
+
+test_expect_success 'batch-command remote-object-info http://' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" >actual <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid
+		remote-object-info "$HTTPD_URL/smart/http_parent" $tree_oid
+		remote-object-info "$HTTPD_URL/smart/http_parent" $commit_oid
+		remote-object-info "$HTTPD_URL/smart/http_parent" $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command remote-object-info http:// one line' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" >actual <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid $tree_oid $commit_oid $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command --buffer remote-object-info http://' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" &&
+
+		# These results prove remote-object-info can get object info from the remote
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		# These results prove remote-object-info did not download objects from the remote
+		echo "$hello_oid missing" >>expect &&
+		echo "$tree_oid missing" >>expect &&
+		echo "$commit_oid missing" >>expect &&
+		echo "$tag_oid missing" >>expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize)" --buffer >actual <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid $tree_oid
+		remote-object-info "$HTTPD_URL/smart/http_parent" $commit_oid $tag_oid
+		info $hello_oid
+		info $tree_oid
+		info $commit_oid
+		info $tag_oid
+		flush
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command remote-object-info http:// default filter' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" &&
+
+		echo "$hello_oid $hello_size" >expect &&
+		echo "$tree_oid $tree_size" >>expect &&
+		echo "$commit_oid $commit_size" >>expect &&
+		echo "$tag_oid $tag_size" >>expect &&
+
+		git cat-file --batch-command >actual <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid $tree_oid
+		remote-object-info "$HTTPD_URL/smart/http_parent" $commit_oid $tag_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'batch-command -Z remote-object-info http:// default filter' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" &&
+
+		printf "%s\0" "$hello_oid $hello_size" >expect &&
+		printf "%s\0" "$tree_oid $tree_size" >>expect &&
+		printf "%s\0" "$commit_oid $commit_size" >>expect &&
+		printf "%s\0" "$tag_oid $tag_size" >>expect &&
+
+		batch_input="remote-object-info $HTTPD_URL/smart/http_parent $hello_oid $tree_oid
+remote-object-info $HTTPD_URL/smart/http_parent $commit_oid $tag_oid
+" &&
+		echo_without_newline_nul "$batch_input" >commands_null_delimited &&
+
+		git cat-file --batch-command -Z < commands_null_delimited >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'remote-object-info fails on unsupported filter option (objectsize:disk)' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+
+		echo "$hello_oid " >expect &&
+
+		git cat-file --batch-command="%(objectname) %(objectsize:disk)" >actual <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'remote-object-info fails on unsupported filter option (deltabase)' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+
+		echo "" >expect &&
+
+		git cat-file --batch-command="%(deltabase)" >actual <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid
+		EOF
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'remote-object-info fails on server with legacy protocol' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+
+		test_must_fail git -c protocol.version=0 cat-file --batch-command="%(objectname) %(objectsize)" 2>err <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid
+		EOF
+		test_grep "object-info requires protocol v2" err
+	)
+'
+
+test_expect_success 'remote-object-info fails on server with legacy protocol with default filter' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+
+		test_must_fail git -c protocol.version=0 cat-file --batch-command 2>err <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid
+		EOF
+		test_grep "object-info requires protocol v2" err
+	)
+'
+
+test_expect_success 'remote-object-info fails on malformed OID' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		malformed_object_id="this_id_is_not_valid" &&
+
+		test_must_fail git cat-file --batch-command="%(objectname) %(objectsize)" 2>err <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $malformed_object_id
+		EOF
+		test_grep "not a valid object name '$malformed_object_id'" err
+	)
+'
+
+test_expect_success 'remote-object-info fails on malformed OID with default filter' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		malformed_object_id="this_id_is_not_valid" &&
+
+		test_must_fail git cat-file --batch-command 2>err <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $malformed_object_id
+		EOF
+		test_grep "not a valid object name '$malformed_object_id'" err
+	)
+'
+
+test_expect_success 'remote-object-info fails on not providing OID' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		cd "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+
+		test_must_fail git cat-file --batch-command="%(objectname) %(objectsize)" 2>err <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent"
+		EOF
+		test_grep "remote-object-info requires objects" err
+	)
+'
+
+
+# Test --batch-command remote-object-info with 'http://' transport and
+# transfer.advertiseobjectinfo set to false, i.e. server does not have object-info capability
+test_expect_success 'batch-command remote-object-info http:// fails when transfer.advertiseobjectinfo=false ' '
+	(
+		set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
+		git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config transfer.advertiseobjectinfo false &&
+
+		test_must_fail git cat-file --batch-command="%(objectname) %(objectsize)" 2>err <<-EOF &&
+		remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid $tree_oid $commit_oid $tag_oid
+		EOF
+		test_grep "object-info capability is not enabled on the server" err &&
+
+		# revert server state back
+		git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config transfer.advertiseobjectinfo true
+	)
+'
+
+# DO NOT add non-httpd-specific tests here, because the last part of this
+# test script is only executed when httpd is available and enabled.
+
+test_done
diff --git a/t/t1050-large.sh b/t/t1050-large.sh
index 7d40d08..d295c26 100755
--- a/t/t1050-large.sh
+++ b/t/t1050-large.sh
@@ -8,7 +8,7 @@
 test_expect_success 'core.bigFileThreshold must be non-negative' '
 	: >input &&
 	test_must_fail git -c core.bigFileThreshold=-1 hash-object input >out 2>err &&
-	grep "bad numeric config value" err &&
+	test_grep "bad numeric config value" err &&
 	test_must_be_empty out
 '
 
@@ -148,12 +148,12 @@
 
 test_expect_success 'diff' '
 	git diff HEAD^ HEAD >actual &&
-	grep "Binary files.*differ" actual
+	test_grep "Binary files.*differ" actual
 '
 
 test_expect_success 'diff --cached' '
 	git diff --cached HEAD^ >actual &&
-	grep "Binary files.*differ" actual
+	test_grep "Binary files.*differ" actual
 '
 
 test_expect_success 'hash-object' '
diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh
index cd0aed9..74b1761 100755
--- a/t/t1091-sparse-checkout-builtin.sh
+++ b/t/t1091-sparse-checkout-builtin.sh
@@ -129,7 +129,7 @@
 		git sparse-checkout add dir &&
 		git config --worktree core.sparseCheckoutCone true &&
 		test_must_fail git sparse-checkout add dir 2>err &&
-		grep "existing sparse-checkout patterns do not use cone mode" err
+		test_grep "existing sparse-checkout patterns do not use cone mode" err
 	)
 '
 
@@ -803,7 +803,7 @@
 	# When an untracked file is in the way, all untracked files
 	# (even ignored files) are preserved.
 	git -C repo sparse-checkout set folder1 2>err &&
-	grep "contains untracked files" err &&
+	test_grep "contains untracked files" err &&
 	test_path_is_file repo/deep/deeper2/ignored.o &&
 	test_path_is_file repo/deep/deeper2/untracked &&
 
@@ -882,8 +882,8 @@
 	# of using the cone-mode translation to a set of directories.
 	git -C repo sparse-checkout list >actual 2>err &&
 	test_cmp repo/.git/info/sparse-checkout actual &&
-	grep "warning: your sparse-checkout file may have issues: pattern .* is repeated" err &&
-	grep "warning: disabling cone pattern matching" err
+	test_grep "warning: your sparse-checkout file may have issues: pattern .* is repeated" err &&
+	test_grep "warning: disabling cone pattern matching" err
 '
 
 test_expect_success 'set from subdir pays attention to prefix' '
@@ -917,34 +917,34 @@
 	git -C repo sparse-checkout disable &&
 	test_must_fail git -C repo/deep sparse-checkout set --no-cone deeper2 ../folder1 2>error &&
 
-	grep "run from the toplevel directory in non-cone mode" error
+	test_grep "run from the toplevel directory in non-cone mode" error
 '
 
 test_expect_success 'set from subdir in non-cone mode throws an error' '
 	git -C repo sparse-checkout set --no-cone deep/deeper2 &&
 	test_must_fail git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 2>error &&
 
-	grep "run from the toplevel directory in non-cone mode" error
+	test_grep "run from the toplevel directory in non-cone mode" error
 '
 
 test_expect_success 'by default, cone mode will error out when passed files' '
 	git -C repo sparse-checkout reapply --cone &&
 	test_must_fail git -C repo sparse-checkout add .gitignore 2>error &&
 
-	grep ".gitignore.*is not a directory" error
+	test_grep ".gitignore.*is not a directory" error
 '
 
 test_expect_success 'error on mistyped command line options' '
 	test_must_fail git -C repo sparse-checkout add --sikp-checks .gitignore 2>error &&
 
-	grep "unknown option.*sikp-checks" error
+	test_grep "unknown option.*sikp-checks" error
 '
 
 test_expect_success 'by default, non-cone mode will warn on individual files' '
 	git -C repo sparse-checkout reapply --no-cone &&
 	git -C repo sparse-checkout add .gitignore 2>warning &&
 
-	grep "pass a leading slash before paths.*if you want a single file" warning
+	test_grep "pass a leading slash before paths.*if you want a single file" warning
 '
 
 test_expect_success 'setup bare repo' '
@@ -1108,11 +1108,11 @@
 	touch repo/folder1/extra/inside/file &&
 
 	test_must_fail git -C repo sparse-checkout clean 2>err &&
-	grep "refusing to clean" err &&
+	test_grep "refusing to clean" err &&
 
 	git -C repo config clean.requireForce true &&
 	test_must_fail git -C repo sparse-checkout clean 2>err &&
-	grep "refusing to clean" err &&
+	test_grep "refusing to clean" err &&
 
 	cat >expect <<-\EOF &&
 	Would remove deep/deeper2/
@@ -1255,7 +1255,7 @@
 		test_must_fail git merge -m "will-conflict" right &&
 
 		test_must_fail git sparse-checkout clean -f 2>err &&
-		grep "failed to convert index to a sparse index" err &&
+		test_grep "failed to convert index to a sparse index" err &&
 
 		echo merged >folder1/even/more/dirs/file &&
 		git add --sparse folder1 &&
diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index 8186da5..4140c4d 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -454,10 +454,10 @@
 	run_on_sparse ../edit-contents folder1/a &&
 	run_on_sparse ../edit-contents folder1/newfile &&
 	test_sparse_match test_must_fail git add folder1/a &&
-	grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+	test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 	test_sparse_unstaged folder1/a &&
 	test_sparse_match test_must_fail git add folder1/newfile &&
-	grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+	test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 	test_sparse_unstaged folder1/newfile
 '
 
@@ -509,13 +509,13 @@
 
 	# Adding the path outside of the sparse-checkout cone should fail.
 	test_sparse_match test_must_fail git add folder1/a &&
-	grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+	test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 	test_sparse_unstaged folder1/a &&
 	test_all_match git add --refresh folder1/a &&
 	test_must_be_empty sparse-checkout-err &&
 	test_sparse_unstaged folder1/a &&
 	test_sparse_match test_must_fail git add folder1/new &&
-	grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+	test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 	test_sparse_unstaged folder1/new &&
 	test_sparse_match git add --sparse folder1/a &&
 	test_sparse_match git add --sparse folder1/new &&
@@ -661,8 +661,8 @@
 	# in sparse-checkout or sparse-index.
 	git -C full-checkout reset update-folder1 >full-checkout-out &&
 	test_sparse_match git reset update-folder1 &&
-	grep "M	folder1/a" full-checkout-out &&
-	! grep "M	folder1/a" sparse-checkout-out &&
+	test_grep "M	folder1/a" full-checkout-out &&
+	test_grep ! "M	folder1/a" sparse-checkout-out &&
 	run_on_sparse test_path_is_missing folder1
 '
 
@@ -880,8 +880,8 @@
 	# update-index will exit silently when provided with a directory name
 	# containing a trailing slash
 	test_all_match git update-index deep/ folder1/ &&
-	grep "Ignoring path deep/" sparse-checkout-err &&
-	grep "Ignoring path folder1/" sparse-checkout-err &&
+	test_grep "Ignoring path deep/" sparse-checkout-err &&
+	test_grep "Ignoring path folder1/" sparse-checkout-err &&
 
 	# When update-index is given a directory name WITHOUT a trailing slash, it will
 	# behave in different ways depending on the status of the directory on disk:
@@ -1067,7 +1067,7 @@
 
 	# 2. Add the file with conflict markers
 	test_sparse_match test_must_fail git add folder1/a &&
-	grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+	test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 	test_sparse_unstaged folder1/a &&
 	test_all_match git add --sparse folder1/a &&
 	test_all_match git status --porcelain=v2 &&
@@ -1076,7 +1076,7 @@
 	#    accept conflict markers as resolved content.
 	run_on_all mv folder2/a folder2/z &&
 	test_sparse_match test_must_fail git add folder2 &&
-	grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+	test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 	test_sparse_unstaged folder2/z &&
 	test_all_match git add --sparse folder2 &&
 	test_all_match git status --porcelain=v2 &&
@@ -1107,7 +1107,7 @@
 		# SKIP_WORKTREE bit from the index entry for folder1/a, we should
 		# warn that this is a problematic add.
 		test_sparse_match test_must_fail git add folder1/a &&
-		grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+		test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 		test_sparse_unstaged folder1/a &&
 		test_all_match git add --sparse folder1/a &&
 		test_all_match git status --porcelain=v2 &&
@@ -1119,7 +1119,7 @@
 		# existing index entry with the SKIP_WORKTREE bit cleared.
 		run_on_all mv folder2/a folder2/z &&
 		test_sparse_match test_must_fail git add folder2 &&
-		grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+		test_grep "Disable or modify the sparsity rules" sparse-checkout-err &&
 		test_sparse_unstaged folder2/z &&
 		test_all_match git add --sparse folder2 &&
 		test_all_match git status --porcelain=v2 &&
@@ -1266,7 +1266,7 @@
 	run_on_all test_must_fail git checkout-index -f -- folder1/ &&
 	test_cmp full-checkout-err sparse-checkout-err &&
 	! test_cmp full-checkout-err sparse-index-err &&
-	grep "is a sparse directory" sparse-index-err
+	test_grep "is a sparse directory" sparse-index-err
 '
 
 test_expect_success 'checkout-index --all' '
@@ -1374,8 +1374,8 @@
 	# having a submodule prevents "modules" from collapse
 	test_sparse_match git sparse-checkout set deep/deeper1 &&
 	git -C sparse-index ls-files --sparse --stage >cache &&
-	grep "100644 .*	modules/a" cache &&
-	grep "160000 $(git -C initial-repo rev-parse HEAD) 0	modules/sub" cache
+	test_grep "100644 .*	modules/a" cache &&
+	test_grep "160000 $(git -C initial-repo rev-parse HEAD) 0	modules/sub" cache
 '
 
 test_expect_success 'git apply functionality' '
@@ -1392,7 +1392,7 @@
 
 	# Apply a patch to a file outside the sparse definition
 	test_sparse_match test_must_fail git apply ../patch-outside &&
-	grep "No such file or directory" sparse-checkout-err &&
+	test_grep "No such file or directory" sparse-checkout-err &&
 
 	# But it works with --index and --cached
 	test_all_match git apply --index --stat ../patch-outside &&
@@ -1598,6 +1598,61 @@
 	ensure_not_expanded stash pop
 '
 
+test_expect_success 'sparse-index is not expanded: stash in-cone pathspec' '
+	init_repos &&
+
+	echo unrelated >>sparse-index/deep/e &&
+	echo literal >>sparse-index/deep/a &&
+	ensure_not_expanded stash push -- deep/a &&
+	test_grep ! literal sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep literal sparse-index/deep/a &&
+
+	echo prefixed >>sparse-index/deep/a &&
+	ensure_not_expanded -C deep stash push -- a &&
+	test_grep ! prefixed sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep prefixed sparse-index/deep/a &&
+
+	echo wildcard >>sparse-index/deep/a &&
+	ensure_not_expanded stash push -- "deep/a*" &&
+	test_grep ! wildcard sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep wildcard sparse-index/deep/a &&
+
+	echo pathspec-file >>sparse-index/deep/a &&
+	echo deep/a >pathspec-file &&
+	ensure_not_expanded stash push --pathspec-from-file=../pathspec-file &&
+	test_grep ! pathspec-file sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep pathspec-file sparse-index/deep/a &&
+
+	echo multiple-a >>sparse-index/deep/a &&
+	echo multiple-e >>sparse-index/deep/e &&
+	ensure_not_expanded stash push -- deep/a deep/e &&
+	test_grep ! multiple-a sparse-index/deep/a &&
+	test_grep ! multiple-e sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep multiple-a sparse-index/deep/a &&
+	test_grep multiple-e sparse-index/deep/e &&
+
+	echo staged >>sparse-index/deep/a &&
+	git -C sparse-index add deep/a &&
+	ensure_not_expanded stash push --staged -- deep/a &&
+	test_grep ! staged sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop --index &&
+	test_grep staged sparse-index/deep/a &&
+	test_must_fail git -C sparse-index diff --cached --quiet -- deep/a &&
+
+	ensure_not_expanded ! stash push -- deep/does-not-exist &&
+	test_grep "did not match any file" sparse-index-error
+'
+
 test_expect_success 'describe tested on all' '
 	init_repos &&
 
@@ -2013,9 +2068,9 @@
 	test_all_match git status --porcelain=v2 &&
 	test_sparse_match git ls-files -t &&
 	git -C sparse-checkout ls-files -t >actual &&
-	grep -e "H deep/folder1/0/0/0" actual &&
-	grep -e "H deep/folder1/0/1" actual &&
-	grep -e "H deep/folder1/a" actual &&
+	test_grep -e "H deep/folder1/0/0/0" actual &&
+	test_grep -e "H deep/folder1/0/1" actual &&
+	test_grep -e "H deep/folder1/a" actual &&
 
 	test_all_match git reset --hard &&
 
@@ -2025,8 +2080,8 @@
 	test_sparse_match git status --porcelain=v2 &&
 	test_sparse_match git ls-files -t &&
 	git -C sparse-checkout ls-files -t >actual &&
-	grep -e "H deep/0/0/0" actual &&
-	grep -e "H deep/0/1" actual
+	test_grep -e "H deep/0/0/0" actual &&
+	test_grep -e "H deep/0/1" actual
 '
 
 test_expect_success 'rm pathspec inside sparse definition' '
@@ -2119,6 +2174,13 @@
 	ensure_not_expanded rm -r deep
 '
 
+test_expect_success 'sparse index is not expanded: prefixed wildcard pathspec' '
+	init_repos &&
+
+	ensure_not_expanded -C deep rm --dry-run -- "a*" &&
+	ensure_not_expanded -C deep reset base -- "a*"
+'
+
 test_expect_success 'grep with and --cached' '
 	init_repos &&
 
@@ -2517,7 +2579,7 @@
 	mkdir -p sparse-index/deep/deeper2/deepest &&
 	touch sparse-index/deep/deeper2/deepest/bogus &&
 	git -C sparse-index status 2>err &&
-	grep "The sparse index is expanding to a full index" err &&
+	test_grep "The sparse index is expanding to a full index" err &&
 
 	git -C sparse-index sparse-checkout disable 2>err &&
 	test_line_count = 0 err
diff --git a/t/t1100-commit-tree-options.sh b/t/t1100-commit-tree-options.sh
index ae66ba5..3e88890 100755
--- a/t/t1100-commit-tree-options.sh
+++ b/t/t1100-commit-tree-options.sh
@@ -14,37 +14,35 @@
 
 . ./test-lib.sh
 
-cat >expected <<EOF
-tree $EMPTY_TREE
-author Author Name <author@email> 1117148400 +0000
-committer Committer Name <committer@email> 1117150200 +0000
+test_expect_success 'test preparation: write empty tree' '
+	cat >expected <<-EOF &&
+	tree $EMPTY_TREE
+	author Author Name <author@email> 1117148400 +0000
+	committer Committer Name <committer@email> 1117150200 +0000
 
-comment text
-EOF
+	comment text
+	EOF
+	git write-tree >treeid
+'
 
-test_expect_success \
-    'test preparation: write empty tree' \
-    'git write-tree >treeid'
+test_expect_success 'construct commit' '
+	echo comment text |
+	GIT_AUTHOR_NAME="Author Name" \
+	GIT_AUTHOR_EMAIL="author@email" \
+	GIT_AUTHOR_DATE="2005-05-26 23:00" \
+	GIT_COMMITTER_NAME="Committer Name" \
+	GIT_COMMITTER_EMAIL="committer@email" \
+	GIT_COMMITTER_DATE="2005-05-26 23:30" \
+	TZ=GMT git commit-tree $(cat treeid) >commitid 2>/dev/null
+'
 
-test_expect_success \
-    'construct commit' \
-    'echo comment text |
-     GIT_AUTHOR_NAME="Author Name" \
-     GIT_AUTHOR_EMAIL="author@email" \
-     GIT_AUTHOR_DATE="2005-05-26 23:00" \
-     GIT_COMMITTER_NAME="Committer Name" \
-     GIT_COMMITTER_EMAIL="committer@email" \
-     GIT_COMMITTER_DATE="2005-05-26 23:30" \
-     TZ=GMT git commit-tree $(cat treeid) >commitid 2>/dev/null'
+test_expect_success 'read commit' '
+	git cat-file commit $(cat commitid) >commit
+'
 
-test_expect_success \
-    'read commit' \
-    'git cat-file commit $(cat commitid) >commit'
-
-test_expect_success \
-    'compare commit' \
-    'test_cmp expected commit'
-
+test_expect_success 'compare commit' '
+	test_cmp expected commit
+'
 
 test_expect_success 'flags and then non flags' '
 	test_tick &&
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 87ca11a..e3f8064 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -856,7 +856,7 @@
 		printf "[a] g = h\\n"
 	} >y &&
 	test_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&
-	grep "refusing to work with overly long line in .y. on line 2" err
+	test_grep "refusing to work with overly long line in .y. on line 2" err
 '
 
 cat >> .git/config << EOF
@@ -1671,9 +1671,9 @@
 
 test_expect_success 'git --config-env with missing value' '
 	test_must_fail env ENVVAR=value git --config-env 2>error &&
-	grep "no config key given for --config-env" error &&
+	test_grep "no config key given for --config-env" error &&
 	test_must_fail env ENVVAR=value git --config-env config core.name 2>error &&
-	grep "invalid config format: config" error
+	test_grep "invalid config format: config" error
 '
 
 test_expect_success 'git --config-env fails with invalid parameters' '
@@ -2104,7 +2104,7 @@
 	key = true
 	EOF
 	git config ${mode_unset} two.key &&
-	! grep two .git/config &&
+	test_grep ! two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
 	[one]
@@ -2124,7 +2124,7 @@
 	Qkey = true
 	EOF
 	git config ${mode_unset} two.key &&
-	grep two .git/config &&
+	test_grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
 	[one]
@@ -2655,7 +2655,7 @@
 
 test_expect_success '--type=int requires at least one digit' '
 	test_must_fail git config --type int --default m some.key >out 2>error &&
-	grep "bad numeric config value" error &&
+	test_grep "bad numeric config value" error &&
 	test_must_be_empty out
 '
 
@@ -2967,12 +2967,12 @@
 
 	# test with any Git command
 	test_must_fail git -C hasremoteurlTest status 2>err &&
-	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
+	test_grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
 '
 
 test_expect_success 'negated mode causes failure' '
 	test_must_fail git config --no-get 2>err &&
-	grep "unknown option \`no-get${SQ}" err
+	test_grep "unknown option \`no-get${SQ}" err
 '
 
 test_expect_success 'specifying multiple modes causes failure' '
@@ -3002,4 +3002,21 @@
 	test_cmp expect actual
 '
 
+test_expect_success 'writing config fails immediately with core.configLockTimeout=0' '
+	test_when_finished "rm -f .git/config.lock" &&
+	>.git/config.lock &&
+	test_must_fail git -c core.configLockTimeout=0 config foo.bar baz 2>err &&
+	test_grep "could not lock config file" err
+'
+
+test_expect_success 'writing config retries until lock is released' '
+	test_when_finished "rm -f .git/config.lock" &&
+	>.git/config.lock &&
+	{
+		( sleep 1 && rm -f .git/config.lock ) &
+	} &&
+	git -c core.configLockTimeout=5000 config retried.key value &&
+	test "$(git config retried.key)" = value
+'
+
 test_done
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
index f389257..507b8eb 100755
--- a/t/t1305-config-include.sh
+++ b/t/t1305-config-include.sh
@@ -353,7 +353,7 @@
 	git -C cycle --git-dir=. config include.path cycle &&
 	git config -f cycle/cycle include.path config &&
 	test_must_fail git -C cycle --git-dir=. config --get-all test.value 2>stderr &&
-	grep "exceeded maximum include depth" stderr
+	test_grep "exceeded maximum include depth" stderr
 '
 
 test_expect_success 'onbranch with unborn branch' '
@@ -396,4 +396,132 @@
 	test_must_fail nongit git --git-dir=nonexistent config get foo.bar
 '
 
+# worktree: conditional include tests
+
+test_expect_success 'conditional include, worktree bare repo' '
+	git init --bare wt-bare &&
+	(
+		cd wt-bare &&
+		echo "[includeIf \"worktree:/\"]path=bar-bare" >>config &&
+		echo "[test]wtbare=1" >bar-bare &&
+		test_must_fail git config test.wtbare
+	)
+'
+
+test_expect_success 'conditional include, worktree multiple worktrees' '
+	git init wt-multi &&
+	(
+		cd wt-multi &&
+		test_commit initial &&
+		git worktree add -b linked-branch ../wt-linked HEAD &&
+		git worktree add -b prefix-branch ../wt-prefix/linked HEAD
+	) &&
+	wt_main="$(cd wt-multi && pwd)" &&
+	wt_linked="$(cd wt-linked && pwd)" &&
+	wt_prefix_parent="$(cd wt-prefix && pwd)" &&
+	cat >>wt-multi/.git/config <<-EOF &&
+	[includeIf "worktree:$wt_main"]
+		path = main-config
+	[includeIf "worktree:$wt_linked"]
+		path = linked-config
+	[includeIf "worktree:$wt_prefix_parent/"]
+		path = prefix-config
+	EOF
+	echo "[test]mainvar=main" >wt-multi/.git/main-config &&
+	echo "[test]linkedvar=linked" >wt-multi/.git/linked-config &&
+	echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config &&
+	echo main >expect &&
+	git -C wt-multi config test.mainvar >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C wt-multi config test.linkedvar &&
+	test_must_fail git -C wt-multi config test.prefixvar &&
+	echo linked >expect &&
+	git -C wt-linked config test.linkedvar >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C wt-linked config test.mainvar &&
+	test_must_fail git -C wt-linked config test.prefixvar &&
+	echo prefix >expect &&
+	git -C wt-prefix/linked config test.prefixvar >actual &&
+	test_cmp expect actual &&
+	test_must_fail git -C wt-prefix/linked config test.mainvar &&
+	test_must_fail git -C wt-prefix/linked config test.linkedvar
+'
+
+test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' '
+	mkdir real-wt &&
+	ln -s real-wt link-wt &&
+	git init link-wt/repo &&
+	(
+		cd link-wt/repo &&
+		# repo->worktree resolves symlinks, so use real path in pattern
+		echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config &&
+		echo "[test]wtlink=2" >.git/bar-link &&
+		echo 2 >expect &&
+		git config test.wtlink >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' '
+	git init wt-case &&
+	(
+		cd wt-case &&
+		test_commit initial &&
+		wt_path="$(pwd)" &&
+		wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+		echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config &&
+		echo "[test]wtcase=1" >.git/case-inc &&
+		test_must_fail git config test.wtcase
+	)
+'
+
+test_expect_success 'conditional include, worktree, icase' '
+	git init wt-icase &&
+	(
+		cd wt-icase &&
+		test_commit initial &&
+		wt_path="$(pwd)" &&
+		wt_upper=$(echo "$wt_path" | tr a-z A-Z) &&
+		echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config &&
+		echo "[test]wticase=1" >.git/icase-inc &&
+		echo 1 >expect &&
+		git config test.wticase >actual &&
+		test_cmp expect actual
+	)
+'
+
+# The "worktree" condition cannot match during early config reading
+# because the repository object is not yet fully initialized and
+# repo_get_work_tree() returns NULL.
+test_expect_success 'conditional include, worktree does not match in early config' '
+	git init wt-early &&
+	(
+		cd wt-early &&
+		test_commit initial &&
+		wt_path="$(pwd)" &&
+		echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config &&
+		echo "[test]wtearly=1" >.git/early-inc &&
+		test-tool config read_early_config test.wtearly >actual &&
+		test_must_be_empty actual
+	)
+'
+
+# Use a loose pattern so the "present in non-worktree cases" check works
+# for Unix-style absolute paths and Windows paths like D:/a/git/...
+test_expect_success 'conditional include, worktree without repository' '
+	test_when_finished "rm -f .gitconfig config.inc" &&
+	git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+	git config set -f config.inc foo.bar baz &&
+	git config get foo.bar &&
+	test_must_fail nongit git config get foo.bar
+'
+
+test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' '
+	test_when_finished "rm -f .gitconfig config.inc" &&
+	git config set -f .gitconfig "includeIf.worktree:**.path" config.inc &&
+	git config set -f config.inc foo.bar baz &&
+	git config get foo.bar &&
+	test_must_fail nongit git --git-dir=nonexistent config get foo.bar
+'
+
 test_done
diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index e0e4905..de95161 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -180,7 +180,7 @@
 
 test_expect_success 'non parse-able integer value during iteration' '
 	check_config expect_code 128 git_config_int lamb.head 2>result &&
-	grep "fatal: bad numeric config value .* in file \.git/config" result
+	test_grep "fatal: bad numeric config value .* in file \.git/config" result
 '
 
 test_expect_success 'find bool value for the entered key' '
@@ -302,7 +302,7 @@
 	echo "Error (-1) reading configuration file a-directory." >expect &&
 	mkdir a-directory &&
 	test_expect_code 2 test-tool config configset_get_value foo.bar a-directory 2>output &&
-	grep "^warning:" output &&
+	test_grep "^warning:" output &&
 	grep "^Error" output >actual &&
 	test_cmp expect actual
 '
@@ -312,7 +312,7 @@
 	test_when_finished "chmod +r .git/config" &&
 	echo "Error (-1) reading configuration file .git/config." >expect &&
 	test_expect_code 2 test-tool config configset_get_value foo.bar .git/config 2>output &&
-	grep "^warning:" output &&
+	test_grep "^warning:" output &&
 	grep "^Error" output >actual &&
 	test_cmp expect actual
 '
diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh
index c7c814d..269fdaa 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -92,7 +92,7 @@
 	git update-ref -m delete-$m -d $m &&
 	test_must_fail git show-ref --verify -q $m &&
 	test-tool ref-store main for-each-reflog-ent HEAD >actual &&
-	grep "delete-$m$" actual
+	test_grep "delete-$m$" actual
 '
 
 test_expect_success "deleting by HEAD adds message to HEAD's log" '
@@ -102,7 +102,7 @@
 	git update-ref -m delete-by-head -d HEAD &&
 	test_must_fail git show-ref --verify -q $m &&
 	test-tool ref-store main for-each-reflog-ent HEAD >actual &&
-	grep "delete-by-head$" actual
+	test_grep "delete-by-head$" actual
 '
 
 test_expect_success 'update-ref does not create reflogs by default' '
@@ -204,7 +204,10 @@
 test_expect_success "delete $m (by HEAD) should remove both packed and loose $m" '
 	test_when_finished "git update-ref -d $m" &&
 	git update-ref -d HEAD $B &&
-	! grep "$m" .git/packed-refs &&
+	if test_have_prereq REFFILES
+	then
+		test_grep ! "$m" .git/packed-refs
+	fi &&
 	test_must_fail git show-ref --verify -q $m
 '
 
@@ -587,103 +590,103 @@
 test_expect_success 'stdin fails on empty line' '
 	echo "" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: empty command in input" err
+	test_grep "fatal: empty command in input" err
 '
 
 test_expect_success 'stdin fails on only whitespace' '
 	echo " " >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: whitespace before command:  " err
+	test_grep "fatal: whitespace before command:  " err
 '
 
 test_expect_success 'stdin fails on leading whitespace' '
 	echo " create $a $m" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: whitespace before command:  create $a $m" err
+	test_grep "fatal: whitespace before command:  create $a $m" err
 '
 
 test_expect_success 'stdin fails on unknown command' '
 	echo "unknown $a" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: unknown command: unknown $a" err
+	test_grep "fatal: unknown command: unknown $a" err
 '
 
 test_expect_success 'stdin fails on unbalanced quotes' '
 	echo "create $a \"main" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: badly quoted argument: \\\"main" err
+	test_grep "fatal: badly quoted argument: \\\"main" err
 '
 
 test_expect_success 'stdin fails on invalid escape' '
 	echo "create $a \"ma\zn\"" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: badly quoted argument: \\\"ma\\\\zn\\\"" err
+	test_grep "fatal: badly quoted argument: \\\"ma\\\\zn\\\"" err
 '
 
 test_expect_success 'stdin fails on junk after quoted argument' '
 	echo "create \"$a\"main" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: unexpected character after quoted argument: \\\"$a\\\"main" err
+	test_grep "fatal: unexpected character after quoted argument: \\\"$a\\\"main" err
 '
 
 test_expect_success 'stdin fails create with no ref' '
 	echo "create " >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: create: missing <ref>" err
+	test_grep "fatal: create: missing <ref>" err
 '
 
 test_expect_success 'stdin fails create with no new value' '
 	echo "create $a" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: create $a: missing <new-oid>" err
+	test_grep "fatal: create $a: missing <new-oid>" err
 '
 
 test_expect_success 'stdin fails create with too many arguments' '
 	echo "create $a $m $m" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: create $a: extra input:  $m" err
+	test_grep "fatal: create $a: extra input:  $m" err
 '
 
 test_expect_success 'stdin fails update with no ref' '
 	echo "update " >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: update: missing <ref>" err
+	test_grep "fatal: update: missing <ref>" err
 '
 
 test_expect_success 'stdin fails update with no new value' '
 	echo "update $a" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: update $a: missing <new-oid>" err
+	test_grep "fatal: update $a: missing <new-oid>" err
 '
 
 test_expect_success 'stdin fails update with too many arguments' '
 	echo "update $a $m $m $m" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: update $a: extra input:  $m" err
+	test_grep "fatal: update $a: extra input:  $m" err
 '
 
 test_expect_success 'stdin fails delete with no ref' '
 	echo "delete " >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: delete: missing <ref>" err
+	test_grep "fatal: delete: missing <ref>" err
 '
 
 test_expect_success 'stdin fails delete with too many arguments' '
 	echo "delete $a $m $m" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: delete $a: extra input:  $m" err
+	test_grep "fatal: delete $a: extra input:  $m" err
 '
 
 test_expect_success 'stdin fails verify with too many arguments' '
 	echo "verify $a $m $m" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: verify $a: extra input:  $m" err
+	test_grep "fatal: verify $a: extra input:  $m" err
 '
 
 test_expect_success 'stdin fails option with unknown name' '
 	echo "option unknown" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: option unknown: unknown" err
+	test_grep "fatal: option unknown: unknown" err
 '
 
 test_expect_success 'stdin fails with duplicate refs' '
@@ -771,28 +774,28 @@
 test_expect_success 'stdin update ref fails with wrong old value' '
 	echo "update $c $m $m~1" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin update ref fails with bad old value' '
 	echo "update $c $m does-not-exist" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: update $c: invalid <old-oid>: does-not-exist" err &&
+	test_grep "fatal: update $c: invalid <old-oid>: does-not-exist" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin create ref fails with bad new value' '
 	echo "create $c does-not-exist" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: create $c: invalid <new-oid>: does-not-exist" err &&
+	test_grep "fatal: create $c: invalid <new-oid>: does-not-exist" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin create ref fails with zero new value' '
 	echo "create $c " >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: create $c: zero <new-oid>" err &&
+	test_grep "fatal: create $c: zero <new-oid>" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
@@ -807,7 +810,7 @@
 test_expect_success 'stdin delete ref fails with wrong old value' '
 	echo "delete $a $m~1" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$a'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$a'"'"'" err &&
 	git rev-parse $m >expect &&
 	git rev-parse $a >actual &&
 	test_cmp expect actual
@@ -816,7 +819,7 @@
 test_expect_success 'stdin delete ref fails with zero old value' '
 	echo "delete $a " >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: delete $a: zero <old-oid>" err &&
+	test_grep "fatal: delete $a: zero <old-oid>" err &&
 	git rev-parse $m >expect &&
 	git rev-parse $a >actual &&
 	test_cmp expect actual
@@ -977,7 +980,7 @@
 	update $c  ''
 	EOF
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
 	git rev-parse $m >expect &&
 	git rev-parse $a >actual &&
 	test_cmp expect actual &&
@@ -1010,123 +1013,123 @@
 test_expect_success 'stdin -z fails on empty line' '
 	echo "" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: whitespace before command: " err
+	test_grep "fatal: whitespace before command: " err
 '
 
 test_expect_success 'stdin -z fails on empty command' '
 	printf $F "" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: empty command in input" err
+	test_grep "fatal: empty command in input" err
 '
 
 test_expect_success 'stdin -z fails on only whitespace' '
 	printf $F " " >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: whitespace before command:  " err
+	test_grep "fatal: whitespace before command:  " err
 '
 
 test_expect_success 'stdin -z fails on leading whitespace' '
 	printf $F " create $a" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: whitespace before command:  create $a" err
+	test_grep "fatal: whitespace before command:  create $a" err
 '
 
 test_expect_success 'stdin -z fails on unknown command' '
 	printf $F "unknown $a" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: unknown command: unknown $a" err
+	test_grep "fatal: unknown command: unknown $a" err
 '
 
 test_expect_success 'stdin -z fails create with no ref' '
 	printf $F "create " >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: create: missing <ref>" err
+	test_grep "fatal: create: missing <ref>" err
 '
 
 test_expect_success 'stdin -z fails create with no new value' '
 	printf $F "create $a" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: create $a: unexpected end of input when reading <new-oid>" err
+	test_grep "fatal: create $a: unexpected end of input when reading <new-oid>" err
 '
 
 test_expect_success 'stdin -z fails create with too many arguments' '
 	printf $F "create $a" "$m" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: unknown command: $m" err
+	test_grep "fatal: unknown command: $m" err
 '
 
 test_expect_success 'stdin -z fails update with no ref' '
 	printf $F "update " >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: update: missing <ref>" err
+	test_grep "fatal: update: missing <ref>" err
 '
 
 test_expect_success 'stdin -z fails update with too few args' '
 	printf $F "update $a" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: update $a: unexpected end of input when reading <old-oid>" err
+	test_grep "fatal: update $a: unexpected end of input when reading <old-oid>" err
 '
 
 test_expect_success 'stdin -z emits warning with empty new value' '
 	git update-ref $a $m &&
 	printf $F "update $a" "" "" >stdin &&
 	git update-ref -z --stdin <stdin 2>err &&
-	grep "warning: update $a: missing <new-oid>, treating as zero" err &&
+	test_grep "warning: update $a: missing <new-oid>, treating as zero" err &&
 	test_must_fail git rev-parse --verify -q $a
 '
 
 test_expect_success 'stdin -z fails update with no new value' '
 	printf $F "update $a" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: update $a: unexpected end of input when reading <new-oid>" err
+	test_grep "fatal: update $a: unexpected end of input when reading <new-oid>" err
 '
 
 test_expect_success 'stdin -z fails update with no old value' '
 	printf $F "update $a" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: update $a: unexpected end of input when reading <old-oid>" err
+	test_grep "fatal: update $a: unexpected end of input when reading <old-oid>" err
 '
 
 test_expect_success 'stdin -z fails update with too many arguments' '
 	printf $F "update $a" "$m" "$m" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: unknown command: $m" err
+	test_grep "fatal: unknown command: $m" err
 '
 
 test_expect_success 'stdin -z fails delete with no ref' '
 	printf $F "delete " >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: delete: missing <ref>" err
+	test_grep "fatal: delete: missing <ref>" err
 '
 
 test_expect_success 'stdin -z fails delete with no old value' '
 	printf $F "delete $a" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: delete $a: unexpected end of input when reading <old-oid>" err
+	test_grep "fatal: delete $a: unexpected end of input when reading <old-oid>" err
 '
 
 test_expect_success 'stdin -z fails delete with too many arguments' '
 	printf $F "delete $a" "$m" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: unknown command: $m" err
+	test_grep "fatal: unknown command: $m" err
 '
 
 test_expect_success 'stdin -z fails verify with too many arguments' '
 	printf $F "verify $a" "$m" "$m" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: unknown command: $m" err
+	test_grep "fatal: unknown command: $m" err
 '
 
 test_expect_success 'stdin -z fails verify with no old value' '
 	printf $F "verify $a" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: verify $a: unexpected end of input when reading <old-oid>" err
+	test_grep "fatal: verify $a: unexpected end of input when reading <old-oid>" err
 '
 
 test_expect_success 'stdin -z fails option with unknown name' '
 	printf $F "option unknown" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: option unknown: unknown" err
+	test_grep "fatal: option unknown: unknown" err
 '
 
 test_expect_success 'stdin -z fails with duplicate refs' '
@@ -1172,14 +1175,14 @@
 test_expect_success 'stdin -z update ref fails with wrong old value' '
 	printf $F "update $c" "$m" "$m~1" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin -z update ref fails with bad old value' '
 	printf $F "update $c" "$m" "does-not-exist" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: update $c: invalid <old-oid>: does-not-exist" err &&
+	test_grep "fatal: update $c: invalid <old-oid>: does-not-exist" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
@@ -1188,7 +1191,7 @@
 	git rev-parse "$c" >expect &&
 	printf $F "create $c" "$m~1" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
 	git rev-parse "$c" >actual &&
 	test_cmp expect actual
 '
@@ -1197,28 +1200,28 @@
 	git update-ref -d "$c" &&
 	printf $F "create $c" "does-not-exist" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: create $c: invalid <new-oid>: does-not-exist" err &&
+	test_grep "fatal: create $c: invalid <new-oid>: does-not-exist" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin -z create ref fails with empty new value' '
 	printf $F "create $c" "" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: create $c: missing <new-oid>" err &&
+	test_grep "fatal: create $c: missing <new-oid>" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin -z create ref fails with non commit object' '
 	printf $F "create $c" "$(test_oid 001)" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object" err &&
+	test_grep "fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
 test_expect_success 'stdin -z update ref fails with non commit object' '
 	printf $F "update $b" "$(test_oid 001)" "" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object" err &&
+	test_grep "fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object" err &&
 	test_must_fail git rev-parse --verify -q $c
 '
 
@@ -1233,7 +1236,7 @@
 test_expect_success 'stdin -z delete ref fails with wrong old value' '
 	printf $F "delete $a" "$m~1" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$a'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$a'"'"'" err &&
 	git rev-parse $m >expect &&
 	git rev-parse $a >actual &&
 	test_cmp expect actual
@@ -1242,7 +1245,7 @@
 test_expect_success 'stdin -z delete ref fails with zero old value' '
 	printf $F "delete $a" "$Z" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: delete $a: zero <old-oid>" err &&
+	test_grep "fatal: delete $a: zero <old-oid>" err &&
 	git rev-parse $m >expect &&
 	git rev-parse $a >actual &&
 	test_cmp expect actual
@@ -1348,7 +1351,7 @@
 	git update-ref $c $m &&
 	printf $F "update $a" "$m" "$m" "update $b" "$m" "$m" "update $c" "$m" "$Z" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
+	test_grep "fatal: cannot lock ref '"'"'$c'"'"'" err &&
 	git rev-parse $m >expect &&
 	git rev-parse $a >actual &&
 	test_cmp expect actual &&
@@ -1427,13 +1430,13 @@
 		cd worktree &&
 		git commit --allow-empty -m "test commit"  &&
 		git for-each-ref >for-each-ref.out &&
-		! grep refs/bisect for-each-ref.out &&
+		test_grep ! refs/bisect for-each-ref.out &&
 		git update-ref refs/bisect/something HEAD &&
 		git rev-parse refs/bisect/something >../worktree-head &&
 		git for-each-ref | grep refs/bisect/something
 	) &&
 	git show-ref >actual &&
-	! grep 'refs/bisect' actual &&
+	test_grep ! 'refs/bisect' actual &&
 	test_must_fail git rev-parse refs/bisect/something &&
 	git update-ref refs/bisect/something HEAD &&
 	git rev-parse refs/bisect/something >main-head &&
@@ -1489,7 +1492,7 @@
 	test_must_fail git update-ref --stdin <stdin >actual 2>err &&
 	printf "%s: ok\n" abort >expect &&
 	test_cmp expect actual &&
-	grep "fatal: transaction is closed" err
+	test_grep "fatal: transaction is closed" err
 '
 
 test_expect_success 'transaction exits on start after prepare' '
@@ -1500,7 +1503,7 @@
 	test_must_fail git update-ref --stdin <stdin 2>err >actual &&
 	printf "%s: ok\n" prepare >expect &&
 	test_cmp expect actual &&
-	grep "fatal: prepared transactions can only be closed" err
+	test_grep "fatal: prepared transactions can only be closed" err
 '
 
 test_expect_success 'transaction handles empty abort with missing prepare' '
@@ -1661,7 +1664,7 @@
 
 	# This must now fail given that we have locked the ref.
 	test_must_fail git update-ref refs/heads/flush $B 2>stderr &&
-	grep "fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref" stderr &&
+	test_grep "fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref" stderr &&
 
 	echo commit >&9 &&
 	echo "commit: ok" >expected &&
@@ -1687,7 +1690,7 @@
 		git symbolic-ref refs/heads/symref $a &&
 		format_command $type "symref-verify refs/heads/symref" "$a" >stdin &&
 		test_must_fail git update-ref --stdin $type <stdin 2>err &&
-		grep "fatal: symref-verify: cannot operate with deref mode" err
+		test_grep "fatal: symref-verify: cannot operate with deref mode" err
 	'
 
 	test_expect_success "stdin $type symref-verify fails with too many arguments" '
@@ -1695,9 +1698,9 @@
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&
 		if test "$type" = "-z"
 		then
-			grep "fatal: unknown command: $a" err
+			test_grep "fatal: unknown command: $a" err
 		else
-			grep "fatal: symref-verify refs/heads/symref: extra input:  $a" err
+			test_grep "fatal: symref-verify refs/heads/symref: extra input:  $a" err
 		fi
 	'
 
@@ -1730,7 +1733,7 @@
 		test-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&
 		format_command $type "symref-verify refs/heads/missing" "refs/heads/unknown" >stdin &&
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
-		grep "fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}" err &&
+		test_grep "fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}" err &&
 		test_must_fail git rev-parse --verify -q refs/heads/missing &&
 		test-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&
 		test_cmp before after
@@ -1756,13 +1759,13 @@
 		git symbolic-ref refs/heads/symref $a &&
 		format_command $type "symref-delete refs/heads/symref" "$a" >stdin &&
 		test_must_fail git update-ref --stdin $type <stdin 2>err &&
-		grep "fatal: symref-delete: cannot operate with deref mode" err
+		test_grep "fatal: symref-delete: cannot operate with deref mode" err
 	'
 
 	test_expect_success "stdin $type symref-delete fails with no ref" '
 		format_command $type "symref-delete " >stdin &&
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
-		grep "fatal: symref-delete: missing <ref>" err
+		test_grep "fatal: symref-delete: missing <ref>" err
 	'
 
 	test_expect_success "stdin $type symref-delete fails deleting regular ref" '
@@ -1770,7 +1773,7 @@
 		git update-ref refs/heads/regularref $a &&
 		format_command $type "symref-delete refs/heads/regularref" "$a" >stdin &&
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
-		grep "fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref" err
+		test_grep "fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref" err
 	'
 
 	test_expect_success "stdin $type symref-delete fails with too many arguments" '
@@ -1778,16 +1781,16 @@
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
 		if test "$type" = "-z"
 		then
-			grep "fatal: unknown command: $a" err
+			test_grep "fatal: unknown command: $a" err
 		else
-			grep "fatal: symref-delete refs/heads/symref: extra input:  $a" err
+			test_grep "fatal: symref-delete refs/heads/symref: extra input:  $a" err
 		fi
 	'
 
 	test_expect_success "stdin $type symref-delete fails with wrong old value" '
 		format_command $type "symref-delete refs/heads/symref" "$m" >stdin &&
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
-		grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main" err &&
+		test_grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main" err &&
 		git symbolic-ref refs/heads/symref >expect &&
 		echo $a >actual &&
 		test_cmp expect actual
@@ -1825,9 +1828,9 @@
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
 		if test "$type" = "-z"
 		then
-			grep "fatal: unknown command: $a" err
+			test_grep "fatal: unknown command: $a" err
 		else
-			grep "fatal: symref-create refs/heads/symref: extra input:  $a" err
+			test_grep "fatal: symref-create refs/heads/symref: extra input:  $a" err
 		fi
 	'
 
@@ -1890,16 +1893,16 @@
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
 		if test "$type" = "-z"
 		then
-			grep "fatal: unknown command: $a" err
+			test_grep "fatal: unknown command: $a" err
 		else
-			grep "fatal: symref-update refs/heads/symref: extra input:  $a" err
+			test_grep "fatal: symref-update refs/heads/symref: extra input:  $a" err
 		fi
 	'
 
 	test_expect_success "stdin $type symref-update fails with wrong old value argument" '
 		format_command $type "symref-update refs/heads/symref" "$a" "foo" "$a" "$a" >stdin &&
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
-		grep "fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value" err
+		test_grep "fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value" err
 	'
 
 	test_expect_success "stdin $type symref-update creates with zero old value" '
@@ -1935,7 +1938,7 @@
 		git symbolic-ref refs/heads/symref $a &&
 		format_command $type "symref-update refs/heads/symref" "$m" "ref" "$b" >stdin &&
 		test_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&
-		grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b" err &&
+		test_grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b" err &&
 		test_must_fail git rev-parse --verify -q $c
 	'
 
@@ -2010,7 +2013,7 @@
 		git symbolic-ref --no-recurse refs/heads/symref >actual &&
 		test_cmp expect actual &&
 		test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&
-		grep "$Z $(git rev-parse $a)" actual
+		test_grep "$Z $(git rev-parse $a)" actual
 	'
 
 	test_expect_success "stdin $type symref-update regular ref to symref with correct old-oid" '
@@ -2022,7 +2025,7 @@
 		git symbolic-ref --no-recurse refs/heads/regularref >actual &&
 		test_cmp expect actual &&
 		test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&
-		grep "$(git rev-parse $a) $(git rev-parse $a)" actual
+		test_grep "$(git rev-parse $a) $(git rev-parse $a)" actual
 	'
 
 	test_expect_success "stdin $type symref-update regular ref to symref fails with wrong old-oid" '
@@ -2030,7 +2033,7 @@
 		git update-ref --no-deref refs/heads/regularref $a &&
 		format_command $type "symref-update refs/heads/regularref" "$a" "oid" "$(git rev-parse refs/heads/target2)" >stdin &&
 		test_must_fail git update-ref --stdin $type <stdin 2>err &&
-		grep "fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)" err &&
+		test_grep "fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)" err &&
 		echo $(git rev-parse $a) >expect &&
 		git rev-parse refs/heads/regularref >actual &&
 		test_cmp expect actual
@@ -2041,7 +2044,7 @@
 		git update-ref --no-deref refs/heads/regularref $a &&
 		format_command $type "symref-update refs/heads/regularref" "$a" "oid" "not-a-ref-oid" >stdin &&
 		test_must_fail git update-ref --stdin $type <stdin 2>err &&
-		grep "fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid" err &&
+		test_grep "fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid" err &&
 		echo $(git rev-parse $a) >expect &&
 		git rev-parse refs/heads/regularref >actual &&
 		test_cmp expect actual
@@ -2052,7 +2055,7 @@
 		git symbolic-ref refs/heads/symref refs/heads/target2 &&
 		format_command $type "symref-update refs/heads/symref" "$a" "oid" "$Z" >stdin &&
 		test_must_fail git update-ref --stdin $type <stdin 2>err &&
-		grep "fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists" err &&
+		test_grep "fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists" err &&
 		echo refs/heads/target2 >expect &&
 		git symbolic-ref refs/heads/symref >actual &&
 		test_cmp expect actual
@@ -2072,7 +2075,7 @@
 		git symbolic-ref --no-recurse refs/heads/symref >actual &&
 		test_cmp expect actual &&
 		test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&
-		grep "$(git rev-parse $a) $(git rev-parse $a)" actual
+		test_grep "$(git rev-parse $a) $(git rev-parse $a)" actual
 	'
 
 	test_expect_success "stdin $type symref-update regular ref to symref" '
@@ -2084,7 +2087,7 @@
 		git symbolic-ref --no-recurse refs/heads/regularref >actual &&
 		test_cmp expect actual &&
 		test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&
-		grep "$(git rev-parse $a) $(git rev-parse $a)" actual
+		test_grep "$(git rev-parse $a) $(git rev-parse $a)" actual
 	'
 
 	test_expect_success "stdin $type batch-updates" '
diff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh
index 36c903c..226b3ec 100755
--- a/t/t1403-show-ref.sh
+++ b/t/t1403-show-ref.sh
@@ -165,7 +165,7 @@
 '
 
 test_expect_success 'show-ref --heads is deprecated and hidden' '
-	test_expect_code 129 git show-ref -h >short-help &&
+	git show-ref -h >short-help &&
 	test_grep ! -e --heads short-help &&
 	git show-ref --heads >actual 2>warning &&
 	test_grep ! deprecated warning &&
@@ -213,19 +213,19 @@
 
 test_expect_success 'show-ref sub-modes are mutually exclusive' '
 	test_must_fail git show-ref --verify --exclude-existing 2>err &&
-	grep "verify" err &&
-	grep "exclude-existing" err &&
-	grep "cannot be used together" err &&
+	test_grep "verify" err &&
+	test_grep "exclude-existing" err &&
+	test_grep "cannot be used together" err &&
 
 	test_must_fail git show-ref --verify --exists 2>err &&
-	grep "verify" err &&
-	grep "exists" err &&
-	grep "cannot be used together" err &&
+	test_grep "verify" err &&
+	test_grep "exists" err &&
+	test_grep "cannot be used together" err &&
 
 	test_must_fail git show-ref --exclude-existing --exists 2>err &&
-	grep "exclude-existing" err &&
-	grep "exists" err &&
-	grep "cannot be used together" err
+	test_grep "exclude-existing" err &&
+	test_grep "exists" err &&
+	test_grep "cannot be used together" err
 '
 
 test_done
diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh
index ce71f9a..8f78cf4 100755
--- a/t/t1410-reflog.sh
+++ b/t/t1410-reflog.sh
@@ -107,13 +107,13 @@
 '
 
 test_expect_success 'correct usage on sub-command -h' '
-	test_expect_code 129 git reflog expire -h >err &&
-	grep "git reflog expire" err
+	git reflog expire -h >err &&
+	test_grep "git reflog expire" err
 '
 
 test_expect_success 'correct usage on "git reflog show -h"' '
-	test_expect_code 129 git reflog show -h >err &&
-	grep -F "git reflog [show]" err
+	git reflog show -h >err &&
+	test_grep -F "git reflog [show]" err
 '
 
 test_expect_success 'pass through -- to sub-command' '
@@ -244,30 +244,22 @@
 	test_tick &&
 	git commit -m tiger C &&
 
-	HEAD_entry_count=$(git reflog | wc -l) &&
-	main_entry_count=$(git reflog show main | wc -l) &&
-
-	test $HEAD_entry_count = 5 &&
-	test $main_entry_count = 5 &&
-
+	test_stdout_line_count = 5 git reflog &&
+	test_stdout_line_count = 5 git reflog show main &&
 
 	git reflog delete main@{1} &&
+	test_stdout_line_count = 4 git reflog show main &&
+	test_stdout_line_count = 5 git reflog &&
 	git reflog show main > output &&
-	test_line_count = $(($main_entry_count - 1)) output &&
-	test $HEAD_entry_count = $(git reflog | wc -l) &&
 	! grep ox < output &&
 
-	main_entry_count=$(wc -l < output) &&
-
 	git reflog delete HEAD@{1} &&
-	test $(($HEAD_entry_count -1)) = $(git reflog | wc -l) &&
-	test $main_entry_count = $(git reflog show main | wc -l) &&
-
-	HEAD_entry_count=$(git reflog | wc -l) &&
+	test_stdout_line_count = 4 git reflog &&
+	test_stdout_line_count = 4 git reflog show main &&
 
 	git reflog delete main@{07.04.2005.15:15:00.-0700} &&
+	test_stdout_line_count = 3 git reflog show main &&
 	git reflog show main > output &&
-	test_line_count = $(($main_entry_count - 1)) output &&
 	! grep dragon < output
 
 '
@@ -321,11 +313,11 @@
 '
 
 test_expect_success 'checkout should not delete log for packed ref' '
-	test $(git reflog main | wc -l) = 4 &&
+	test_stdout_line_count = 4 git reflog main &&
 	git branch foo &&
 	git pack-refs --all &&
 	git checkout foo &&
-	test $(git reflog main | wc -l) = 4
+	test_stdout_line_count = 4 git reflog main
 '
 
 test_expect_success 'stale dirs do not cause d/f conflicts (reflogs on)' '
diff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh
index 51d79ba..6b2ad04 100755
--- a/t/t1415-worktree-refs.sh
+++ b/t/t1415-worktree-refs.sh
@@ -32,7 +32,7 @@
 	test_when_finished git update-ref -d refs/heads/main-worktree/HEAD &&
 	git update-ref refs/heads/main-worktree/HEAD $(git rev-parse HEAD) &&
 	git rev-parse main-worktree/HEAD 2>warn &&
-	grep "main-worktree/HEAD.*ambiguous" warn
+	test_grep "main-worktree/HEAD.*ambiguous" warn
 '
 
 test_expect_success 'resolve worktrees/xx/HEAD' '
@@ -45,7 +45,7 @@
 	git update-ref refs/heads/worktrees/wt1/HEAD $(git rev-parse HEAD) &&
 	test_when_finished git update-ref -d refs/heads/worktrees/wt1/HEAD &&
 	git rev-parse worktrees/wt1/HEAD 2>warn &&
-	grep "worktrees/wt1/HEAD.*ambiguous" warn
+	test_grep "worktrees/wt1/HEAD.*ambiguous" warn
 '
 
 test_expect_success 'reflog of main-worktree/HEAD' '
diff --git a/t/t1418-reflog-exists.sh b/t/t1418-reflog-exists.sh
index d51ecd5..1038779 100755
--- a/t/t1418-reflog-exists.sh
+++ b/t/t1418-reflog-exists.sh
@@ -12,7 +12,7 @@
 
 test_expect_success 'usage' '
 	test_expect_code 129 git reflog exists &&
-	test_expect_code 129 git reflog exists -h
+	git reflog exists -h
 '
 
 test_expect_success 'usage: unknown option' '
diff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh
index 3ab65f7..1ed4c7d 100755
--- a/t/t1430-bad-ref-name.sh
+++ b/t/t1430-bad-ref-name.sh
@@ -47,7 +47,7 @@
 	test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" &&
 	git branch >output 2>error &&
 	test_grep -e "ignoring ref with broken name refs/heads/broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'branch -d can delete badly named ref' '
@@ -55,8 +55,8 @@
 	test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" &&
 	git branch -d broken...ref &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'branch -D can delete badly named ref' '
@@ -64,8 +64,8 @@
 	test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" &&
 	git branch -D broken...ref &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'branch -D cannot delete non-ref in .git dir' '
@@ -93,8 +93,8 @@
 	test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" &&
 	test_must_fail git branch broken...ref &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'branch -m cannot rename to a bad ref name' '
@@ -104,8 +104,8 @@
 	test_must_fail git branch -m goodref broken...ref &&
 	test_cmp_rev main goodref &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_failure 'branch -m can rename from a bad ref name' '
@@ -115,16 +115,16 @@
 	git branch -m broken...ref renamed &&
 	test_cmp_rev main renamed &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'push cannot create a badly named ref' '
 	test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref" &&
 	test_must_fail git push "file://$(pwd)" HEAD:refs/heads/broken...ref &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_failure 'push --mirror can delete badly named ref' '
@@ -144,8 +144,8 @@
 	) &&
 	git -C src push --mirror "file://$top/dest" &&
 	git -C dest branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'rev-parse skips symref pointing to broken name' '
@@ -168,9 +168,9 @@
 	test-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main &&
 	test_when_finished "test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref" &&
 	git for-each-ref >output 2>error &&
-	! grep -e "broken\.\.\.ref" output &&
-	! grep -e "badname" output &&
-	! grep -e "broken\.\.\.symref" output &&
+	test_grep ! -e "broken\.\.\.ref" output &&
+	test_grep ! -e "badname" output &&
+	test_grep ! -e "broken\.\.\.symref" output &&
 	test_grep "ignoring ref with broken name refs/heads/broken\.\.\.ref" error &&
 	test_grep ! "ignoring broken ref refs/heads/badname" error &&
 	test_grep "ignoring ref with broken name refs/heads/broken\.\.\.symref" error
@@ -183,8 +183,8 @@
 	test_must_be_empty output &&
 	test_must_be_empty error &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'branch -d can delete broken name' '
@@ -194,8 +194,8 @@
 	test_grep "Deleted branch broken...ref (was broken)" output &&
 	test_must_be_empty error &&
 	git branch >output 2>error &&
-	! grep -e "broken\.\.\.ref" error &&
-	! grep -e "broken\.\.\.ref" output
+	test_grep ! -e "broken\.\.\.ref" error &&
+	test_grep ! -e "broken\.\.\.ref" output
 '
 
 test_expect_success 'update-ref --no-deref -d can delete symref to broken name' '
@@ -313,37 +313,37 @@
 test_expect_success 'update-ref --stdin fails create with bad ref name' '
 	echo "create ~a refs/heads/main" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: invalid ref format: ~a" err
+	test_grep "fatal: invalid ref format: ~a" err
 '
 
 test_expect_success 'update-ref --stdin fails update with bad ref name' '
 	echo "update ~a refs/heads/main" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: invalid ref format: ~a" err
+	test_grep "fatal: invalid ref format: ~a" err
 '
 
 test_expect_success 'update-ref --stdin fails delete with bad ref name' '
 	echo "delete ~a refs/heads/main" >stdin &&
 	test_must_fail git update-ref --stdin <stdin 2>err &&
-	grep "fatal: invalid ref format: ~a" err
+	test_grep "fatal: invalid ref format: ~a" err
 '
 
 test_expect_success 'update-ref --stdin -z fails create with bad ref name' '
 	printf "%s\0" "create ~a " refs/heads/main >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: invalid ref format: ~a " err
+	test_grep "fatal: invalid ref format: ~a " err
 '
 
 test_expect_success 'update-ref --stdin -z fails update with bad ref name' '
 	printf "%s\0" "update ~a" refs/heads/main "" >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: invalid ref format: ~a" err
+	test_grep "fatal: invalid ref format: ~a" err
 '
 
 test_expect_success 'update-ref --stdin -z fails delete with bad ref name' '
 	printf "%s\0" "delete ~a" refs/heads/main >stdin &&
 	test_must_fail git update-ref -z --stdin <stdin 2>err &&
-	grep "fatal: invalid ref format: ~a" err
+	test_grep "fatal: invalid ref format: ~a" err
 '
 
 test_expect_success 'branch rejects HEAD as a branch name' '
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 54e81c2..77cd96d 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -68,7 +68,7 @@
 		git update-ref refs/heads/bogus $cmt &&
 
 		test_must_fail git fsck 2>out &&
-		grep "$oldoid: hash-path mismatch, found at: .*$new" out
+		test_grep "$oldoid: hash-path mismatch, found at: .*$new" out
 	)
 '
 
@@ -172,7 +172,7 @@
 	test_when_finished "git update-ref -d refs/heads/bogus" &&
 	git fsck 2>out &&
 	cat out &&
-	! grep "commit $new" out
+	test_grep ! "commit $new" out
 '
 
 test_expect_success 'email without @ is okay' '
@@ -183,7 +183,7 @@
 	git update-ref refs/heads/bogus "$new" &&
 	test_when_finished "git update-ref -d refs/heads/bogus" &&
 	git fsck 2>out &&
-	! grep "commit $new" out
+	test_grep ! "commit $new" out
 '
 
 test_expect_success 'email with embedded > is not okay' '
@@ -538,6 +538,23 @@
 	test_grep -q "error: hash mismatch $(dirname $new)$(test_oid ff_2)" out
 '
 
+test_expect_success 'rev-list --verify-objects with truncated loose blob' '
+	git init truncated-blob &&
+	(
+		cd truncated-blob &&
+		blob=$(test-tool genrandom one 5k | git hash-object -t blob -w --stdin) &&
+		obj=.git/objects/$(test_oid_to_path $blob) &&
+
+		# Truncate the loose blob such that its header can still be
+		# parsed, but reading the object data fails mid-stream.
+		test_copy_bytes 64 <"$obj" >obj.tmp &&
+		mv obj.tmp "$obj" &&
+
+		test_must_fail git rev-list --verify-objects "$blob" 2>err &&
+		test_grep "hash mismatch" err
+	)
+'
+
 # An actual bit corruption is more likely than swapped commits, but
 # this provides an easy way to have commits which don't match their purported
 # hashes, but which aren't so broken we can't read them at all.
@@ -626,7 +643,7 @@
 		cd large-name &&
 		test_commit a-long-name &&
 		git -c fsck.largePathname=warn:10 fsck 2>out &&
-		grep "warning.*large pathname" out
+		test_grep "warning.*large pathname" out
 	)
 '
 
@@ -849,7 +866,7 @@
 	test_must_fail git fsck 2>out &&
 	test_grep "error in commit $one.* - bad name" out &&
 	test_grep "error in commit $two.* - bad name" out &&
-	! grep corrupt out
+	test_grep ! corrupt out
 '
 
 test_expect_success 'fsck handles multiple packfiles with big blobs' '
@@ -1027,7 +1044,7 @@
 	test_when_finished "git rm --cached foo" &&
 	remove_object $blob &&
 	test_must_fail git fsck $ZERO_OID >out 2>&1 &&
-	! grep $blob out
+	test_grep ! $blob out
 '
 
 # Corrupt the checksum on the index.
diff --git a/t/t1451-fsck-buffer.sh b/t/t1451-fsck-buffer.sh
index 3a3d33f..14151ea 100755
--- a/t/t1451-fsck-buffer.sh
+++ b/t/t1451-fsck-buffer.sh
@@ -46,7 +46,7 @@
 			echo "$content"
 		} >input &&
 		test_must_fail git hash-object -t "$type" input 2>err &&
-		grep "$fsck" err
+		test_grep "$fsck" err
 	'
 }
 
@@ -125,7 +125,7 @@
 test_expect_success 'truncated tree (short hash)' '
 	printf "100644 foo\0\1\1\1\1" >input &&
 	test_must_fail git hash-object -t tree input 2>err &&
-	grep badTree err
+	test_grep badTree err
 '
 
 test_expect_success 'truncated tree (missing nul)' '
@@ -135,7 +135,7 @@
 	# parser does not walk past the end of the buffer).
 	printf "100644 a long filename, or a hash with missing nul?" >input &&
 	test_must_fail git hash-object -t tree input 2>err &&
-	grep badTree err
+	test_grep badTree err
 '
 
 test_done
diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh
index 5246468..8f42697 100755
--- a/t/t1460-refs-migrate.sh
+++ b/t/t1460-refs-migrate.sh
@@ -212,7 +212,7 @@
 			test_commit -C repo initial &&
 			git -C repo refs migrate --dry-run \
 				--ref-format=$to_format >output &&
-			grep "Finished dry-run migration of refs" output &&
+			test_grep "Finished dry-run migration of refs" output &&
 			test_path_is_dir repo/.git/ref_migration.* &&
 			echo $from_format >expect &&
 			git -C repo rev-parse --show-ref-format >actual &&
diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh
new file mode 100755
index 0000000..c88063e
--- /dev/null
+++ b/t/t1464-refs-delete.sh
@@ -0,0 +1,152 @@
+#!/bin/sh
+
+test_description='git refs delete'
+
+. ./test-lib.sh
+
+setup_repo () {
+	git init "$1" &&
+	test_commit -C "$1" A &&
+	test_commit -C "$1" B
+}
+
+test_expect_success 'delete without oldvalue verification' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git refs delete refs/heads/foo &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with matching oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git refs delete refs/heads/foo $A &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with stale oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete refs/heads/foo $B 2>err &&
+		test_grep " but expected " err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with null oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err &&
+		test_grep "null old object ID" err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with invalid oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete refs/heads/foo invalid-oid 2>err &&
+		test_grep "invalid old object ID" err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete symref with --no-deref leaves target intact' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git symbolic-ref refs/heads/symref refs/heads/foo &&
+		git refs delete --no-deref refs/heads/symref &&
+		test_must_fail git refs exists refs/heads/symref &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete symref with --no-deref verifies target OID' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git update-ref refs/heads/foo $A &&
+		git symbolic-ref refs/heads/symref refs/heads/foo &&
+
+		test_must_fail git refs delete --no-deref refs/heads/symref $B &&
+		git refs exists refs/heads/symref &&
+
+		git refs delete --no-deref refs/heads/symref $A &&
+		test_must_fail git refs exists refs/heads/symref &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete with message records reason in reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		git symbolic-ref HEAD refs/heads/foo &&
+		git refs delete --message=delete-reason refs/heads/foo &&
+		test_must_fail git refs exists refs/heads/foo &&
+		test-tool ref-store main for-each-reflog-ent HEAD >actual &&
+		test_grep "delete-reason$" actual
+	)
+'
+
+test_expect_success 'delete with empty message fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git update-ref refs/heads/foo $A &&
+		test_must_fail git refs delete --message= refs/heads/foo 2>err &&
+		test_grep "empty message" err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'delete without arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs delete 2>err &&
+	test_grep "requires reference name" err
+'
+
+test_expect_success 'delete with too many arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git refs delete one two three 2>err &&
+	test_grep "requires reference name" err
+'
+
+test_done
diff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh
new file mode 100755
index 0000000..a9becdd
--- /dev/null
+++ b/t/t1465-refs-update.sh
@@ -0,0 +1,268 @@
+#!/bin/sh
+
+test_description='git refs update'
+
+. ./test-lib.sh
+
+setup_repo () {
+	git init "$1" &&
+	test_commit -C "$1" A &&
+	test_commit -C "$1" B
+}
+
+test_ref_matches () {
+	git rev-parse "$1" >expect &&
+	echo "$2" >actual &&
+	test_cmp expect actual
+}
+
+test_expect_success 'update creates a new reference' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A &&
+		test_ref_matches refs/heads/foo "$A"
+	)
+'
+
+test_expect_success 'update an existing reference without oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		git refs update refs/heads/foo $B &&
+		test_ref_matches refs/heads/foo $B
+	)
+'
+
+test_expect_success 'update with matching oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		git refs update refs/heads/foo $B $A &&
+		test_ref_matches refs/heads/foo $B
+	)
+'
+
+test_expect_success 'update with stale oldvalue fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs update refs/heads/foo $B $B 2>err &&
+		test_grep " but expected " err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update can create a new branch with oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A $ZERO_OID 2>err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update can create a new branch without oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A 2>err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update refuses to create preexisting branch' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
+		test_grep "reference already exists" err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update can delete a branch with oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A 2>err &&
+		git refs update refs/heads/foo $ZERO_OID $A 2>err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'update can delete a branch without oldvalue' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A 2>err &&
+		git refs update refs/heads/foo $ZERO_OID 2>err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'update refuses to delete a branch with mismatching value' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A 2>err &&
+		test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err &&
+		test_grep " but expected " err &&
+		git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'update refuses to create preexisting branch' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err &&
+		test_grep "reference already exists" err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+
+test_expect_success 'update with invalid new value fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		test_must_fail git refs update refs/heads/foo invalid-oid 2>err &&
+		test_grep "invalid new object ID" err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'update with invalid old value fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs update refs/heads/foo $B invalid-oid 2>err &&
+		test_grep "invalid old object ID" err &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update --no-deref rewrites the symref itself' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		git symbolic-ref refs/heads/symref refs/heads/foo &&
+		git refs update --no-deref refs/heads/symref $B &&
+		test_must_fail git symbolic-ref refs/heads/symref &&
+		test_ref_matches refs/heads/symref $B &&
+		test_ref_matches refs/heads/foo $A
+	)
+'
+
+test_expect_success 'update does not create a reflog by default' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/foo $A &&
+		test_must_fail git reflog exists refs/foo
+	)
+'
+
+test_expect_success 'update creates a reflog with --create-reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update --create-reflog refs/foo $A &&
+		git reflog exists refs/foo
+	)
+'
+
+test_expect_success 'update with message records reason in reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		git refs update --message=update-reason refs/heads/foo $B &&
+		git reflog show refs/heads/foo >actual &&
+		test_grep "update-reason$" actual
+	)
+'
+
+test_expect_success 'update with empty message fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs update --message= refs/heads/foo $B 2>err &&
+		test_grep "empty message" err
+	)
+'
+
+test_expect_success 'update with too few arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs update refs/heads/foo 2>err &&
+	test_grep "requires reference name, new value" err
+'
+
+test_expect_success 'update with too many arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		test_must_fail git refs update refs/heads/foo $A $B extra 2>err &&
+		test_grep "requires reference name, new value" err
+	)
+'
+
+test_done
diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh
new file mode 100755
index 0000000..cfb21bf
--- /dev/null
+++ b/t/t1466-refs-create.sh
@@ -0,0 +1,151 @@
+#!/bin/sh
+
+test_description='git refs create'
+
+. ./test-lib.sh
+
+setup_repo () {
+	git init "$1" &&
+	test_commit -C "$1" A &&
+	test_commit -C "$1" B
+}
+
+test_ref_matches () {
+	git rev-parse "$1" >expect &&
+	echo "$2" >actual &&
+	test_cmp expect actual
+}
+
+test_expect_success 'create a new reference' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs create refs/heads/foo $A &&
+		test_ref_matches refs/heads/foo "$A"
+	)
+'
+
+test_expect_success 'create fails when the reference already exists' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs create refs/heads/foo $A &&
+		test_must_fail git refs create refs/heads/foo $B 2>err &&
+		test_grep "reference already exists" err &&
+		test_ref_matches refs/heads/foo "$A"
+	)
+'
+
+test_expect_success 'create with null new value fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		test_must_fail git refs create refs/heads/foo $ZERO_OID 2>err &&
+		test_grep "null new object ID" err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'create with invalid new value fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		test_must_fail git refs create refs/heads/foo invalid-oid 2>err &&
+		test_grep "invalid object ID" err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'create does not create a reflog by default' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs create refs/foo $A &&
+		test_must_fail git reflog exists refs/foo
+	)
+'
+
+test_expect_success 'create creates a reflog with --create-reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs create --create-reflog refs/foo $A &&
+		git reflog exists refs/foo
+	)
+'
+
+test_expect_success 'create with message records reason in reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs create --message="create reason" refs/heads/foo $A &&
+		git reflog show refs/heads/foo >actual &&
+		test_grep "create reason$" actual
+	)
+'
+
+test_expect_success 'create with symref target creates target reference' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git symbolic-ref refs/heads/symref refs/heads/target &&
+		git refs create refs/heads/symref $A &&
+		git reflog exists refs/heads/target
+	)
+'
+
+test_expect_success 'create with symref target and --no-deref refuses to create reference' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git symbolic-ref refs/heads/symref refs/heads/target &&
+		test_must_fail git refs create --no-deref refs/heads/symref $A 2>err &&
+		test_grep "dangling symref already exists" err &&
+		test_must_fail git reflog exists refs/heads/target
+	)
+'
+
+test_expect_success 'create with empty message fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		test_must_fail git refs create --message= refs/heads/foo $A 2>err &&
+		test_grep "empty message" err &&
+		test_must_fail git refs exists refs/heads/foo
+	)
+'
+
+test_expect_success 'create without arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs create 2>err &&
+	test_grep "requires reference name" err
+'
+
+test_expect_success 'create with too many arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs create refs/heads/foo a b 2>err &&
+	test_grep "requires reference name" err
+'
+
+test_done
diff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh
new file mode 100755
index 0000000..2b28be7
--- /dev/null
+++ b/t/t1467-refs-rename.sh
@@ -0,0 +1,144 @@
+#!/bin/sh
+
+test_description='git refs rename'
+
+. ./test-lib.sh
+
+setup_repo () {
+	git init "$1" &&
+	test_commit -C "$1" A &&
+	test_commit -C "$1" B
+}
+
+test_ref_matches () {
+	git rev-parse "$1" >expect &&
+	echo "$2" >actual &&
+	test_cmp expect actual
+}
+
+test_expect_success 'rename an existing reference' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A &&
+		git refs rename refs/heads/foo refs/heads/bar &&
+		test_must_fail git refs exists refs/heads/foo &&
+		test_ref_matches refs/heads/bar $A
+	)
+'
+
+test_expect_success 'rename moves the reflog along with the reference' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update --message="rename me" refs/heads/foo $A &&
+		git refs rename refs/heads/foo refs/heads/bar &&
+		git reflog show refs/heads/bar >reflog &&
+		test_grep "rename me" reflog &&
+		test_must_fail git reflog exists refs/heads/foo
+	)
+'
+
+test_expect_success 'rename with message records reason in reflog' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A &&
+		git refs rename --message="rename reason" refs/heads/foo refs/heads/bar &&
+		git reflog show refs/heads/bar >actual &&
+		test_grep "rename reason" actual
+	)
+'
+
+test_expect_success 'rename a nonexistent reference fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+		test_grep "reference does not exist" err
+	)
+'
+
+test_expect_success 'rename to an existing reference fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		B=$(git rev-parse B) &&
+		git refs update refs/heads/foo $A &&
+		git refs update refs/heads/bar $B &&
+		test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err &&
+		test_grep "reference already exists" err
+	)
+'
+
+test_expect_success 'rename with symbolic ref fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs create refs/heads/target $A &&
+		git symbolic-ref refs/heads/symref refs/heads/target &&
+		! git refs rename refs/heads/symref refs/heads/renamed 2>err &&
+		test_grep "is a symbolic ref, .* not supported" err
+	)
+'
+
+test_expect_success 'rename with empty message fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err &&
+		test_grep "empty message" err
+	)
+'
+
+test_expect_success 'rename with invalid old reference name fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err &&
+		test_grep "invalid ref format" err
+	)
+'
+
+test_expect_success 'rename with invalid new reference name fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	(
+		cd repo &&
+		A=$(git rev-parse A) &&
+		git refs update refs/heads/foo $A &&
+		test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err &&
+		test_grep "invalid ref format" err
+	)
+'
+
+test_expect_success 'rename with too few arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs rename refs/heads/foo 2>err &&
+	test_grep "requires old and new reference name" err
+'
+
+test_expect_success 'rename with too many arguments fails' '
+	test_when_finished "rm -rf repo" &&
+	setup_repo repo &&
+	test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err &&
+	test_grep "requires old and new reference name" err
+'
+
+test_done
diff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh
index 38067d9..4174ca4 100755
--- a/t/t1500-rev-parse.sh
+++ b/t/t1500-rev-parse.sh
@@ -204,7 +204,7 @@
 	git rev-parse --show-object-format=output >actual &&
 	test_cmp expect actual &&
 	test_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&
-	grep "unknown mode for --show-object-format: squeamish-ossifrage" err
+	test_grep "unknown mode for --show-object-format: squeamish-ossifrage" err
 '
 
 
@@ -228,7 +228,7 @@
 		git rev-parse --show-object-format=compat >actual &&
 		test_cmp expect actual &&
 		test_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&
-		grep "unknown mode for --show-object-format: squeamish-ossifrage" err
+		test_grep "unknown mode for --show-object-format: squeamish-ossifrage" err
 	) &&
 	mkdir repo2 &&
 	(
@@ -254,7 +254,7 @@
 		cd repo &&
 		git config extensions.refstorage broken &&
 		test_must_fail git rev-parse --show-ref-format 2>err &&
-		grep "error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}" err
+		test_grep "error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}" err
 	)
 '
 
diff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh
index 3962f1d2..7167adf 100755
--- a/t/t1502-rev-parse-parseopt.sh
+++ b/t/t1502-rev-parse-parseopt.sh
@@ -12,7 +12,7 @@
 			cat <<-\EOF &&
 			error: unknown option `'${opt#--}\''
 			EOF
-			sed -e 1d -e \$d <"$TEST_DIRECTORY/t1502/$spec.help"
+			sed -e 1d -e /EOF/d -e \$d <"$TEST_DIRECTORY/t1502/$spec.help"
 		} >expect &&
 		test_expect_code 129 git rev-parse --parseopt -- $opt \
 			2>output <"$TEST_DIRECTORY/t1502/$spec" &&
@@ -75,7 +75,7 @@
 '
 
 test_expect_success 'test --parseopt help output' '
-	test_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec &&
+	git rev-parse --parseopt -- -h > output < optionspec &&
 	test_cmp "$TEST_DIRECTORY/t1502/optionspec.help" output
 '
 
@@ -87,8 +87,9 @@
 |    some-command does foo and bar!
 |
 |EOF
+|exit 0
 END_EXPECT
-	test_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_no_switches &&
+	git rev-parse --parseopt -- -h > output < optionspec_no_switches &&
 	test_cmp expect output
 '
 
@@ -100,8 +101,9 @@
 |    some-command does foo and bar!
 |
 |EOF
+|exit 0
 END_EXPECT
-	test_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&
+	git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&
 	test_cmp expect output
 '
 
@@ -115,8 +117,9 @@
 |    --[no-]hidden1        A hidden switch
 |
 |EOF
+|exit 0
 END_EXPECT
-	test_expect_code 129 git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&
+	git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&
 	test_cmp expect output
 '
 
@@ -125,7 +128,7 @@
 		cat <<-\EOF &&
 		error: unknown option `does-not-exist'\''
 		EOF
-		sed -e 1d -e \$d <"$TEST_DIRECTORY/t1502/optionspec.help"
+		sed -e 1d -e /EOF/d -e \$d <"$TEST_DIRECTORY/t1502/optionspec.help"
 	} >expect &&
 	test_expect_code 129 git rev-parse --parseopt -- --does-not-exist 1>/dev/null 2>output < optionspec &&
 	test_cmp expect output
@@ -252,9 +255,10 @@
 	|    -h, --help            show the help
 	|
 	|EOF
+	|exit 0
 	END_EXPECT
 
-	test_must_fail git rev-parse --parseopt -- -h <spec >actual &&
+	git rev-parse --parseopt -- -h <spec >actual &&
 	test_cmp expect actual
 '
 
@@ -289,14 +293,15 @@
 	|    -h, --help            show the help
 	|
 	|EOF
+	|exit 0
 	END_EXPECT
 
-	test_must_fail git rev-parse --parseopt -- -h <spec >actual &&
+	git rev-parse --parseopt -- -h <spec >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'test --parseopt help output for optionspec-neg' '
-	test_expect_code 129 git rev-parse --parseopt -- \
+	git rev-parse --parseopt -- \
 		-h >output <"$TEST_DIRECTORY/t1502/optionspec-neg" &&
 	test_cmp "$TEST_DIRECTORY/t1502/optionspec-neg.help" output
 '
@@ -331,7 +336,7 @@
 	EOF
 	test_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \
 	git rev-parse --parseopt -- <spec 2>err --no &&
-	grep "error: ambiguous option: no (could be --noble or --no-noble)" err
+	test_grep "error: ambiguous option: no (could be --noble or --no-noble)" err
 '
 
 test_done
diff --git a/t/t1502/optionspec-neg.help b/t/t1502/optionspec-neg.help
index 7a29f8c..f85be7b 100644
--- a/t/t1502/optionspec-neg.help
+++ b/t/t1502/optionspec-neg.help
@@ -10,3 +10,4 @@
     --no-negative         cannot be positivated
 
 EOF
+exit 0
diff --git a/t/t1502/optionspec.help b/t/t1502/optionspec.help
index cbdd54d..ded35eb 100755
--- a/t/t1502/optionspec.help
+++ b/t/t1502/optionspec.help
@@ -34,3 +34,4 @@
     --[no-]extra1         line above used to cause a segfault but no longer does
 
 EOF
+exit 0
diff --git a/t/t1503-rev-parse-verify.sh b/t/t1503-rev-parse-verify.sh
index 75a708f..87638a4 100755
--- a/t/t1503-rev-parse-verify.sh
+++ b/t/t1503-rev-parse-verify.sh
@@ -62,15 +62,15 @@
 
 test_expect_success 'fails with any bad rev or many good revs' '
 	test_must_fail git rev-parse --verify 2>error &&
-	grep "single revision" error &&
+	test_grep "single revision" error &&
 	test_must_fail git rev-parse --verify foo 2>error &&
-	grep "single revision" error &&
+	test_grep "single revision" error &&
 	test_must_fail git rev-parse --verify HEAD bar 2>error &&
-	grep "single revision" error &&
+	test_grep "single revision" error &&
 	test_must_fail git rev-parse --verify baz HEAD 2>error &&
-	grep "single revision" error &&
+	test_grep "single revision" error &&
 	test_must_fail git rev-parse --verify $HASH2 HEAD 2>error &&
-	grep "single revision" error
+	test_grep "single revision" error
 '
 
 test_expect_success 'fails silently when using -q' '
diff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh
index bbfe05b..d330b8d 100755
--- a/t/t1510-repo-setup.sh
+++ b/t/t1510-repo-setup.sh
@@ -604,7 +604,7 @@
 		cd 20b/.git &&
 		test_must_fail git status >/dev/null
 	) 2>message &&
-	grep "core.bare and core.worktree" message
+	test_grep "core.bare and core.worktree" message
 '
 
 test_expect_success '#20d: core.worktree and core.bare OK when working tree not needed' '
@@ -721,8 +721,8 @@
 		export GIT_DIR &&
 		test_must_fail git status 2>result
 	) &&
-	grep "core.bare and core.worktree" 22/.git/result &&
-	grep "core.bare and core.worktree" 22/result
+	test_grep "core.bare and core.worktree" 22/.git/result &&
+	test_grep "core.bare and core.worktree" 22/result
 '
 
 # Case #23: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses bareness.
@@ -767,7 +767,7 @@
 		cd 28 &&
 		test_must_fail git status
 	) 2>message &&
-	grep "core.bare and core.worktree" message
+	test_grep "core.bare and core.worktree" message
 '
 
 # Case #29: GIT_WORK_TREE(+core.worktree) overrides core.bare (gitfile case).
@@ -791,7 +791,7 @@
 		cd 30 &&
 		test_must_fail env GIT_DIR=.git git status 2>result
 	) &&
-	grep "core.bare and core.worktree" 30/result
+	test_grep "core.bare and core.worktree" 30/result
 '
 
 # Case #31: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses
diff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh
index 1a380a4..0e6b42e 100755
--- a/t/t1512-rev-parse-disambiguation.sh
+++ b/t/t1512-rev-parse-disambiguation.sh
@@ -378,7 +378,7 @@
 	VAL=$(git commit-tree $TREE </dev/null) &&
 	git update-ref refs/heads/$REF $VAL &&
 	test $(git rev-parse $REF 2>err) = $REF &&
-	grep "refname.*${REF}.*ambiguous" err
+	test_grep "refname.*${REF}.*ambiguous" err
 '
 
 test_expect_success 'ambiguous short sha1 ref' '
@@ -387,7 +387,7 @@
 	VAL=$(git commit-tree $TREE </dev/null) &&
 	git update-ref refs/heads/$REF $VAL &&
 	test $(git rev-parse $REF 2>err) = $VAL &&
-	grep "refname.*${REF}.*ambiguous" err
+	test_grep "refname.*${REF}.*ambiguous" err
 '
 
 test_expect_success 'ambiguity errors are not repeated (raw)' '
diff --git a/t/t1515-rev-parse-outside-repo.sh b/t/t1515-rev-parse-outside-repo.sh
index 75e89c4..2cb3c7c 100755
--- a/t/t1515-rev-parse-outside-repo.sh
+++ b/t/t1515-rev-parse-outside-repo.sh
@@ -32,7 +32,7 @@
 	git rev-parse --local-env-vars >actual &&
 	# we do not want to depend on the complete list here,
 	# so just look for something plausible
-	grep ^GIT_DIR actual
+	test_grep ^GIT_DIR actual
 '
 
 test_expect_success 'rev-parse --resolve-git-dir' '
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c557f2f..d1e9157 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -4,6 +4,13 @@
 
 . ./test-lib.sh
 
+test_lazy_prereq SVN '
+	test_have_prereq PERL && test -z "$NO_SVN_TESTS" && perl -w -e "
+		use SVN::Core;
+		use SVN::Repos;
+	"
+'
+
 test_expect_success 'set up a non-repo directory and test file' '
 	GIT_CEILING_DIRECTORIES=$(pwd) &&
 	export GIT_CEILING_DIRECTORIES &&
@@ -122,39 +129,48 @@
 	archimport | citool | credential-netrc | credential-libsecret | \
 	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
 	daemon | \
-	difftool--helper | filter-branch | format-rev | fsck-objects | \
-	get-tar-commit-id | \
+	difftool--helper | format-rev | fsck-objects |  get-tar-commit-id | \
 	gui | gui--askpass | \
 	http-backend | http-fetch | http-push | init-db | \
-	merge-octopus | merge-one-file | merge-resolve | mergetool | \
 	mktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \
 	remote-http | remote-https | replay | send-email | \
-	sh-i18n--envsubst | shell | show | stage | submodule | svn | \
-	upload-archive--writer | upload-pack | web--browse | whatchanged)
-		expect_outcome=expect_failure ;;
+	sh-i18n--envsubst | shell | show | stage | \
+	upload-archive--writer | upload-pack | whatchanged)
+		h_expect_outcome=expect_failure
+		all_expect_outcome=expect_failure
+		;;
+	filter-branch | merge-octopus | merge-one-file | merge-resolve | \
+	mergetool | submodule | svn | web--browse)
+		h_expect_outcome=expect_success
+		all_expect_outcome=expect_failure
+		;;
 	*)
-		expect_outcome=expect_success ;;
+		h_expect_outcome=expect_success
+		all_expect_outcome=expect_success
+		;;
 	esac
 	case "$cmd" in
 	instaweb)
 		prereq=PERL ;;
+	svn)
+		prereq=SVN ;;
 	*)
 		prereq= ;;
 	esac
-	test_$expect_outcome $prereq "'git $cmd -h' outside a repository" '
-		test_expect_code 129 nongit git $cmd -h >usage &&
+	test_$h_expect_outcome $prereq "'git $cmd -h' outside a repository" '
+		nongit git $cmd -h >usage &&
 		test_grep "[Uu]sage: git $cmd " usage
 	'
-	test_$expect_outcome $prereq "'git $cmd --help-all' outside a repository" '
-		test_expect_code 129 nongit git $cmd --help-all >usage &&
+	test_$all_expect_outcome $prereq "'git $cmd --help-all' outside a repository" '
+		nongit git $cmd --help-all >usage &&
 		test_grep "[Uu]sage: git $cmd " usage
 	'
 done
 
 test_expect_success 'fmt-merge-msg does not crash with -h' '
-	test_expect_code 129 git fmt-merge-msg -h >usage &&
+	git fmt-merge-msg -h >usage &&
 	test_grep "[Uu]sage: git fmt-merge-msg " usage &&
-	test_expect_code 129 nongit git fmt-merge-msg -h >usage &&
+	nongit git fmt-merge-msg -h >usage &&
 	test_grep "[Uu]sage: git fmt-merge-msg " usage
 '
 
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 0132e77..7d4b298 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -75,11 +75,11 @@
 test_expect_success 'git hook usage' '
 	test_expect_code 129 git hook &&
 	test_expect_code 129 git hook run &&
-	test_expect_code 129 git hook run -h &&
+	git hook run -h &&
 	test_expect_code 129 git hook run --unknown 2>err &&
 	test_expect_code 129 git hook list &&
-	test_expect_code 129 git hook list -h &&
-	grep "unknown option" err
+	git hook list -h &&
+	test_grep "unknown option" err
 '
 
 test_expect_success 'git hook list: unknown hook name is rejected' '
@@ -361,9 +361,9 @@
 
 	# 'ghi' should be included in both 'pre-commit' and 'test-hook'
 	git hook list pre-commit >actual &&
-	grep "ghi" actual &&
+	test_grep "ghi" actual &&
 	git hook list --allow-unknown-hook-name test-hook >actual &&
-	grep "ghi" actual
+	test_grep "ghi" actual
 '
 
 test_expect_success 'git hook list shows hooks from the hookdir' '
@@ -569,7 +569,7 @@
 	# TODO: We should emit the same (or at least a more similar)
 	# error on MINGW (essentially Git for Windows) and all other
 	# platforms.. See the OS-specific code in start_command()
-	grep -E "^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook" err
+	test_grep -E "^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook" err
 '
 
 test_expect_success 'stdin to hooks' '
@@ -826,8 +826,8 @@
 	# neither hook has parallel=true
 
 	git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&
-	grep "hook .hook-1. is not marked as parallel=true" err &&
-	grep "hook .hook-2. is not marked as parallel=true" err
+	test_grep "hook .hook-1. is not marked as parallel=true" err &&
+	test_grep "hook .hook-2. is not marked as parallel=true" err
 '
 
 test_expect_success 'hook.jobs=1 config runs hooks in series' '
@@ -1068,7 +1068,7 @@
 	cpus=$(test-tool online-cpus) &&
 	GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
 		git hook run --allow-unknown-hook-name test-hook >out 2>err &&
-	grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
+	test_grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
 '
 
 test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '
@@ -1081,7 +1081,7 @@
 	cpus=$(test-tool online-cpus) &&
 	GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
 		git hook run --allow-unknown-hook-name test-hook >out 2>err &&
-	grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
+	test_grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
 '
 
 test_expect_success 'git hook run -j-1 resolves to online_cpus()' '
@@ -1092,7 +1092,7 @@
 	cpus=$(test-tool online-cpus) &&
 	GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
 		git hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err &&
-	grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
+	test_grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
 '
 
 test_expect_success 'hook.jobs rejects values less than -1' '
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index ae8c22c..c85d390 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -150,7 +150,7 @@
 '
 
 test_expect_success 'git repo info -h shows only repo info usage' '
-	test_must_fail git repo info -h >actual &&
+	git repo info -h >actual &&
 	test_grep "git repo info" actual &&
 	test_grep ! "git repo structure" actual
 '
diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh
index 10050ab..02cc2b5 100755
--- a/t/t1901-repo-structure.sh
+++ b/t/t1901-repo-structure.sh
@@ -225,7 +225,7 @@
 '
 
 test_expect_success 'git repo structure -h shows only repo structure usage' '
-	test_must_fail git repo structure -h >actual &&
+	git repo structure -h >actual &&
 	test_grep "git repo structure" actual &&
 	test_grep ! "git repo info" actual
 '
diff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh
index 0afe0ff..db06c32 100755
--- a/t/t2004-checkout-cache-temp.sh
+++ b/t/t2004-checkout-cache-temp.sh
@@ -132,8 +132,8 @@
 test_expect_success '--stage=all --no-temp is rejected' '
 	rm -f path* .merge_* actual &&
 	test_must_fail git checkout-index --stage=all --no-temp -- path1 2>err &&
-	grep -v "already exists" err &&
-	grep "options .--stage=all. and .--no-temp. cannot be used together" err
+	test_grep -v "already exists" err &&
+	test_grep "options .--stage=all. and .--no-temp. cannot be used together" err
 '
 
 test_expect_success 'checkout some stages/one file to temporary files' '
diff --git a/t/t2006-checkout-index-basic.sh b/t/t2006-checkout-index-basic.sh
index fedd2cc..6538a24 100755
--- a/t/t2006-checkout-index-basic.sh
+++ b/t/t2006-checkout-index-basic.sh
@@ -16,15 +16,15 @@
 		cd broken &&
 		git init &&
 		>.git/index &&
-		test_expect_code 129 git checkout-index -h >usage 2>&1
+		git checkout-index -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage" broken/usage
 '
 
 test_expect_success 'checkout-index does not crash with -h' '
-	test_expect_code 129 git checkout-index -h >usage &&
+	git checkout-index -h >usage &&
 	test_grep "[Uu]sage: git checkout-index " usage &&
-	test_expect_code 129 nongit git checkout-index -h >usage &&
+	nongit git checkout-index -h >usage &&
 	test_grep "[Uu]sage: git checkout-index " usage
 '
 
diff --git a/t/t2019-checkout-ambiguous-ref.sh b/t/t2019-checkout-ambiguous-ref.sh
index 1fcef4b..0ac3579 100755
--- a/t/t2019-checkout-ambiguous-ref.sh
+++ b/t/t2019-checkout-ambiguous-ref.sh
@@ -19,7 +19,7 @@
 '
 
 test_expect_success 'checkout produces ambiguity warning' '
-	grep "warning.*ambiguous" stderr
+	test_grep "warning.*ambiguous" stderr
 '
 
 test_expect_success 'checkout chooses branch over tag' '
@@ -41,7 +41,7 @@
 '
 
 test_expect_success VAGUENESS_SUCCESS 'checkout produces ambiguity warning' '
-	grep "warning.*ambiguous" stderr
+	test_grep "warning.*ambiguous" stderr
 '
 
 test_expect_success VAGUENESS_SUCCESS 'checkout chooses branch over tag' '
diff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh
index a3b1449..752e7f0 100755
--- a/t/t2024-checkout-dwim.sh
+++ b/t/t2024-checkout-dwim.sh
@@ -311,7 +311,7 @@
 	git checkout loose >actual.raw 2>&1 &&
 	sed -e "s/loose/BRANCHNAME/g" <actual.raw >actual &&
 	status_uno_is_clean &&
-	grep BRANCHNAME actual &&
+	test_grep BRANCHNAME actual &&
 
 	test_cmp expect actual
 '
@@ -324,7 +324,7 @@
 	echo bar >dwim-arg &&
 	test_must_fail git checkout dwim-arg &&
 	test_must_fail git rev-parse refs/heads/dwim-arg -- &&
-	grep bar dwim-arg
+	test_grep bar dwim-arg
 '
 
 test_expect_success 'disambiguate dwim branch and checkout path (1)' '
@@ -334,7 +334,7 @@
 	echo bar >dwim-arg1 &&
 	git checkout -- dwim-arg1 &&
 	test_must_fail git rev-parse refs/heads/dwim-arg1 -- &&
-	grep foo dwim-arg1
+	test_grep foo dwim-arg1
 '
 
 test_expect_success 'disambiguate dwim branch and checkout path (2)' '
@@ -344,7 +344,7 @@
 	echo bar >dwim-arg2 &&
 	git checkout dwim-arg2 -- &&
 	git rev-parse refs/heads/dwim-arg2 -- &&
-	grep bar dwim-arg2
+	test_grep bar dwim-arg2
 '
 
 test_done
diff --git a/t/t2030-unresolve-info.sh b/t/t2030-unresolve-info.sh
index be3fcdd..9af24b1 100755
--- a/t/t2030-unresolve-info.sh
+++ b/t/t2030-unresolve-info.sh
@@ -122,7 +122,7 @@
 	check_resolve_undo removed &&
 	echo the index and the work tree is unmerged again &&
 	git diff >actual &&
-	grep "^++<<<<<<<" actual
+	test_grep "^++<<<<<<<" actual
 '
 
 test_expect_success 'unmerge with plumbing' '
@@ -173,7 +173,7 @@
 	test -f .git/rr-cache/$rerere_id/postimage &&
 	git checkout -m fi/le &&
 	echo resurrect the conflict &&
-	grep "^=======" fi/le &&
+	test_grep "^=======" fi/le &&
 	echo reresolve the conflict &&
 	git rerere &&
 	test "z$(cat fi/le)" = zdifferent &&
@@ -199,7 +199,7 @@
 	test -f .git/rr-cache/$rerere_id/postimage &&
 	(cd fi && git checkout -m le) &&
 	echo resurrect the conflict &&
-	grep "^=======" fi/le &&
+	test_grep "^=======" fi/le &&
 	echo reresolve the conflict &&
 	(cd fi && git rerere) &&
 	test "z$(cat fi/le)" = zdifferent &&
diff --git a/t/t2060-switch.sh b/t/t2060-switch.sh
index c91c4db..cec2fe6 100755
--- a/t/t2060-switch.sh
+++ b/t/t2060-switch.sh
@@ -34,13 +34,13 @@
 
 test_expect_success 'suggestion to detach' '
 	test_must_fail git switch main^{commit} 2>stderr &&
-	grep "try again with the --detach option" stderr
+	test_grep "try again with the --detach option" stderr
 '
 
 test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '
 	test_config advice.suggestDetachingHead false &&
 	test_must_fail git switch main^{commit} 2>stderr &&
-	! grep "try again with the --detach option" stderr
+	test_grep ! "try again with the --detach option" stderr
 '
 
 test_expect_success 'switch and detach current branch' '
@@ -76,7 +76,7 @@
 	git switch --orphan new-orphan &&
 	test_commit orphan &&
 	git cat-file commit refs/heads/new-orphan >commit &&
-	! grep ^parent commit &&
+	test_grep ! ^parent commit &&
 	git ls-files >tracked-files &&
 	echo orphan.t >expected &&
 	test_cmp expected tracked-files
diff --git a/t/t2070-restore.sh b/t/t2070-restore.sh
index 16d6348..2c222fb 100755
--- a/t/t2070-restore.sh
+++ b/t/t2070-restore.sh
@@ -216,7 +216,7 @@
 		"--staged --worktree --conflict=zdiff3"
 	do
 		test_must_fail git restore $opts . 2>err &&
-		grep "cannot be used" err || return
+		test_grep "cannot be used" err || return
 	done
 '
 
diff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh
index 5ffe1a4..88a6ec1 100755
--- a/t/t2080-parallel-checkout-basics.sh
+++ b/t/t2080-parallel-checkout-basics.sh
@@ -200,13 +200,13 @@
 		# We expect 0 workers because there is nothing to be done
 		test_checkout_workers 0 git checkout HEAD &&
 		test_path_is_file D &&
-		grep changed D &&
-		grep changed F.t &&
+		test_grep changed D &&
+		test_grep changed F.t &&
 
 		test_checkout_workers 2 git checkout --force HEAD &&
 		test_path_is_dir D &&
-		grep D/F D/F.t &&
-		grep F F.t
+		test_grep D/F D/F.t &&
+		test_grep F F.t
 	)
 '
 
@@ -224,8 +224,8 @@
 
 		test_checkout_workers 2 git checkout --force HEAD &&
 		! test -h D &&
-		grep D/A D/A.t &&
-		grep D/B D/B.t
+		test_grep D/A D/A.t &&
+		test_grep D/B D/B.t
 	)
 '
 
@@ -268,7 +268,7 @@
 		#  - missing-delay.a: the delay filter will drop this path
 		#  - parallel-*.a: the blob will be missing
 		#
-		grep "Updated 3 paths from the index" err &&
+		test_grep "Updated 3 paths from the index" err &&
 		test_stdout_line_count = 3 ls *.b &&
 		! ls *.a
 	)
diff --git a/t/t2081-parallel-checkout-collisions.sh b/t/t2081-parallel-checkout-collisions.sh
index f6fcfc0..db45f5f 100755
--- a/t/t2081-parallel-checkout-collisions.sh
+++ b/t/t2081-parallel-checkout-collisions.sh
@@ -78,8 +78,8 @@
 
 	# Check that it used the right number of workers and detected the collisions
 	test_workers_in_event_trace 2 trace &&
-	grep "category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}" trace &&
-	grep "category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}" trace
+	test_grep "category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}" trace &&
+	test_grep "category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}" trace
 '
 
 test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'do not follow symlinks colliding with leading dir' '
@@ -115,11 +115,11 @@
 	set_checkout_config 2 0 &&
 	test_checkout_workers 2 git clone . clone-repo 2>stderr &&
 
-	grep FILE_X stderr &&
-	grep FILE_x stderr &&
-	grep file_X stderr &&
-	grep file_x stderr &&
-	grep "the following paths have collided" stderr
+	test_grep FILE_X stderr &&
+	test_grep FILE_x stderr &&
+	test_grep file_X stderr &&
+	test_grep file_x stderr &&
+	test_grep "the following paths have collided" stderr
 '
 
 # This test ensures that the collision report code is correctly looking for
@@ -148,11 +148,11 @@
 	test_checkout_workers 2 \
 		git -c core.ignoreCase=false clone . clone-repo 2>stderr &&
 
-	grep FILE_X stderr &&
-	grep FILE_x stderr &&
-	grep file_X stderr &&
-	grep file_x stderr &&
-	grep "the following paths have collided" stderr &&
+	test_grep FILE_X stderr &&
+	test_grep FILE_x stderr &&
+	test_grep file_X stderr &&
+	test_grep file_x stderr &&
+	test_grep "the following paths have collided" stderr &&
 
 	# Check that only "file_x" was filtered
 	echo file_x >expected.log &&
diff --git a/t/t2082-parallel-checkout-attributes.sh b/t/t2082-parallel-checkout-attributes.sh
index 79fb11f..1f43c2a 100755
--- a/t/t2082-parallel-checkout-attributes.sh
+++ b/t/t2082-parallel-checkout-attributes.sh
@@ -28,8 +28,8 @@
 		rm A B &&
 		test_checkout_workers 2 git reset --hard &&
 		hexsz=$(test_oid hexsz) &&
-		grep -E "\\\$Id: [0-9a-f]{$hexsz} \\\$" A &&
-		grep "\\\$Id\\\$" B
+		test_grep -E "\\\$Id: [0-9a-f]{$hexsz} \\\$" A &&
+		test_grep "\\\$Id\\\$" B
 	)
 '
 
@@ -175,15 +175,15 @@
 	verify_checkout delayed &&
 
 	# Check that the *.d files got to the delay queue and were filtered
-	grep "smudge W.d .* \[DELAYED\]" delayed.log &&
-	grep "smudge X.d .* \[DELAYED\]" delayed.log &&
+	test_grep "smudge W.d .* \[DELAYED\]" delayed.log &&
+	test_grep "smudge X.d .* \[DELAYED\]" delayed.log &&
 	test_cmp delayed/W.d original &&
 	test_cmp delayed/X.d original &&
 
 	# Check that the parallel-eligible entries went to the right queue and
 	# were not filtered
-	! grep "smudge Y .* \[DELAYED\]" delayed.log &&
-	! grep "smudge Z .* \[DELAYED\]" delayed.log &&
+	test_grep ! "smudge Y .* \[DELAYED\]" delayed.log &&
+	test_grep ! "smudge Z .* \[DELAYED\]" delayed.log &&
 	test_cmp delayed/Y original &&
 	test_cmp delayed/Z original
 '
diff --git a/t/t2103-update-index-ignore-missing.sh b/t/t2103-update-index-ignore-missing.sh
index 6938ecc..fbfc86b 100755
--- a/t/t2103-update-index-ignore-missing.sh
+++ b/t/t2103-update-index-ignore-missing.sh
@@ -63,9 +63,9 @@
 	git update-index --unmerged --refresh &&
 	echo 2 >two &&
 	test_must_fail git update-index --unmerged --refresh >actual &&
-	grep two actual &&
-	! grep one actual &&
-	! grep three actual
+	test_grep two actual &&
+	test_grep ! one actual &&
+	test_grep ! three actual
 '
 
 test_expect_success '--ignore-submodules --refresh (1)' '
diff --git a/t/t2107-update-index-basic.sh b/t/t2107-update-index-basic.sh
index 3bffe5d..0048783 100755
--- a/t/t2107-update-index-basic.sh
+++ b/t/t2107-update-index-basic.sh
@@ -23,7 +23,7 @@
 		cd broken &&
 		git init &&
 		>.git/index &&
-		test_expect_code 129 git update-index -h >usage 2>&1
+		git update-index -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage: git update-index" broken/usage
 '
diff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh
index 0a96655..bb019d7 100755
--- a/t/t2200-add-update.sh
+++ b/t/t2200-add-update.sh
@@ -241,7 +241,7 @@
 test_expect_success '"add -u non-existent" should fail' '
 	test_must_fail git add -u non-existent &&
 	git ls-files >actual &&
-	! grep "non-existent" actual
+	test_grep ! "non-existent" actual
 '
 
 test_expect_success '"commit -a" implies "add -u" if index becomes empty' '
diff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh
index 44c1936..69fef7e 100755
--- a/t/t2203-add-intent.sh
+++ b/t/t2203-add-intent.sh
@@ -57,7 +57,7 @@
 	git add -N file elif &&
 	empty=$(git hash-object --stdin </dev/null) &&
 	git ls-files -s >actual &&
-	! grep "$empty" actual
+	test_grep ! "$empty" actual
 '
 
 test_expect_success 'i-t-a entry is simply ignored' '
@@ -124,7 +124,7 @@
 		git add -N 2/1 &&
 		git commit -m committed &&
 		git ls-tree -r HEAD >actual &&
-		grep 2/2 actual
+		test_grep 2/2 actual
 	)
 '
 
@@ -312,7 +312,7 @@
 	echo new >new-ita &&
 	git add -N new-ita &&
 	git diff >expected &&
-	grep "new file" expected &&
+	test_grep "new file" expected &&
 	git reset --hard &&
 	git apply --intent-to-add expected &&
 	git diff >actual &&
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index 58b4445..87b9267 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -122,7 +122,7 @@
 	(
 		cd here &&
 		test_must_fail git checkout newmain 2>actual &&
-		grep "already used by worktree at" actual
+		test_grep "already used by worktree at" actual
 	)
 '
 
@@ -139,7 +139,7 @@
 		git rev-parse --verify refs/heads/newmain >new.branch &&
 		git rev-parse --verify HEAD >new.head &&
 
-		grep "already used by worktree at" error &&
+		test_grep "already used by worktree at" error &&
 		test_cmp old.branch new.branch &&
 		test_cmp old.head new.head &&
 
@@ -328,7 +328,7 @@
 	local opts="$*" &&
 	test_expect_success "'worktree add' with '$opts' has mutually exclusive options" '
 		test_must_fail git worktree add $opts 2>actual &&
-		grep -E "fatal:( options)? .* cannot be used together" actual
+		test_grep -E "fatal:( options)? .* cannot be used together" actual
 	'
 }
 
@@ -436,13 +436,13 @@
 		(cd repo && test_commit commit) &&
 		git -C repo switch --orphan noref &&
 		test_must_fail git -C repo worktree add $opts foobar/ 2>actual &&
-		! grep "error: unknown switch" actual &&
-		grep "hint: If you meant to create a worktree containing a new unborn branch" actual &&
+		test_grep ! "error: unknown switch" actual &&
+		test_grep "hint: If you meant to create a worktree containing a new unborn branch" actual &&
 		if [ $use_branch -eq 1 ]
 		then
-			grep -E "^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$" actual
+			test_grep -E "^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$" actual
 		else
-			grep -E "^hint: +git worktree add --orphan [^ ]+$" actual
+			test_grep -E "^hint: +git worktree add --orphan [^ ]+$" actual
 		fi
 
 	'
@@ -457,8 +457,8 @@
 	git init repo &&
 	(cd repo && test_commit commit) &&
 	test_must_fail git -C repo worktree add --quiet foobar_branch foobar/ 2>actual &&
-	! grep "error: unknown switch" actual &&
-	! grep "hint: If you meant to create a worktree containing a new unborn branch" actual
+	test_grep ! "error: unknown switch" actual &&
+	test_grep ! "hint: If you meant to create a worktree containing a new unborn branch" actual
 '
 
 test_expect_success 'local clone from linked checkout' '
@@ -469,7 +469,7 @@
 test_expect_success 'local clone --shared from linked checkout' '
 	git -C bare worktree add --detach ../baretree &&
 	git clone --local --shared baretree bare-clone &&
-	grep /bare/ bare-clone/.git/objects/info/alternates
+	test_grep /bare/ bare-clone/.git/objects/info/alternates
 '
 
 test_expect_success '"add" worktree with --no-checkout' '
@@ -491,7 +491,7 @@
 		set_fake_editor &&
 		FAKE_LINES="edit 1" git rebase -i HEAD^ &&
 		git worktree list >actual &&
-		grep "under-rebase.*detached HEAD" actual
+		test_grep "under-rebase.*detached HEAD" actual
 	)
 '
 
@@ -533,7 +533,7 @@
 		git bisect bad &&
 		git bisect good HEAD~2 &&
 		git worktree list >actual &&
-		grep "under-bisect.*detached HEAD" actual &&
+		test_grep "under-bisect.*detached HEAD" actual &&
 		test_must_fail git worktree add new-bisect under-bisect &&
 		test_path_is_missing new-bisect
 	)
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index e0c6abd..eeaf800 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -101,8 +101,8 @@
 	git worktree lock locked &&
 	test_when_finished "git worktree unlock locked" &&
 	git worktree list >out &&
-	grep "/locked  *[0-9a-f].* locked$" out &&
-	! grep "/unlocked  *[0-9a-f].* locked$" out
+	test_grep "/locked  *[0-9a-f].* locked$" out &&
+	test_grep ! "/unlocked  *[0-9a-f].* locked$" out
 '
 
 test_expect_success '"list" all worktrees --porcelain with locked' '
@@ -143,8 +143,8 @@
 	git worktree add --detach unprunable &&
 	rm -rf prunable &&
 	git worktree list >out &&
-	grep "/prunable  *[0-9a-f].* prunable$" out &&
-	! grep "/unprunable  *[0-9a-f].* prunable$"
+	test_grep "/prunable  *[0-9a-f].* prunable$" out &&
+	test_grep ! "/unprunable  *[0-9a-f].* prunable$" out
 '
 
 test_expect_success '"list" all worktrees --porcelain with prunable' '
@@ -162,8 +162,8 @@
 	git worktree add --detach unprunable &&
 	rm -rf prunable &&
 	git worktree list >out &&
-	grep "/prunable  *[0-9a-f].* prunable$" out &&
-	! grep "/unprunable  *[0-9a-f].* unprunable$" out &&
+	test_grep "/prunable  *[0-9a-f].* prunable$" out &&
+	test_grep ! "/unprunable  *[0-9a-f].* unprunable$" out &&
 	git worktree prune --verbose 2>out &&
 	test_grep "^Removing worktrees/prunable" out &&
 	test_grep ! "^Removing worktrees/unprunable" out
@@ -184,7 +184,7 @@
 	echo "$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >expect &&
 	printf "\tlocked: with reason\n" >>expect &&
 	git worktree list --verbose >out &&
-	grep "/locked1  *[0-9a-f].* locked$" out &&
+	test_grep "/locked1  *[0-9a-f].* locked$" out &&
 	sed -n "s/  */ /g;/\/locked2  *[0-9a-f].*$/,/locked: .*$/p" <out >actual &&
 	test_cmp actual expect
 '
@@ -266,7 +266,7 @@
 		test_cmp ../expected actual &&
 		git worktree list >out &&
 		head -n 1 out >actual.2 &&
-		grep -F "(error)" actual.2
+		test_grep -F "(error)" actual.2
 	)
 '
 
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 0bb33e8..69768c1 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -75,8 +75,8 @@
 	git worktree move source destination &&
 	test_path_is_missing source &&
 	git worktree list --porcelain >out &&
-	grep "^worktree.*/destination$" out &&
-	! grep "^worktree.*/source$" out &&
+	test_grep "^worktree.*/destination$" out &&
+	test_grep ! "^worktree.*/source$" out &&
 	git -C destination log --format=%s >actual2 &&
 	echo init >expected2 &&
 	test_cmp expected2 actual2
@@ -92,7 +92,7 @@
 	test_when_finished "git worktree move some-dir/destination destination" &&
 	test_path_is_missing destination &&
 	git worktree list --porcelain >out &&
-	grep "^worktree.*/some-dir/destination$" out &&
+	test_grep "^worktree.*/some-dir/destination$" out &&
 	git -C some-dir/destination log --format=%s >actual2 &&
 	echo init >expected2 &&
 	test_cmp expected2 actual2
diff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh
index 11018f3..f5c94d4 100755
--- a/t/t2405-worktree-submodule.sh
+++ b/t/t2405-worktree-submodule.sh
@@ -36,7 +36,7 @@
 
 test_expect_failure 'submodule is checked out just after worktree add' '
 	git -C worktree diff --submodule main"^!" >out &&
-	grep "file1 updated" out
+	test_grep "file1 updated" out
 '
 
 test_expect_success 'add superproject worktree and initialize submodules' '
@@ -46,7 +46,7 @@
 
 test_expect_success 'submodule is checked out just after submodule update in linked worktree' '
 	git -C worktree-submodule-update diff --submodule main"^!" >out &&
-	grep "file1 updated" out
+	test_grep "file1 updated" out
 '
 
 test_expect_success 'add superproject worktree and manually add submodule worktree' '
@@ -56,7 +56,7 @@
 
 test_expect_success 'submodule is checked out after manually adding submodule worktree' '
 	git -C linked_submodule diff --submodule main"^!" >out &&
-	grep "file1 updated" out
+	test_grep "file1 updated" out
 '
 
 test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '
diff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh
index 57c2018..a6d7e0f 100755
--- a/t/t2407-worktree-heads.sh
+++ b/t/t2407-worktree-heads.sh
@@ -41,10 +41,10 @@
 	for i in 1 2 3 4
 	do
 		test_must_fail git branch -f wt-$i HEAD 2>err &&
-		grep "cannot force update the branch" err &&
+		test_grep "cannot force update the branch" err &&
 
 		test_must_fail git branch -D wt-$i 2>err &&
-		grep "cannot delete branch" err || return 1
+		test_grep "cannot delete branch" err || return 1
 	done
 '
 
@@ -57,7 +57,7 @@
 	git -C wt-4 bisect good wt-1 &&
 
 	test_must_fail git branch -f wt-4 HEAD 2>err &&
-	grep "cannot force update the branch '\''wt-4'\'' used by worktree at.*wt-4" err
+	test_grep "cannot force update the branch '\''wt-4'\'' used by worktree at.*wt-4" err
 '
 
 test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '
@@ -67,7 +67,7 @@
 	test_must_fail git -C wt-2 rebase --apply conflict-2 &&
 
 	test_must_fail git branch -f wt-2 HEAD 2>err &&
-	grep "cannot force update the branch '\''wt-2'\'' used by worktree at.*wt-2" err
+	test_grep "cannot force update the branch '\''wt-2'\'' used by worktree at.*wt-2" err
 '
 
 test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '
@@ -77,7 +77,7 @@
 	test_must_fail git -C wt-2 rebase conflict-2 &&
 
 	test_must_fail git branch -f wt-2 HEAD 2>err &&
-	grep "cannot force update the branch '\''wt-2'\'' used by worktree at.*wt-2" err
+	test_grep "cannot force update the branch '\''wt-2'\'' used by worktree at.*wt-2" err
 '
 
 test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs' '
@@ -89,19 +89,19 @@
 	for i in 3 4
 	do
 		test_must_fail git branch -f can-be-updated HEAD 2>err &&
-		grep "cannot force update the branch '\''can-be-updated'\'' used by worktree at.*wt-3" err ||
+		test_grep "cannot force update the branch '\''can-be-updated'\'' used by worktree at.*wt-3" err ||
 			return 1
 	done
 '
 
 test_expect_success 'refuse to fetch over ref: checked out' '
 	test_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&
-	grep "refusing to fetch into branch '\''refs/heads/wt-3'\''" err &&
+	test_grep "refusing to fetch into branch '\''refs/heads/wt-3'\''" err &&
 
 	# General fetch into refs/heads/ will fail on first ref,
 	# so use a generic error message check.
 	test_must_fail git fetch server +refs/heads/*:refs/heads/* 2>err &&
-	grep "refusing to fetch into branch" err
+	test_grep "refusing to fetch into branch" err
 '
 
 test_expect_success 'refuse to fetch over ref: worktree in bisect' '
@@ -113,7 +113,7 @@
 	git -C wt-4 bisect good wt-1 &&
 
 	test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err &&
-	grep "refusing to fetch into branch" err
+	test_grep "refusing to fetch into branch" err
 '
 
 test_expect_success 'refuse to fetch over ref: worktree in rebase' '
@@ -123,7 +123,7 @@
 	test_must_fail git -C wt-3 rebase conflict-3 &&
 
 	test_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&
-	grep "refusing to fetch into branch" err
+	test_grep "refusing to fetch into branch" err
 '
 
 test_expect_success 'refuse to overwrite when in error states' '
@@ -149,7 +149,7 @@
 	for i in 1 2
 	do
 		test_must_fail git branch -f fake-$i HEAD 2>err &&
-		grep "cannot force update the branch '\''fake-$i'\'' used by worktree at" err ||
+		test_grep "cannot force update the branch '\''fake-$i'\'' used by worktree at" err ||
 			return 1
 	done
 '
@@ -161,13 +161,13 @@
 	(
 		set_cat_todo_editor &&
 		test_must_fail git rebase -i --update-refs HEAD~3 >todo &&
-		! grep "update-refs" todo
+		test_grep ! "update-refs" todo
 	) &&
 	git branch -f allow-update HEAD~2 &&
 	(
 		set_cat_todo_editor &&
 		test_must_fail git rebase -i --update-refs HEAD~3 >todo &&
-		grep "update-ref refs/heads/allow-update" todo
+		test_grep "update-ref refs/heads/allow-update" todo
 	)
 '
 
diff --git a/t/t2500-untracked-overwriting.sh b/t/t2500-untracked-overwriting.sh
index 5c0bf4d..8e84f29 100755
--- a/t/t2500-untracked-overwriting.sh
+++ b/t/t2500-untracked-overwriting.sh
@@ -51,7 +51,7 @@
 
 		test_must_fail git reset --merge work 2>error &&
 		test_cmp expect foo.t/file &&
-		grep "Updating .foo.t. would lose untracked files" error
+		test_grep "Updating .foo.t. would lose untracked files" error
 	)
 '
 
@@ -66,7 +66,7 @@
 
 		test_must_fail git reset --merge work 2>error &&
 		test_cmp expect foo.t/file &&
-		grep "Updating.*foo.t.*would lose untracked files" error
+		test_grep "Updating.*foo.t.*would lose untracked files" error
 	)
 '
 
@@ -214,7 +214,7 @@
 
 		test_must_fail git am --abort 2>errors &&
 		test_path_is_dir filler &&
-		grep "Updating .filler. would lose untracked files in it" errors
+		test_grep "Updating .filler. would lose untracked files in it" errors
 	)
 '
 
@@ -237,7 +237,7 @@
 		# Change our mind about resolutions, just skip this patch
 		test_must_fail git am --skip 2>errors &&
 		test_path_is_dir newfile &&
-		grep "Updating .newfile. would lose untracked files in it" errors
+		test_grep "Updating .newfile. would lose untracked files in it" errors
 	)
 '
 
diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh
index be9140b..eb6451d 100755
--- a/t/t2501-cwd-empty.sh
+++ b/t/t2501-cwd-empty.sh
@@ -114,7 +114,7 @@
 	) &&
 
 	test_path_is_dir dirORfile &&
-	grep "Refusing to remove the current working directory" error
+	test_grep "Refusing to remove the current working directory" error
 '
 
 test_expect_success 'merge fails if cwd needs to be removed' '
@@ -179,7 +179,7 @@
 test_expect_success 'clean does not remove cwd incidentally' '
 	test_incidental_untracked_dir_removal \
 		git -C .. clean -fd -e warnings . >warnings &&
-	grep "Refusing to remove current working directory" warnings
+	test_grep "Refusing to remove current working directory" warnings
 '
 
 test_expect_success 'stash does not remove cwd incidentally' '
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
index 202fb8d..29a0a25 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -161,21 +161,21 @@
 
 	>two &&
 	git ls-files --others --exclude=two/ >output &&
-	grep "^two" output
+	test_grep "^two" output
 
 '
 
 test_expect_success 'trailing slash in exclude forces directory match (2)' '
 
 	git ls-files --others --exclude=one/a.1/ >output &&
-	grep "^one/a.1" output
+	test_grep "^one/a.1" output
 
 '
 
 test_expect_success 'negated exclude matches can override previous ones' '
 
 	git ls-files --others --exclude="a.*" --exclude="!a.1" >output &&
-	grep "^a.1" output
+	test_grep "^a.1" output
 '
 
 test_expect_success 'excluded directory overrides content patterns' '
diff --git a/t/t3004-ls-files-basic.sh b/t/t3004-ls-files-basic.sh
index 4034a5a..c57afcb 100755
--- a/t/t3004-ls-files-basic.sh
+++ b/t/t3004-ls-files-basic.sh
@@ -29,15 +29,15 @@
 		cd broken &&
 		git init &&
 		>.git/index &&
-		test_expect_code 129 git ls-files -h >usage 2>&1
+		git ls-files -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage: git ls-files " broken/usage
 '
 
 test_expect_success 'ls-files does not crash with -h' '
-	test_expect_code 129 git ls-files -h >usage &&
+	git ls-files -h >usage &&
 	test_grep "[Uu]sage: git ls-files " usage &&
-	test_expect_code 129 nongit git ls-files -h >usage &&
+	nongit git ls-files -h >usage &&
 	test_grep "[Uu]sage: git ls-files " usage
 '
 
diff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh
index 61771ee..218964e 100755
--- a/t/t3007-ls-files-recurse-submodules.sh
+++ b/t/t3007-ls-files-recurse-submodules.sh
@@ -302,7 +302,7 @@
 test_expect_success '--recurse-submodules parses submodule repo config' '
 	test_config -C submodule index.sparse "invalid non-boolean value" &&
 	test_must_fail git ls-files --recurse-submodules 2>err &&
-	grep "bad boolean config value" err
+	test_grep "bad boolean config value" err
 '
 
 test_expect_success '--recurse-submodules parses submodule worktree config' '
@@ -310,7 +310,7 @@
 	test_config -C submodule --worktree index.sparse "invalid non-boolean value" &&
 
 	test_must_fail git ls-files --recurse-submodules 2>err &&
-	grep "bad boolean config value" err
+	test_grep "bad boolean config value" err
 '
 
 test_expect_success '--recurse-submodules submodules ignore super project worktreeConfig extension' '
@@ -329,7 +329,7 @@
 	# With extensions.worktreeConfig disabled in the submodule, the invalid
 	# worktree config is not picked up.
 	git ls-files --recurse-submodules 2>err &&
-	! grep "bad boolean config value" err
+	test_grep ! "bad boolean config value" err
 '
 
 test_incompatible_with_recurse_submodules () {
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index e7829c2..7f7807e 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -33,7 +33,7 @@
 		cd broken &&
 		git init -b main &&
 		>.git/refs/heads/main &&
-		test_expect_code 129 git branch -h >usage 2>&1
+		git branch -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage" broken/usage
 '
@@ -204,7 +204,7 @@
 
 test_expect_success 'git branch -M baz bam should add entries to HEAD reflog' '
 	git reflog show HEAD >actual &&
-	grep "HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam" actual
+	test_grep "HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam" actual
 '
 
 test_expect_success 'git branch -M should leave orphaned HEAD alone' '
@@ -339,7 +339,7 @@
 	test_when_finished "git branch -D to-delete" &&
 	git branch to-delete main &&
 	test_must_fail git branch -d to-delete 2>err &&
-	grep "not fully merged" err
+	test_grep "not fully merged" err
 '
 
 test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '
@@ -350,7 +350,7 @@
 	test_when_finished "rm -rf .git/objects/commit-graph*" &&
 	git commit-graph write --reachable &&
 	test_must_fail git branch -d to-delete 2>err &&
-	grep "not fully merged" err
+	test_grep "not fully merged" err
 '
 
 test_expect_success 'git branch -v -d t should work' '
@@ -712,7 +712,7 @@
 test_expect_success 'git branch -C c1 c2 should never touch HEAD' '
 	msg="Branch: copied refs/heads/c1 to refs/heads/c2" &&
 	git reflog HEAD >actual &&
-	! grep "$msg$" actual
+	test_grep ! "$msg$" actual
 '
 
 test_expect_success 'git branch -C main should work when main is checked out' '
@@ -930,7 +930,7 @@
 	git worktree add -b my7 my7 &&
 	test_must_fail git -C my7 branch -d my7 &&
 	test_must_fail git branch -d my7 2>actual &&
-	grep "^error: cannot delete branch .my7. used by worktree at " actual &&
+	test_grep "^error: cannot delete branch .my7. used by worktree at " actual &&
 	rm -r my7 &&
 	git worktree prune
 '
@@ -941,7 +941,7 @@
 	git -C my7 bisect start HEAD HEAD~2 &&
 	test_must_fail git -C my7 branch -d my7 &&
 	test_must_fail git branch -d my7 2>actual &&
-	grep "^error: cannot delete branch .my7. used by worktree at " actual &&
+	test_grep "^error: cannot delete branch .my7. used by worktree at " actual &&
 	rm -r my7 &&
 	git worktree prune
 '
@@ -1022,6 +1022,44 @@
 	test_cmp expect err
 '
 
+test_expect_success '--set-upstream-to suggests <remote>/<branch> on slip' '
+	test_when_finished "git remote remove slip-remote" &&
+	git remote add slip-remote . &&
+	git update-ref refs/remotes/slip-remote/slip-feature HEAD &&
+	test_must_fail git branch --set-upstream-to slip-remote slip-feature 2>err &&
+	test_grep "takes a single <remote>/<branch> argument" err &&
+	test_grep "hint: Did you mean to use: git branch --set-upstream-to=slip-remote/slip-feature?" err &&
+	test_must_fail git -c advice.setUpstreamFailure=false \
+		branch --set-upstream-to slip-remote slip-feature 2>err &&
+	test_grep ! "Did you mean" err
+'
+
+test_expect_success '--set-upstream-to does not suggest when no matching remote ref' '
+	test_when_finished "git remote remove slip-remote" &&
+	git remote add slip-remote . &&
+	test_must_fail git branch --set-upstream-to slip-remote no-such-branch 2>err &&
+	test_grep "branch ${SQ}no-such-branch${SQ} does not exist" err &&
+	test_grep ! "Did you mean" err
+'
+
+test_expect_success '--set-upstream-to to a local branch is not mistaken for a slip' '
+	git branch slip-local-upstream &&
+	git branch slip-local-target &&
+	git branch --set-upstream-to=slip-local-upstream slip-local-target 2>err &&
+	test_grep ! "Did you mean" err &&
+	echo refs/heads/slip-local-upstream >expect &&
+	git config branch.slip-local-target.merge >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '--set-upstream-to slip suggestion keeps a slashed branch name' '
+	test_when_finished "git remote remove slip-remote" &&
+	git remote add slip-remote . &&
+	git update-ref refs/remotes/slip-remote/slip/feature HEAD &&
+	test_must_fail git branch --set-upstream-to slip-remote slip/feature 2>err &&
+	test_grep "hint: Did you mean to use: git branch --set-upstream-to=slip-remote/slip/feature?" err
+'
+
 test_expect_success '--set-upstream-to fails on a missing src branch' '
 	test_must_fail git branch --set-upstream-to does-not-exist main 2>err &&
 	test_grep "the requested upstream branch '"'"'does-not-exist'"'"' does not exist" err
@@ -1037,7 +1075,8 @@
 	test_when_finished "rm -f .git/config.lock" &&
 	>.git/config.lock &&
 	git branch locked &&
-	test_must_fail git branch --set-upstream-to locked 2>err &&
+	test_must_fail git -c core.configLockTimeout=0 \
+		branch --set-upstream-to locked 2>err &&
 	test_grep "could not lock config file .git/config" err
 '
 
@@ -1068,7 +1107,8 @@
 	test_when_finished "rm -f .git/config.lock" &&
 	git branch --set-upstream-to locked &&
 	>.git/config.lock &&
-	test_must_fail git branch --unset-upstream 2>err &&
+	test_must_fail git -c core.configLockTimeout=0 \
+		branch --unset-upstream 2>err &&
 	test_grep "could not lock config file .git/config" err
 '
 
diff --git a/t/t3202-show-branch.sh b/t/t3202-show-branch.sh
index a1139f7..f402334 100755
--- a/t/t3202-show-branch.sh
+++ b/t/t3202-show-branch.sh
@@ -123,13 +123,13 @@
 	git commit --allow-empty -m "another" &&
 
 	git show-branch --sparse >out &&
-	grep "merge 1 and 10 to make A" out &&
+	test_grep "merge 1 and 10 to make A" out &&
 
 	git show-branch >out &&
-	! grep "merge 1 and 10 to make A" out &&
+	test_grep ! "merge 1 and 10 to make A" out &&
 
 	git show-branch --no-sparse >out &&
-	! grep "merge 1 and 10 to make A" out
+	test_grep ! "merge 1 and 10 to make A" out
 '
 
 test_expect_success 'setup show branch --list' '
@@ -189,7 +189,7 @@
 do
 	test_expect_success "show-branch $combo (should fail)" '
 		test_must_fail git show-branch $combo 2>error &&
-		grep -e "cannot be used together" -e "usage:" error
+		test_grep -e "cannot be used together" -e "usage:" error
 	'
 done <<\EOF
 --all --reflog
@@ -203,7 +203,7 @@
 do
 	test_expect_success "show-branch --no-$opt (should fail)" '
 		test_must_fail git show-branch --no-$opt 2>err &&
-		grep "unknown option .no-$opt." err
+		test_grep "unknown option .no-$opt." err
 	'
 done
 
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index a6bd88a..7c44186 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -64,7 +64,7 @@
 
 test_expect_success 'git branch --no-remotes is rejected' '
 	test_must_fail git branch --no-remotes 2>err &&
-	grep "unknown option .no-remotes." err
+	test_grep "unknown option .no-remotes." err
 '
 
 cat >expect <<'EOF'
@@ -85,7 +85,7 @@
 
 test_expect_success 'git branch --no-all is rejected' '
 	test_must_fail git branch --no-all 2>err &&
-	grep "unknown option .no-all." err
+	test_grep "unknown option .no-all." err
 '
 
 cat >expect <<'EOF'
diff --git a/t/t3206-range-diff.sh b/t/t3206-range-diff.sh
index 1e812df..ef92704 100755
--- a/t/t3206-range-diff.sh
+++ b/t/t3206-range-diff.sh
@@ -538,10 +538,10 @@
 			main..unmodified >actual &&
 		test_line_count = 5 actual &&
 		test_grep "^Range-diff:$" 0000-* &&
-		grep "= 1: .* s/5/A" 0000-* &&
-		grep "= 2: .* s/4/A" 0000-* &&
-		grep "= 3: .* s/11/B" 0000-* &&
-		grep "= 4: .* s/12/B" 0000-*
+		test_grep "= 1: .* s/5/A" 0000-* &&
+		test_grep "= 2: .* s/4/A" 0000-* &&
+		test_grep "= 3: .* s/11/B" 0000-* &&
+		test_grep "= 4: .* s/12/B" 0000-*
 	'
 done
 
@@ -564,7 +564,7 @@
 	git format-patch --range-diff=HEAD~1 HEAD~1 >actual &&
 	test_line_count = 1 actual &&
 	test_grep "^Range-diff:$" 0001-* &&
-	grep "> 1: .* new message" 0001-*
+	test_grep "> 1: .* new message" 0001-*
 '
 
 test_expect_success 'format-patch --range-diff reroll-count with a non-integer' '
@@ -572,7 +572,7 @@
 	git format-patch --range-diff=HEAD~1 -v2.9 HEAD~1 >actual &&
 	test_line_count = 1 actual &&
 	test_grep "^Range-diff:$" v2.9-0001-* &&
-	grep "> 1: .* new message" v2.9-0001-*
+	test_grep "> 1: .* new message" v2.9-0001-*
 '
 
 test_expect_success 'format-patch --range-diff reroll-count with a integer' '
@@ -580,7 +580,7 @@
 	git format-patch --range-diff=HEAD~1 -v2 HEAD~1 >actual &&
 	test_line_count = 1 actual &&
 	test_grep "^Range-diff ..* v1:$" v2-0001-* &&
-	grep "> 1: .* new message" v2-0001-*
+	test_grep "> 1: .* new message" v2-0001-*
 '
 
 test_expect_success 'format-patch --range-diff with v0' '
@@ -588,7 +588,7 @@
 	git format-patch --range-diff=HEAD~1 -v0 HEAD~1 >actual &&
 	test_line_count = 1 actual &&
 	test_grep "^Range-diff:$" v0-0001-* &&
-	grep "> 1: .* new message" v0-0001-*
+	test_grep "> 1: .* new message" v0-0001-*
 '
 
 test_expect_success 'range-diff overrides diff.noprefix internally' '
@@ -686,8 +686,8 @@
 	git notes --ref=custom add -m "unmodified note" unmodified &&
 	git range-diff --notes=custom main..topic main..unmodified \
 		>actual &&
-	! grep "## Notes ##" actual &&
-	grep "## Notes (custom) ##" actual
+	test_grep ! "## Notes ##" actual &&
+	test_grep "## Notes (custom) ##" actual
 '
 
 test_expect_success 'format-patch --range-diff does not compare notes by default' '
@@ -699,12 +699,12 @@
 		main..unmodified >actual &&
 	test_line_count = 5 actual &&
 	test_grep "^Range-diff:$" 0000-* &&
-	grep "= 1: .* s/5/A" 0000-* &&
-	grep "= 2: .* s/4/A" 0000-* &&
-	grep "= 3: .* s/11/B" 0000-* &&
-	grep "= 4: .* s/12/B" 0000-* &&
-	! grep "Notes" 0000-* &&
-	! grep "note" 0000-*
+	test_grep "= 1: .* s/5/A" 0000-* &&
+	test_grep "= 2: .* s/4/A" 0000-* &&
+	test_grep "= 3: .* s/11/B" 0000-* &&
+	test_grep "= 4: .* s/12/B" 0000-* &&
+	test_grep ! "Notes" 0000-* &&
+	test_grep ! "note" 0000-*
 '
 
 test_expect_success 'format-patch --notes=custom --range-diff --cover-letter only compares custom notes' '
@@ -717,8 +717,8 @@
 	test_when_finished "rm -f 000?-*" &&
 	git format-patch --notes=custom --cover-letter --range-diff=$prev \
 		main..unmodified >actual &&
-	grep "## Notes (custom) ##" 0000-* &&
-	! grep "## Notes ##" 0000-*
+	test_grep "## Notes (custom) ##" 0000-* &&
+	test_grep ! "## Notes ##" 0000-*
 '
 
 # --range-diff on a single commit requires --no-cover-letter
@@ -744,12 +744,12 @@
 		main..unmodified >actual &&
 	test_line_count = 5 actual &&
 	test_grep "^Range-diff:$" 0000-* &&
-	grep "= 1: .* s/5/A" 0000-* &&
-	grep "= 2: .* s/4/A" 0000-* &&
-	grep "= 3: .* s/11/B" 0000-* &&
-	grep "= 4: .* s/12/B" 0000-* &&
-	! grep "Notes" 0000-* &&
-	! grep "note" 0000-*
+	test_grep "= 1: .* s/5/A" 0000-* &&
+	test_grep "= 2: .* s/4/A" 0000-* &&
+	test_grep "= 3: .* s/11/B" 0000-* &&
+	test_grep "= 4: .* s/12/B" 0000-* &&
+	test_grep ! "Notes" 0000-* &&
+	test_grep ! "note" 0000-*
 '
 
 test_expect_success 'format-patch --range-diff with --notes' '
@@ -761,10 +761,10 @@
 		main..unmodified >actual &&
 	test_line_count = 5 actual &&
 	test_grep "^Range-diff:$" 0000-* &&
-	grep "= 1: .* s/5/A" 0000-* &&
-	grep "= 2: .* s/4/A" 0000-* &&
-	grep "= 3: .* s/11/B" 0000-* &&
-	grep "! 4: .* s/12/B" 0000-* &&
+	test_grep "= 1: .* s/5/A" 0000-* &&
+	test_grep "= 2: .* s/4/A" 0000-* &&
+	test_grep "= 3: .* s/11/B" 0000-* &&
+	test_grep "! 4: .* s/12/B" 0000-* &&
 	sed s/Z/\ /g >expect <<-EOF &&
 	    @@ Commit message
 	    Z
@@ -790,10 +790,10 @@
 		main..unmodified >actual &&
 	test_line_count = 5 actual &&
 	test_grep "^Range-diff:$" 0000-* &&
-	grep "= 1: .* s/5/A" 0000-* &&
-	grep "= 2: .* s/4/A" 0000-* &&
-	grep "= 3: .* s/11/B" 0000-* &&
-	grep "! 4: .* s/12/B" 0000-* &&
+	test_grep "= 1: .* s/5/A" 0000-* &&
+	test_grep "= 2: .* s/4/A" 0000-* &&
+	test_grep "= 3: .* s/11/B" 0000-* &&
+	test_grep "! 4: .* s/12/B" 0000-* &&
 	sed s/Z/\ /g >expect <<-EOF &&
 	    @@ Commit message
 	    Z
@@ -821,10 +821,10 @@
 		main..unmodified >actual &&
 	test_line_count = 5 actual &&
 	test_grep "^Range-diff:$" 0000-* &&
-	grep "= 1: .* s/5/A" 0000-* &&
-	grep "= 2: .* s/4/A" 0000-* &&
-	grep "= 3: .* s/11/B" 0000-* &&
-	grep "! 4: .* s/12/B" 0000-* &&
+	test_grep "= 1: .* s/5/A" 0000-* &&
+	test_grep "= 2: .* s/4/A" 0000-* &&
+	test_grep "= 3: .* s/11/B" 0000-* &&
+	test_grep "! 4: .* s/12/B" 0000-* &&
 	sed s/Z/\ /g >expect <<-EOF &&
 	    @@ Commit message
 	    Z
@@ -866,9 +866,9 @@
 	topic_oid=$(git rev-parse --short topic) &&
 	mode_change_oid=$(git rev-parse --short mode-only-change^) &&
 	file_change_oid=$(git rev-parse --short mode-only-change) &&
-	grep "$mode_change_oid" actual &&
-	! grep "$file_change_oid" actual &&
-	! grep "$topic_oid" actual
+	test_grep "$mode_change_oid" actual &&
+	test_grep ! "$file_change_oid" actual &&
+	test_grep ! "$topic_oid" actual
 '
 
 test_expect_success 'submodule changes are shown irrespective of diff.submodule' '
diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh
index fe72b24..4afb2d3 100755
--- a/t/t3207-branch-submodule.sh
+++ b/t/t3207-branch-submodule.sh
@@ -136,7 +136,7 @@
 		git -C sub branch branch-a &&
 		test_must_fail git branch --recurse-submodules branch-a 2>actual &&
 		test_no_branch . branch-a &&
-		grep "submodule .sub.: fatal: a branch named .branch-a. already exists" actual
+		test_grep "submodule .sub.: fatal: a branch named .branch-a. already exists" actual
 	)
 '
 
@@ -251,7 +251,7 @@
 		git branch --recurse-submodules branch-a origin/branch-a &&
 		# This should fail because super-clone does not have sub2 .git/modules
 		test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual &&
-		grep "fatal: submodule .sub2.: unable to find submodule" actual &&
+		test_grep "fatal: submodule .sub2.: unable to find submodule" actual &&
 		test_no_branch . branch-b &&
 		test_no_branch sub branch-b &&
 		# User can fix themselves by initializing the submodule
diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index d6c5046..18a6000 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -164,7 +164,7 @@
 		${indent}b1
 	EOF
 	git cat-file commit HEAD >commits &&
-	! grep b1 commits &&
+	test_grep ! b1 commits &&
 	git log -1 >actual &&
 	test_cmp expect actual
 '
@@ -248,17 +248,17 @@
 
 test_expect_success 'git log --no-notes' '
 	git log -1 --no-notes >actual &&
-	! grep xyzzy actual
+	test_grep ! xyzzy actual
 '
 
 test_expect_success 'git format-patch does not show notes' '
 	git format-patch -1 --stdout >actual &&
-	! grep xyzzy actual
+	test_grep ! xyzzy actual
 '
 
 test_expect_success 'git format-patch --show-notes does show notes' '
 	git format-patch --show-notes -1 --stdout >actual &&
-	grep xyzzy actual
+	test_grep xyzzy actual
 '
 
 for pretty in \
@@ -281,36 +281,36 @@
 
 test_expect_success 'git log --notes shows default notes' '
 	git log -1 --notes >actual &&
-	grep xyzzy actual &&
-	! grep alternate actual
+	test_grep xyzzy actual &&
+	test_grep ! alternate actual
 '
 
 test_expect_success 'git log --notes=X shows only X' '
 	git log -1 --notes=alternate >actual &&
-	! grep xyzzy actual &&
-	grep alternate actual
+	test_grep ! xyzzy actual &&
+	test_grep alternate actual
 '
 
 test_expect_success 'git log --notes --notes=X shows both' '
 	git log -1 --notes --notes=alternate >actual &&
-	grep xyzzy actual &&
-	grep alternate actual
+	test_grep xyzzy actual &&
+	test_grep alternate actual
 '
 
 test_expect_success 'git log --no-notes resets default state' '
 	git log -1 --notes --notes=alternate \
 		--no-notes --notes=alternate \
 		>actual &&
-	! grep xyzzy actual &&
-	grep alternate actual
+	test_grep ! xyzzy actual &&
+	test_grep alternate actual
 '
 
 test_expect_success 'git log --no-notes resets ref list' '
 	git log -1 --notes --notes=alternate \
 		--no-notes --notes \
 		>actual &&
-	grep xyzzy actual &&
-	! grep alternate actual
+	test_grep xyzzy actual &&
+	test_grep ! alternate actual
 '
 
 test_expect_success 'show -m notes' '
@@ -543,7 +543,7 @@
 
 test_expect_success '"git notes" without subcommand does not take arguments' '
 	test_expect_code 129 git notes HEAD^^ 2>err &&
-	grep "^error: unknown subcommand" err
+	test_grep "^error: unknown subcommand" err
 '
 
 test_expect_success 'list specific note with "git notes list <object>"' '
@@ -1464,7 +1464,7 @@
 	GIT_NOTES_REWRITE_REF=refs/notes/commits \
 		git notes copy --for-rewrite=foo <copy &&
 	git log -1 >actual &&
-	grep "replacement note 3" actual
+	test_grep "replacement note 3" actual
 '
 
 test_expect_success 'git notes copy diagnoses too many or too few arguments' '
diff --git a/t/t3310-notes-merge-manual-resolve.sh b/t/t3310-notes-merge-manual-resolve.sh
index 0bb366f..1d61c99 100755
--- a/t/t3310-notes-merge-manual-resolve.sh
+++ b/t/t3310-notes-merge-manual-resolve.sh
@@ -382,12 +382,12 @@
 	test_cmp pre_merge_z actual &&
 	# Merge commit mentions the notes refs merged
 	git log -1 --format=%B refs/notes/m > merge_commit_msg &&
-	grep -q refs/notes/m merge_commit_msg &&
-	grep -q refs/notes/z merge_commit_msg &&
+	test_grep -q refs/notes/m merge_commit_msg &&
+	test_grep -q refs/notes/z merge_commit_msg &&
 	# Merge commit mentions conflicting notes
-	grep -q "Conflicts" merge_commit_msg &&
+	test_grep -q "Conflicts" merge_commit_msg &&
 	( for sha1 in $(cat expect_conflicts); do
-		grep -q "$sha1" merge_commit_msg ||
+		test_grep -q "$sha1" merge_commit_msg ||
 		exit 1
 	done ) &&
 	# Verify contents of merge result
@@ -512,12 +512,12 @@
 	test_cmp pre_merge_z actual &&
 	# Merge commit mentions the notes refs merged
 	git log -1 --format=%B refs/notes/m > merge_commit_msg &&
-	grep -q refs/notes/m merge_commit_msg &&
-	grep -q refs/notes/z merge_commit_msg &&
+	test_grep -q refs/notes/m merge_commit_msg &&
+	test_grep -q refs/notes/z merge_commit_msg &&
 	# Merge commit mentions conflicting notes
-	grep -q "Conflicts" merge_commit_msg &&
+	test_grep -q "Conflicts" merge_commit_msg &&
 	( for sha1 in $(cat expect_conflicts); do
-		grep -q "$sha1" merge_commit_msg ||
+		test_grep -q "$sha1" merge_commit_msg ||
 		exit 1
 	done ) &&
 	# Verify contents of merge result
diff --git a/t/t3320-notes-merge-worktrees.sh b/t/t3320-notes-merge-worktrees.sh
index 96243b7..9a2c3ac 100755
--- a/t/t3320-notes-merge-worktrees.sh
+++ b/t/t3320-notes-merge-worktrees.sh
@@ -67,7 +67,7 @@
 		git config core.notesRef refs/notes/x &&
 		test_must_fail git notes merge z >out 2>&1 &&
 		test_grep "Automatic notes merge failed" out &&
-		grep -v "A notes merge into refs/notes/x is already in-progress in" out
+		test_grep -v "A notes merge into refs/notes/x is already in-progress in" out
 	) &&
 	echo "refs/notes/x" >expect &&
 	git -C worktree2 symbolic-ref NOTES_MERGE_REF >actual &&
diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
index c0c00fb..accaaba 100755
--- a/t/t3400-rebase.sh
+++ b/t/t3400-rebase.sh
@@ -270,12 +270,24 @@
 	test "a note" = "$(git notes show HEAD)"
 '
 
-test_expect_success 'rebase -m can copy notes' '
+test_expect_success 'rebase --apply can copy notes' '
 	git reset --hard n3 &&
-	git rebase -m --onto n1 n2 &&
+	git rebase --apply --onto n1 n2 &&
 	test "a note" = "$(git notes show HEAD)"
 '
 
+test_expect_success 'rebase drops notes of dropped commits' '
+	git checkout n1 &&
+	echo n3 >n3.t &&
+	echo n4 >n4.t &&
+	git add n3.t n4.t &&
+	git commit -m n34 &&
+	git rebase HEAD n3 &&
+	test_commit_message HEAD -m n2 &&
+	test_must_fail git notes list HEAD >actual &&
+	test_must_be_empty actual
+'
+
 test_expect_success 'rebase commit with an ancient timestamp' '
 	git reset --hard &&
 
@@ -287,16 +299,16 @@
 	git commit --date="@34567 +0600" -m "Old three" &&
 
 	git cat-file commit HEAD^^ >actual &&
-	grep "author .* 12345 +0400$" actual &&
+	test_grep "author .* 12345 +0400$" actual &&
 	git cat-file commit HEAD^ >actual &&
-	grep "author .* 23456 +0500$" actual &&
+	test_grep "author .* 23456 +0500$" actual &&
 	git cat-file commit HEAD >actual &&
-	grep "author .* 34567 +0600$" actual &&
+	test_grep "author .* 34567 +0600$" actual &&
 
 	git rebase --onto HEAD^^ HEAD^ &&
 
 	git cat-file commit HEAD >actual &&
-	grep "author .* 34567 +0600$" actual
+	test_grep "author .* 34567 +0600$" actual
 '
 
 test_expect_success 'rebase with "From " line in commit message' '
@@ -333,7 +345,7 @@
 		git tag two &&
 		test_must_fail git rebase --apply -f --onto init HEAD^ &&
 		GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
-		grep "show.*$(git rev-parse two)" stderr
+		test_grep "show.*$(git rev-parse two)" stderr
 	)
 '
 
@@ -364,12 +376,12 @@
 
 		git checkout test &&
 		git rebase main &&
-		grep "smudged" a.txt &&
+		test_grep "smudged" a.txt &&
 
 		git checkout removal &&
 		git reset --hard &&
 		git rebase main &&
-		grep "clean" a.txt
+		test_grep "clean" a.txt
 	)
 '
 
@@ -386,7 +398,7 @@
 		test_must_fail git rebase --merge --onto init HEAD^ &&
 		git rebase --show-current-patch >actual.patch &&
 		GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
-		grep "show.*REBASE_HEAD" stderr &&
+		test_grep "show.*REBASE_HEAD" stderr &&
 		test "$(git rev-parse REBASE_HEAD)" = "$(git rev-parse two)"
 	)
 '
diff --git a/t/t3402-rebase-merge.sh b/t/t3402-rebase-merge.sh
index 761de63..41ebcde 100755
--- a/t/t3402-rebase-merge.sh
+++ b/t/t3402-rebase-merge.sh
@@ -84,8 +84,8 @@
 	echo "AB $T" >> original &&
 	git commit -mconflicting original &&
 	git rebase -Xtheirs main &&
-	grep AB original &&
-	! grep 11 original
+	test_grep AB original &&
+	test_grep ! 11 original
 '
 
 test_expect_success 'rebase -Xtheirs from orphan' '
@@ -93,8 +93,8 @@
 	echo "AB $T" >> original &&
 	git commit -morphan-conflicting original &&
 	git rebase -Xtheirs main &&
-	grep AB original &&
-	! grep 11 original
+	test_grep AB original &&
+	test_grep ! 11 original
 '
 
 test_expect_success 'merge and rebase should match' '
@@ -210,15 +210,15 @@
 	git -C client rev-list --objects --all --missing=print >missing_list &&
 	MERGE_BASE_BLOB=$(git -C server rev-parse main^^:file.txt) &&
 	ADD_11_BLOB=$(git -C server rev-parse main^:file.txt) &&
-	grep "[?]$MERGE_BASE_BLOB" missing_list &&
-	grep "[?]$ADD_11_BLOB" missing_list &&
+	test_grep "[?]$MERGE_BASE_BLOB" missing_list &&
+	test_grep "[?]$ADD_11_BLOB" missing_list &&
 
 	git -C client rebase --merge --reapply-cherry-picks origin/main &&
 
 	# The blob from the merge base had to be fetched, but not "add 11"
 	git -C client rev-list --objects --all --missing=print >missing_list &&
-	! grep "[?]$MERGE_BASE_BLOB" missing_list &&
-	grep "[?]$ADD_11_BLOB" missing_list
+	test_grep ! "[?]$MERGE_BASE_BLOB" missing_list &&
+	test_grep "[?]$ADD_11_BLOB" missing_list
 '
 
 test_done
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 58b3bb0..9bf5351 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -144,7 +144,7 @@
 	mkdir subdir &&
 	git rebase -x "(cd subdir && git rev-parse --show-toplevel)" HEAD^ \
 		>actual &&
-	! grep "/subdir$" actual
+	test_grep ! "/subdir$" actual
 '
 
 test_expect_success 'rebase -i with the exec command checks tree cleanness' '
@@ -196,7 +196,7 @@
 		test_must_fail env FAKE_LINES="exec_this-command-does-not-exist 1" \
 			git rebase -i HEAD^ >actual 2>&1
 	) &&
-	! grep "Maybe git-rebase is broken" actual
+	test_grep ! "Maybe git-rebase is broken" actual
 '
 
 test_expect_success 'implicit interactive rebase does not invoke sequence editor' '
@@ -293,7 +293,7 @@
 
 test_expect_success 'show conflicted patch' '
 	GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
-	grep "show.*REBASE_HEAD" stderr &&
+	test_grep "show.*REBASE_HEAD" stderr &&
 	# the original stopped-sha1 is abbreviated
 	stopped_sha1="$(git rev-parse $(cat ".git/rebase-merge/stopped-sha"))" &&
 	test "$(git rev-parse REBASE_HEAD)" = "$stopped_sha1"
@@ -326,7 +326,7 @@
 	git tag twerp &&
 	git rebase -i --onto primary HEAD^ &&
 	git show HEAD >actual &&
-	grep "^Author: Twerp Snog" actual
+	test_grep "^Author: Twerp Snog" actual
 '
 
 test_expect_success 'retain authorship w/ conflicts' '
@@ -348,7 +348,7 @@
 	git rebase --continue &&
 	test_cmp_rev conflict-a^0 HEAD^ &&
 	git show >out &&
-	grep AttributeMe out
+	test_grep AttributeMe out
 '
 
 test_expect_success 'squash' '
@@ -368,7 +368,7 @@
 
 test_expect_success 'retain authorship when squashing' '
 	git show HEAD >actual &&
-	grep "^Author: Twerp Snog" actual
+	test_grep "^Author: Twerp Snog" actual
 '
 
 test_expect_success '--continue tries to commit' '
@@ -383,7 +383,7 @@
 	) &&
 	test_cmp_rev HEAD^ new-branch1 &&
 	git show HEAD >actual &&
-	grep chouette actual
+	test_grep chouette actual
 '
 
 test_expect_success 'verbose flag is heeded, even after --continue' '
@@ -393,7 +393,7 @@
 	echo resolved > file1 &&
 	git add file1 &&
 	git rebase --continue > output &&
-	grep "^ file1 | 2 +-$" output
+	test_grep "^ file1 | 2 +-$" output
 '
 
 test_expect_success 'multi-squash only fires up editor once' '
@@ -422,7 +422,7 @@
 	) &&
 	test $base = $(git rev-parse HEAD^) &&
 	git show >output &&
-	! grep NEVER output &&
+	test_grep ! NEVER output &&
 	git checkout @{-1} &&
 	git branch -D multi-fixup
 '
@@ -487,9 +487,9 @@
 	git cat-file commit HEAD | sed -e 1,/^\$/d > actual-squash-fixup &&
 	test_cmp expect-squash-fixup actual-squash-fixup &&
 	git cat-file commit HEAD@{2} >actual &&
-	grep "^# This is a combination of 3 commits\." actual &&
+	test_grep "^# This is a combination of 3 commits\." actual &&
 	git cat-file commit HEAD@{3} >actual &&
-	grep "^# This is a combination of 2 commits\." actual  &&
+	test_grep "^# This is a combination of 2 commits\." actual  &&
 	git checkout @{-1} &&
 	git branch -D squash-fixup
 '
@@ -593,7 +593,7 @@
 	) &&
 	test edited = $(git show HEAD:file7) &&
 	git show HEAD >actual &&
-	grep chouette actual &&
+	test_grep chouette actual &&
 	test $parent = $(git rev-parse HEAD^)
 '
 
@@ -779,22 +779,22 @@
 		FAKE_LINES="1 2 3 reword 4" FAKE_COMMIT_MESSAGE="E changed" \
 			git rebase -i A &&
 		git show HEAD >actual &&
-		grep "E changed" actual &&
+		test_grep "E changed" actual &&
 		test $(git rev-parse primary) != $(git rev-parse HEAD) &&
 		test_cmp_rev primary^ HEAD^ &&
 		FAKE_LINES="1 2 reword 3 4" FAKE_COMMIT_MESSAGE="D changed" \
 			git rebase -i A &&
 		git show HEAD^ >actual &&
-		grep "D changed" actual &&
+		test_grep "D changed" actual &&
 		FAKE_LINES="reword 1 2 3 4" FAKE_COMMIT_MESSAGE="B changed" \
 			git rebase -i A &&
 		git show HEAD~3 >actual &&
-		grep "B changed" actual &&
+		test_grep "B changed" actual &&
 		FAKE_LINES="1 r 2 pick 3 p 4" FAKE_COMMIT_MESSAGE="C changed" \
 			git rebase -i A
 	) &&
 	git show HEAD~2 >actual &&
-	grep "C changed" actual
+	test_grep "C changed" actual
 '
 
 test_expect_success 'reword fast-forwarded empty commit' '
@@ -1043,9 +1043,9 @@
 		FAKE_LINES="2" git rebase -i --root
 	) &&
 	git cat-file commit HEAD >output &&
-	grep -q "^author Twerp Snog" output &&
+	test_grep -q "^author Twerp Snog" output &&
 	git cat-file commit HEAD >actual &&
-	grep -q "^different author$" actual
+	test_grep -q "^different author$" actual
 '
 
 test_expect_success 'rebase -i --root temporary sentinel commit' '
@@ -1055,7 +1055,7 @@
 		test_must_fail env FAKE_LINES="2" git rebase -i --root
 	) &&
 	git cat-file commit HEAD >actual &&
-	grep "^tree $EMPTY_TREE" actual &&
+	test_grep "^tree $EMPTY_TREE" actual &&
 	git rebase --abort
 '
 
@@ -1079,7 +1079,7 @@
 			git rebase -i --root
 	) &&
 	git show HEAD^ >actual &&
-	grep "A changed" actual &&
+	test_grep "A changed" actual &&
 	test -z "$(git show -s --format=%p HEAD^)"
 '
 
@@ -1092,7 +1092,7 @@
 		git rebase -i --root
 	) &&
 	git show HEAD^ >actual &&
-	grep "C changed" actual &&
+	test_grep "C changed" actual &&
 	test -z "$(git show -s --format=%p HEAD^)"
 '
 
@@ -1251,6 +1251,17 @@
 	test $(cat file1) = Z
 '
 
+test_expect_success 'failing pick with --strategy is rescheduled' '
+	test_when_finished "rm -rf bin; test_might_fail git rebase --abort" &&
+	mkdir bin &&
+	echo exit 2 | write_script bin/git-merge-fail &&
+	git log -1 --format="pick %H # %s" HEAD >expect &&
+	test_must_fail env PATH="$PWD/bin:$PATH" \
+		git rebase --no-ff --strategy fail HEAD^ &&
+	test_cmp expect .git/rebase-merge/git-rebase-todo &&
+	test_cmp expect .git/rebase-merge/done
+'
+
 test_expect_success 'rebase -i error on commits with \ in message' '
 	current_head=$(git rev-parse HEAD) &&
 	test_when_finished "git rebase --abort; git reset --hard $current_head; rm -f error" &&
@@ -1315,11 +1326,11 @@
 		FAKE_COMMIT_MESSAGE="collide2 $(test_oid t3404_collider)" \
 		FAKE_LINES="reword 1 break 2" git rebase -i HEAD~2 &&
 		test $colliding_id = "$(git rev-parse HEAD | cut -c 1-4)" &&
-		grep "^pick $colliding_id " \
+		test_grep "^pick $colliding_id " \
 			.git/rebase-merge/git-rebase-todo.tmp &&
-		grep -E "^pick [0-9a-f]{$hexsz}" \
+		test_grep -E "^pick [0-9a-f]{$hexsz}" \
 			.git/rebase-merge/git-rebase-todo &&
-		grep -E "^pick [0-9a-f]{$hexsz}" \
+		test_grep -E "^pick [0-9a-f]{$hexsz}" \
 			.git/rebase-merge/git-rebase-todo.backup &&
 		git rebase --continue
 	) &&
@@ -1371,7 +1382,7 @@
 	echo changed >file1 &&
 	git add file1 &&
 	test_must_fail git rebase --continue 2>err &&
-	grep "error: you have staged changes in your working tree" err &&
+	test_grep "error: you have staged changes in your working tree" err &&
 	git reset --hard HEAD &&
 	git rebase --continue &&
 	test_cmp_rev HEAD D &&
@@ -1398,7 +1409,7 @@
 	echo changed >file1 &&
 	git add file1 &&
 	test_must_fail git rebase --continue 2>err &&
-	grep "error: you have staged changes in your working tree" err &&
+	test_grep "error: you have staged changes in your working tree" err &&
 	git reset --hard HEAD &&
 	git rebase --continue &&
 	test $(git cat-file commit HEAD | sed -ne \$p) = I &&
@@ -1423,7 +1434,7 @@
 	echo changed >file1 &&
 	git add file1 &&
 	test_must_fail git rebase --continue 2>err &&
-	grep "error: you have staged changes in your working tree" err &&
+	test_grep "error: you have staged changes in your working tree" err &&
 	git reset --hard HEAD &&
 	git rebase --continue &&
 	test $(git cat-file commit HEAD | sed -ne \$p) = I
@@ -1709,15 +1720,15 @@
 		set_replace_editor orig &&
 		test_must_fail git rebase -i A 2>actual
 	) &&
-	grep "cannot .fixup. without a previous commit" actual &&
-	grep "You can fix this with .git rebase --edit-todo.." actual &&
+	test_grep "cannot .fixup. without a previous commit" actual &&
+	test_grep "You can fix this with .git rebase --edit-todo.." actual &&
 	# verify that the todo list has not been truncated
 	grep -v "^#" .git/rebase-merge/git-rebase-todo >actual &&
 	test_cmp orig actual &&
 
 	test_must_fail git rebase --edit-todo 2>actual &&
-	grep "cannot .fixup. without a previous commit" actual &&
-	grep "You can fix this with .git rebase --edit-todo.." actual &&
+	test_grep "cannot .fixup. without a previous commit" actual &&
+	test_grep "You can fix this with .git rebase --edit-todo.." actual &&
 	# verify that the todo list has not been truncated
 	grep -v "^#" .git/rebase-merge/git-rebase-todo >actual &&
 	test_cmp orig actual
@@ -2252,7 +2263,7 @@
 	git update-ref refs/heads/second third &&
 
 	test_must_fail git rebase --continue 2>err &&
-	grep "update_ref failed for ref '\''refs/heads/second'\''" err &&
+	test_grep "update_ref failed for ref '\''refs/heads/second'\''" err &&
 
 	q_to_tab >expect <<-\EOF &&
 	Updated the following refs with --update-refs:
@@ -2283,10 +2294,10 @@
 		set_replace_editor todo &&
 		test_must_fail git rebase -i HEAD 2>err
 	) &&
-	grep "'\''#'\'' is not a valid label" err &&
-	grep "'\'':invalid'\'' is not a valid label" err &&
-	grep "'\'':bad'\'' is not a valid refname" err &&
-	grep "update-ref requires a fully qualified refname e.g. refs/heads/topic" \
+	test_grep "'\''#'\'' is not a valid label" err &&
+	test_grep "'\'':invalid'\'' is not a valid label" err &&
+	test_grep "'\'':bad'\'' is not a valid refname" err &&
+	test_grep "update-ref requires a fully qualified refname e.g. refs/heads/topic" \
 		err &&
 	test_path_is_missing execed
 '
diff --git a/t/t3406-rebase-message.sh b/t/t3406-rebase-message.sh
index bc51a9d..7b61188 100755
--- a/t/t3406-rebase-message.sh
+++ b/t/t3406-rebase-message.sh
@@ -62,21 +62,21 @@
 test_expect_success 'rebase --stat' '
 	git reset --hard start &&
 	git rebase --stat main >diffstat.txt &&
-	grep "^ fileX |  *1 +$" diffstat.txt
+	test_grep "^ fileX |  *1 +$" diffstat.txt
 '
 
 test_expect_success 'rebase w/config rebase.stat' '
 	git reset --hard start &&
 	git config rebase.stat true &&
 	git rebase main >diffstat.txt &&
-	grep "^ fileX |  *1 +$" diffstat.txt
+	test_grep "^ fileX |  *1 +$" diffstat.txt
 '
 
 test_expect_success 'rebase -n overrides config rebase.stat config' '
 	git reset --hard start &&
 	git config rebase.stat true &&
 	git rebase -n main >diffstat.txt &&
-	! grep "^ fileX |  *1 +$" diffstat.txt
+	test_grep ! "^ fileX |  *1 +$" diffstat.txt
 '
 
 test_expect_success 'rebase --onto outputs the invalid ref' '
diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh
index 5033411..07a5a11 100755
--- a/t/t3415-rebase-autosquash.sh
+++ b/t/t3415-rebase-autosquash.sh
@@ -223,7 +223,7 @@
 	git cat-file blob HEAD^:file1 >actual &&
 	test_cmp expect actual &&
 	git cat-file commit HEAD^ >commit &&
-	! grep "squash" commit &&
+	test_grep ! "squash" commit &&
 	grep "^extra para" commit >actual &&
 	test_line_count = 1 actual
 '
@@ -245,7 +245,7 @@
 	git cat-file blob HEAD^:file1 >actual &&
 	test_cmp expect actual &&
 	git cat-file commit HEAD^ >commit &&
-	! grep "squash" commit &&
+	test_grep ! "squash" commit &&
 	grep "^extra para" commit >actual &&
 	test_line_count = 1 actual
 '
@@ -377,7 +377,7 @@
 	git cat-file blob HEAD^:file1 >actual &&
 	test_cmp expect actual &&
 	git cat-file commit HEAD^ >commit &&
-	! grep "squash" commit &&
+	test_grep ! "squash" commit &&
 	grep first commit >actual &&
 	test_line_count = 3 actual
 '
@@ -424,7 +424,7 @@
 		set_backup_editor &&
 		GIT_USE_REBASE_HELPER=false \
 		git rebase -i --force-rebase --autosquash HEAD~4 &&
-		grep empty2 .git/backup-git-rebase-todo
+		test_grep empty2 .git/backup-git-rebase-todo
 	)
 '
 
@@ -467,7 +467,7 @@
 	git commit --allow-empty --amend -m edited-first &&
 	git rebase --skip &&
 	git show >actual &&
-	! grep first actual
+	test_grep ! first actual
 '
 
 test_expect_success 'fixup a fixup' '
diff --git a/t/t3416-rebase-onto-threedots.sh b/t/t3416-rebase-onto-threedots.sh
index ea501f2..e7f725b 100755
--- a/t/t3416-rebase-onto-threedots.sh
+++ b/t/t3416-rebase-onto-threedots.sh
@@ -107,7 +107,7 @@
 	git reset --hard K &&
 
 	test_must_fail git rebase -i --onto main...side J 2>err &&
-	grep "need exactly one merge base" err
+	test_grep "need exactly one merge base" err
 '
 
 test_expect_success 'rebase --keep-base --onto incompatible' '
@@ -196,7 +196,7 @@
 	git reset --hard K &&
 
 	test_must_fail git rebase -i --keep-base main 2>err &&
-	grep "need exactly one merge base with branch" err
+	test_grep "need exactly one merge base with branch" err
 '
 
 test_expect_success 'rebase --keep-base keeps cherry picks' '
diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh
index f9b8999..cb5c3a1 100755
--- a/t/t3418-rebase-continue.sh
+++ b/t/t3418-rebase-continue.sh
@@ -134,6 +134,7 @@
 	EOF
 
 	: skip and continue &&
+	test_config commit.status false &&
 	echo "cp \"\$1\" .git/copy.txt" | write_script copy-editor.sh &&
 	(test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
 
@@ -145,7 +146,8 @@
 
 	: now, let us ensure that "squash" is handled correctly &&
 	git reset --hard wants-fixup-3 &&
-	test_must_fail env FAKE_LINES="1 squash 2 squash 1 squash 3 squash 1" \
+	test_must_fail env \
+		FAKE_LINES="1 squash 2 squash 1 squash 3 squash 1 squash 4 squash 1" \
 		git rebase -i HEAD~4 &&
 
 	: the second squash failed, but there are two more in the chain &&
@@ -171,6 +173,32 @@
 	fixup 2
 	EOF
 
+	(test_set_editor "$PWD/copy-editor.sh" &&
+	 test_must_fail git rebase --skip) &&
+	: not the final squash, no need to edit the commit message &&
+	test_path_is_missing .git/copy.txt &&
+
+	: The first, third and fifth squashes succeeded, therefore: &&
+	cat >expect <<-\EOF &&
+	# This is a combination of 4 commits.
+	# This is the 1st commit message:
+
+	wants-fixup
+
+	# This is the commit message #2:
+
+	fixup 1
+
+	# This is the commit message #3:
+
+	fixup 2
+
+	# This is the commit message #4:
+
+	fixup 3
+	EOF
+	test_commit_message HEAD expect &&
+
 	(test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
 	test_commit_message HEAD <<-\EOF &&
 	wants-fixup
@@ -178,12 +206,12 @@
 	fixup 1
 
 	fixup 2
+
+	fixup 3
 	EOF
 
 	: Final squash failed, but there was still a squash &&
-	head -n1 .git/copy.txt >first-line &&
-	test_grep "# This is a combination of 3 commits" first-line &&
-	test_grep "# This is the commit message #3:" .git/copy.txt
+	test_cmp expect .git/copy.txt
 '
 
 test_expect_success 'setup rerere database' '
@@ -289,18 +317,18 @@
 test_expect_success '--reschedule-failed-exec' '
 	test_when_finished "git rebase --abort" &&
 	test_must_fail git rebase -x false --reschedule-failed-exec HEAD^ &&
-	grep "^exec false" .git/rebase-merge/git-rebase-todo &&
+	test_grep "^exec false" .git/rebase-merge/git-rebase-todo &&
 	git rebase --abort &&
 	test_must_fail git -c rebase.rescheduleFailedExec=true \
 		rebase -x false HEAD^ 2>err &&
-	grep "^exec false" .git/rebase-merge/git-rebase-todo &&
+	test_grep "^exec false" .git/rebase-merge/git-rebase-todo &&
 	test_grep "has been rescheduled" err
 '
 
 test_expect_success 'rebase.rescheduleFailedExec only affects `rebase -i`' '
 	test_config rebase.rescheduleFailedExec true &&
 	test_must_fail git rebase -x false HEAD^ &&
-	grep "^exec false" .git/rebase-merge/git-rebase-todo &&
+	test_grep "^exec false" .git/rebase-merge/git-rebase-todo &&
 	git rebase --abort &&
 	git rebase HEAD^
 '
@@ -310,7 +338,7 @@
 	test_config rebase.rescheduleFailedExec true &&
 	test_must_fail git rebase -x false --no-reschedule-failed-exec HEAD~2 &&
 	test_must_fail git rebase --continue 2>err &&
-	! grep "has been rescheduled" err
+	test_grep ! "has been rescheduled" err
 '
 
 test_expect_success 'new rebase.rescheduleFailedExec=true setting in an ongoing rebase is ignored' '
@@ -318,7 +346,7 @@
 	test_must_fail git rebase -x false HEAD~2 &&
 	test_config rebase.rescheduleFailedExec true &&
 	test_must_fail git rebase --continue 2>err &&
-	! grep "has been rescheduled" err
+	test_grep ! "has been rescheduled" err
 '
 
 test_expect_success 'there is no --no-reschedule-failed-exec in an ongoing rebase' '
diff --git a/t/t3420-rebase-autostash.sh b/t/t3420-rebase-autostash.sh
index 9ae06b6..1e4deb2 100755
--- a/t/t3420-rebase-autostash.sh
+++ b/t/t3420-rebase-autostash.sh
@@ -141,8 +141,8 @@
 		git checkout -b rebased-feature-branch feature-branch &&
 		echo dirty >>file3 &&
 		git rebase$type unrelated-onto-branch >actual 2>&1 &&
-		grep unrelated file4 &&
-		grep dirty file3 &&
+		test_grep unrelated file4 &&
+		test_grep dirty file3 &&
 		git checkout feature-branch
 	'
 
@@ -165,8 +165,8 @@
 		echo dirty >>file3 &&
 		git add file3 &&
 		git rebase$type unrelated-onto-branch &&
-		grep unrelated file4 &&
-		grep dirty file3 &&
+		test_grep unrelated file4 &&
+		test_grep dirty file3 &&
 		git checkout feature-branch
 	'
 
@@ -197,7 +197,7 @@
 		git add file2 &&
 		git rebase --continue &&
 		test_path_is_missing $dotest/autostash &&
-		grep dirty file3 &&
+		test_grep dirty file3 &&
 		git checkout feature-branch
 	'
 
@@ -212,7 +212,7 @@
 		test_path_is_missing file3 &&
 		git rebase --skip &&
 		test_path_is_missing $dotest/autostash &&
-		grep dirty file3 &&
+		test_grep dirty file3 &&
 		git checkout feature-branch
 	'
 
@@ -227,7 +227,7 @@
 		test_path_is_missing file3 &&
 		git rebase --abort &&
 		test_path_is_missing $dotest/autostash &&
-		grep dirty file3 &&
+		test_grep dirty file3 &&
 		git checkout feature-branch
 	'
 
@@ -260,11 +260,11 @@
 		git rebase$type unrelated-onto-branch >actual 2>&1 &&
 		test_path_is_missing $dotest &&
 		git reset --hard &&
-		grep unrelated file4 &&
-		! grep dirty file4 &&
+		test_grep unrelated file4 &&
+		test_grep ! dirty file4 &&
 		git checkout feature-branch &&
 		git stash pop &&
-		grep dirty file4
+		test_grep dirty file4
 	'
 
 	test_expect_success "rebase$type: check output with conflicting stash" '
@@ -286,7 +286,7 @@
 	test_when_finished git branch -D behind-feature-branch &&
 	echo dirty >>file1 &&
 	git rebase feature-branch &&
-	grep dirty file1 &&
+	test_grep dirty file1 &&
 	git checkout feature-branch
 '
 
@@ -297,7 +297,7 @@
 	test_when_finished git branch -D same-feature-branch &&
 	echo dirty >>file1 &&
 	git rebase feature-branch &&
-	grep dirty file1 &&
+	test_grep dirty file1 &&
 	git checkout feature-branch
 '
 
diff --git a/t/t3422-rebase-incompatible-options.sh b/t/t3422-rebase-incompatible-options.sh
index b9408f9..d6830b8 100755
--- a/t/t3422-rebase-incompatible-options.sh
+++ b/t/t3422-rebase-incompatible-options.sh
@@ -102,13 +102,13 @@
 	test_expect_success "$opt incompatible with rebase.rebaseMerges" "
 		git checkout B^0 &&
 		test_must_fail git -c rebase.rebaseMerges=true rebase $opt A 2>err &&
-		grep -e --no-rebase-merges err
+		test_grep -e --no-rebase-merges err
 	"
 
 	test_expect_success "$opt incompatible with rebase.updateRefs" "
 		git checkout B^0 &&
 		test_must_fail git -c rebase.updateRefs=true rebase $opt A 2>err &&
-		grep -e --no-update-refs err
+		test_grep -e --no-update-refs err
 	"
 
 	test_expect_success "$opt okay with overridden rebase.rebaseMerges" "
diff --git a/t/t3429-rebase-edit-todo.sh b/t/t3429-rebase-edit-todo.sh
index abd66f3..28e4bdd 100755
--- a/t/t3429-rebase-edit-todo.sh
+++ b/t/t3429-rebase-edit-todo.sh
@@ -19,7 +19,7 @@
 
 test_expect_success 'rebase exec with an empty list does not exec anything' '
 	git rebase HEAD -x "true" 2>output &&
-	! grep "Executing: true" output
+	test_grep ! "Executing: true" output
 '
 
 test_expect_success 'loose object cache vs re-reading todo list' '
diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh
index 84b2d0e..9bd61c5 100755
--- a/t/t3430-rebase-merges.sh
+++ b/t/t3430-rebase-merges.sh
@@ -161,7 +161,7 @@
 	test_when_finished "test_might_fail git rebase --abort" &&
 	test_must_fail env GIT_SEQUENCE_EDITOR="echo reset A^{tree} >" \
 		git rebase -i B C >out 2>err &&
-	grep "object .* is a tree" err &&
+	test_grep "object .* is a tree" err &&
 	test_must_be_empty out
 '
 
@@ -170,7 +170,7 @@
 	git branch refs/rewritten/my-label A &&
 	test_must_fail env GIT_SEQUENCE_EDITOR="echo reset my-label >" \
 		git rebase -i B C >out 2>err &&
-	grep "could not resolve ${SQ}my-label${SQ}" err &&
+	test_grep "could not resolve ${SQ}my-label${SQ}" err &&
 	test_must_be_empty out
 '
 
@@ -185,18 +185,18 @@
 	test_tick &&
 	test_must_fail git rebase -ir HEAD &&
 	test_cmp_rev REBASE_HEAD H^0 &&
-	grep "^merge -C .* G$" .git/rebase-merge/done &&
-	grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
+	test_grep "^merge -C .* G$" .git/rebase-merge/done &&
+	test_grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
 	test_path_is_missing .git/rebase-merge/patch &&
 	echo changed >file1 &&
 	git add file1 &&
 	test_must_fail git rebase --continue 2>err &&
-	grep "error: you have staged changes in your working tree" err &&
+	test_grep "error: you have staged changes in your working tree" err &&
 
 	: fail because of merge conflict &&
 	git reset --hard conflicting-G &&
 	test_must_fail git rebase --continue &&
-	! grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
+	test_grep ! "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
 	test_path_is_file .git/rebase-merge/patch
 '
 
@@ -208,8 +208,8 @@
 	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
 	test_tick &&
 	test_must_fail git rebase -ir HEAD &&
-	! grep "^merge G$" .git/rebase-merge/git-rebase-todo &&
-	grep "^Merge branch ${SQ}G${SQ}$" .git/rebase-merge/message
+	test_grep ! "^merge G$" .git/rebase-merge/git-rebase-todo &&
+	test_grep "^Merge branch ${SQ}G${SQ}$" .git/rebase-merge/message
 '
 
 test_expect_success 'merge -c commits before rewording and reloads todo-list' '
@@ -481,8 +481,8 @@
 	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
 	test_tick &&
 	git rebase -i -r A &&
-	grep "^label $third-" .git/ORIGINAL-TODO &&
-	! grep "^label $third$" .git/ORIGINAL-TODO
+	test_grep "^label $third-" .git/ORIGINAL-TODO &&
+	test_grep ! "^label $third$" .git/ORIGINAL-TODO
 '
 
 test_expect_success 'octopus merges' '
@@ -533,9 +533,9 @@
 	EOF
 	test_tick &&
 	git rebase -ir --autosquash --exec ./show.sh A >actual &&
-	grep "B: +Booh" actual &&
-	grep "E: +Booh" actual &&
-	grep "G: +G" actual
+	test_grep "B: +Booh" actual &&
+	test_grep "E: +Booh" actual &&
+	test_grep "G: +G" actual
 '
 
 test_expect_success '--continue after resolving conflicts after a merge' '
@@ -546,7 +546,7 @@
 	git checkout -b conflicts-in-merge H &&
 	test_commit H2 H2.t conflicts H2-conflict &&
 	test_must_fail git rebase -r already-has-g &&
-	grep conflicts H2.t &&
+	test_grep conflicts H2.t &&
 	echo resolved >H2.t &&
 	git add -u &&
 	git rebase --continue &&
@@ -616,9 +616,9 @@
 
 	done="$(git rev-parse --git-path rebase-merge/done)" &&
 	git -c rebase.maxLabelLength=14 rebase --rebase-merges -x "cp \"$done\" out" --root &&
-	grep "label 0123456789-我$" out &&
+	test_grep "label 0123456789-我$" out &&
 	git -c rebase.maxLabelLength=13 rebase --rebase-merges -x "cp \"$done\" out" --root &&
-	grep "label 0123456789-$" out
+	test_grep "label 0123456789-$" out
 '
 
 test_expect_success 'reword fast-forwarded empty merge commit' '
diff --git a/t/t3437-rebase-fixup-options.sh b/t/t3437-rebase-fixup-options.sh
index 5d306a4..a4b2a63 100755
--- a/t/t3437-rebase-fixup-options.sh
+++ b/t/t3437-rebase-fixup-options.sh
@@ -186,6 +186,53 @@
 	test_commit_message HEAD expected-message
 '
 
+test_expect_success 'fixup -c is remembered after skipping final fixup' '
+	test_when_finished "test_might_fail git rebase --abort" &&
+	cat >todo <<-\EOF &&
+	pick B
+	fixup -c A1
+	fixup A3
+	EOF
+	(
+		set_fake_editor &&
+		set_replace_editor todo &&
+		test_must_fail git rebase -i A A &&
+		git show && cat .git/rebase-merge/message-squash &&
+		FAKE_COMMIT_AMEND=edited git rebase --skip
+	) &&
+	test_commit_message HEAD <<-\EOF
+	new subject
+
+	new
+	body
+
+	edited
+	EOF
+'
+test_expect_success 'fixup -c is remembered after skipping later fixup' '
+	test_when_finished "test_might_fail git rebase --abort" &&
+	cat >todo <<-\EOF &&
+	pick B
+	fixup -c A1
+	fixup A3
+	fixup A2
+	EOF
+	(
+		set_fake_editor &&
+		set_replace_editor todo &&
+		test_must_fail git rebase -i A A &&
+		FAKE_COMMIT_AMEND=edited git rebase --skip
+	) &&
+	test_commit_message HEAD <<-\EOF
+	new subject
+
+	new
+	body
+
+	edited
+	EOF
+'
+
 test_expect_success 'sequence squash, fixup & fixup -c gives combined message' '
 	test_when_finished "test_might_fail git rebase --abort" &&
 	git checkout --detach A3 &&
diff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh
new file mode 100755
index 0000000..68a86d1
--- /dev/null
+++ b/t/t3454-history-drop.sh
@@ -0,0 +1,561 @@
+#!/bin/sh
+
+test_description='tests for git-history drop subcommand'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-log-graph.sh"
+
+expect_graph () {
+	cat >expect &&
+	lib_test_cmp_graph --format=%s "$@"
+}
+
+expect_log () {
+	git log --format="%s" "$@" >actual &&
+	cat >expect &&
+	test_cmp expect actual
+}
+
+test_expect_success 'errors on missing commit argument' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history drop 2>err &&
+		test_grep "command expects a single revision" err
+	)
+'
+
+test_expect_success 'errors on too many arguments' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history drop HEAD HEAD 2>err &&
+		test_grep "command expects a single revision" err
+	)
+'
+
+test_expect_success 'errors on unknown revision' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history drop does-not-exist 2>err &&
+		test_grep "commit cannot be found: does-not-exist" err
+	)
+'
+
+test_expect_success 'errors with invalid --empty= value' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_commit second &&
+		test_must_fail git history drop --empty=bogus HEAD 2>err &&
+		test_grep "unrecognized.*--empty.*bogus" err
+	)
+'
+
+test_expect_success 'drops a commit in the middle and replays descendants' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+
+		git symbolic-ref HEAD >expect &&
+		git history drop HEAD~ &&
+		git symbolic-ref HEAD >actual &&
+		test_cmp expect actual &&
+
+		expect_log <<-\EOF &&
+		third
+		first
+		EOF
+
+		test_must_fail git show HEAD:second.t &&
+		test_path_is_missing second.t &&
+
+		git reflog >reflog &&
+		test_grep "drop: dropping HEAD~" reflog
+	)
+'
+
+test_expect_success 'drops the HEAD commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+
+		git history drop HEAD &&
+
+		expect_log <<-\EOF
+		first
+		EOF
+	)
+'
+
+test_expect_success 'drops a commit on detached HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+		git checkout --detach HEAD &&
+
+		git history drop HEAD~ &&
+
+		expect_log <<-\EOF
+		third
+		first
+		EOF
+	)
+'
+
+# Note: in this case it would actually be fine to drop the root commit, as we
+# do have a descendant commit, and no reference points to the root commit
+# directly. So this is something that we may relax eventually.
+test_expect_success 'refuses to drop the root commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+
+		test_must_fail git history drop HEAD~ 2>err &&
+		test_grep "cannot drop root commit" err
+	)
+'
+
+# In contrast to the above case, we actually don't want to drop the root commit
+# here as that would cause us to end up with an empty commit graph.
+test_expect_success 'refuses to drop the root commit when branch becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+
+		test_must_fail git history drop HEAD 2>err &&
+		test_grep "cannot drop root commit" err
+	)
+'
+
+test_expect_success 'refuses to drop a merge commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+		git switch - &&
+		git merge theirs &&
+
+		test_must_fail git history drop HEAD 2>err &&
+		test_grep "cannot drop merge commit" err
+	)
+'
+
+test_expect_success 'refuses when descendants contain a merge commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit middle &&
+		git branch branch &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+		git switch - &&
+		git merge theirs &&
+
+		test_must_fail git history drop middle 2>err &&
+		test_grep "replaying merge commits is not supported yet" err
+	)
+'
+
+test_expect_success 'works in a bare repository' '
+	test_when_finished "rm -rf repo repo.git" &&
+
+	git init repo &&
+	test_commit -C repo first &&
+	test_commit -C repo second &&
+	test_commit -C repo third &&
+
+	git clone --bare repo repo.git &&
+	(
+		cd repo.git &&
+
+		git history drop HEAD~ &&
+		expect_log <<-\EOF
+		third
+		first
+		EOF
+	)
+'
+
+test_expect_success 'updates branches on other lines of descent' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit target &&
+		git branch theirs &&
+		test_commit ours &&
+		git switch theirs &&
+		test_commit theirs &&
+
+		expect_graph --branches <<-\EOF &&
+		* theirs
+		| * ours
+		|/
+		* target
+		* base
+		EOF
+
+		git history drop target &&
+
+		expect_graph --branches <<-\EOF
+		* ours
+		| * theirs
+		|/
+		* base
+		EOF
+	)
+'
+
+test_expect_success 'moves branch pointing at dropped commit to its parent' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		git branch points-at-second &&
+		test_commit third &&
+
+		git rev-parse first >expect &&
+		git history drop second &&
+		git rev-parse points-at-second >actual &&
+		test_cmp expect actual &&
+
+		expect_log --format="%s %D" --branches <<-\EOF
+		third HEAD -> main
+		first tag: first, points-at-second
+		EOF
+	)
+'
+
+test_expect_success '--dry-run prints ref updates without modifying repo' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit middle &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+
+		git refs list >refs-expect &&
+		git history drop --dry-run main~ >updates &&
+		git refs list >refs-actual &&
+		test_cmp refs-expect refs-actual &&
+		test_grep "update refs/heads/main" updates &&
+
+		git update-ref --stdin <updates &&
+		expect_log main <<-\EOF
+		ours
+		base
+		EOF
+	)
+'
+
+test_expect_success '--dry-run detects conflicts with modified working tree' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second modify-me &&
+		echo modified >modify-me &&
+
+		git refs list >refs-expect &&
+		git diff >diff-expect &&
+		test_must_fail git history drop --dry-run HEAD 2>err &&
+		test_grep "dropping this commit would overwrite local changes" err &&
+		git diff >diff-actual &&
+		git refs list >refs-actual &&
+
+		test_cmp diff-expect diff-actual &&
+		test_cmp refs-expect refs-actual
+	)
+'
+
+test_expect_success '--update-refs=head updates only HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit target &&
+		git branch theirs &&
+		test_commit ours &&
+		git switch theirs &&
+		test_commit theirs &&
+
+		# When told to update HEAD only, the command refuses to
+		# rewrite commits that are not an ancestor of HEAD.
+		test_must_fail git history drop --update-refs=head main 2>err &&
+		test_grep "rewritten commit must be an ancestor of HEAD" err &&
+
+		expect_graph --branches <<-\EOF &&
+		* theirs
+		| * ours
+		|/
+		* target
+		* base
+		EOF
+
+		git switch main &&
+		git history drop --update-refs=head target &&
+
+		expect_graph --branches <<-\EOF
+		* ours
+		| * theirs
+		| * target
+		|/
+		* base
+		EOF
+	)
+'
+
+test_expect_success '--update-refs=head can rewrite detached HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+		git switch --detach HEAD &&
+
+		git history drop --update-refs=head second &&
+
+		expect_log HEAD <<-\EOF &&
+		third
+		first
+		EOF
+		expect_log main <<-\EOF
+		third
+		second
+		first
+		EOF
+	)
+'
+
+test_expect_success 'conflict with replayed commit aborts cleanly' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit conflict-a file &&
+		test_commit conflict-b file &&
+
+		git refs list >refs-expect &&
+		test_must_fail git history drop HEAD~ 2>err &&
+		test_grep "failed replaying descendants" err &&
+		git refs list >refs-actual &&
+		test_cmp refs-expect refs-actual
+	)
+'
+
+# Build a history where a descendant of the drop target reverts the change
+# introduced by the drop target. After dropping, the descendant's diff applies
+# against a tree that already lacks the change, so it becomes empty.
+setup_empty_descendant_repo () {
+	git init "$1" &&
+	(
+		cd "$1" &&
+		echo C1 >file &&
+		git add file &&
+		git commit -m "base" &&
+		git tag base &&
+		echo C2 >file &&
+		git add file &&
+		git commit -m "drop-me" &&
+		git tag drop-me &&
+		test_commit middle &&
+		echo C1 >file &&
+		git add file &&
+		git commit -m "revert-drop-me" &&
+		git tag revert-drop-me
+	)
+}
+
+test_expect_success '--empty=drop drops descendants that become empty' '
+	test_when_finished "rm -rf repo" &&
+	setup_empty_descendant_repo repo &&
+	(
+		cd repo &&
+
+		git history drop --empty=drop drop-me &&
+
+		expect_log <<-\EOF
+		middle
+		base
+		EOF
+	)
+'
+
+test_expect_success '--empty=keep keeps descendants that become empty' '
+	test_when_finished "rm -rf repo" &&
+	setup_empty_descendant_repo repo &&
+	(
+		cd repo &&
+
+		git history drop --empty=keep drop-me &&
+
+		expect_log <<-\EOF &&
+		revert-drop-me
+		middle
+		base
+		EOF
+		git diff HEAD~ HEAD >diff &&
+		test_must_be_empty diff
+	)
+'
+
+test_expect_success '--empty=abort errors out when a descendant becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	setup_empty_descendant_repo repo &&
+	(
+		cd repo &&
+
+		test_must_fail git history drop --empty=abort drop-me 2>err &&
+		test_grep "became empty after replay" err
+	)
+'
+
+test_expect_success 'updates index and worktree when HEAD moves' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+
+		git history drop second &&
+
+		# Worktree should no longer contain second.t.
+		test_path_is_missing second.t &&
+		test_path_is_file first.t &&
+		test_path_is_file third.t &&
+
+		# Index and worktree should both match the new HEAD.
+		git status --porcelain --untracked-files=no >status &&
+		test_must_be_empty status
+	)
+'
+
+test_expect_success 'updates worktree when dropping HEAD itself' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+
+		git history drop HEAD &&
+
+		test_path_is_missing second.t &&
+		test_path_is_file first.t &&
+
+		git status --porcelain --untracked-files=no >status &&
+		test_must_be_empty status
+	)
+'
+
+test_expect_success 'preserves unrelated unstaged modifications' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		echo first-content >unrelated.txt &&
+		git add unrelated.txt &&
+		git commit -m "add unrelated" &&
+		test_commit second &&
+		test_commit third &&
+
+		echo locally-modified >unrelated.txt &&
+
+		git diff >diff-expect &&
+		git history drop second &&
+		git diff >diff-actual &&
+		test_cmp diff-expect diff-actual &&
+		test_path_is_missing second.t
+	)
+'
+
+test_expect_success 'preserves unrelated staged changes' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		echo first-content >unrelated.txt &&
+		git add unrelated.txt &&
+		git commit -m "add unrelated" &&
+		test_commit second &&
+		test_commit third &&
+
+		echo staged-change >unrelated.txt &&
+		git add unrelated.txt &&
+
+		git diff --cached >diff-expect &&
+		git history drop second &&
+		git diff --cached >diff-actual &&
+		test_cmp diff-expect diff-actual &&
+		test_path_is_missing second.t
+	)
+'
+
+test_expect_success 'aborts when local modifications would be overwritten' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit conflict &&
+
+		echo local-edit >conflict.t &&
+		git diff >diff-expect &&
+		test_must_fail git history drop HEAD 2>err &&
+		test_grep "would overwrite local changes" err &&
+		git diff >diff-actual &&
+		test_cmp diff-expect diff-actual
+	)
+'
+
+test_done
diff --git a/t/t3500-cherry.sh b/t/t3500-cherry.sh
index 3e66827..03655c8 100755
--- a/t/t3500-cherry.sh
+++ b/t/t3500-cherry.sh
@@ -100,8 +100,8 @@
 		GIT_TRACE2_EVENT="$(pwd)/trace2.output" git cherry upstream-with-space feature-without-space >actual &&
 		test_cmp ../expect actual &&
 
-		! grep "child_start.*fetch.negotiationAlgorithm" trace2.output &&
-		! grep "\"key\":\"fetch_count\"" trace2.output
+		test_grep ! "child_start.*fetch.negotiationAlgorithm" trace2.output &&
+		test_grep ! "\"key\":\"fetch_count\"" trace2.output
 	)
 '
 
diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh
index 8025a28..939e7a1 100755
--- a/t/t3501-revert-cherry-pick.sh
+++ b/t/t3501-revert-cherry-pick.sh
@@ -67,7 +67,7 @@
 	git checkout rename2 &&
 	git cherry-pick added &&
 	test_cmp_rev rename2 HEAD^ &&
-	grep "Add extra line at the end" opos &&
+	test_grep "Add extra line at the end" opos &&
 	git reflog -1 | grep cherry-pick
 
 '
@@ -162,7 +162,7 @@
 	echo modified >renamed &&
 	git cherry-pick refs/heads/unrelated &&
 	test $(git rev-parse :0:renamed) = $(git rev-parse HEAD~2:to-rename.t) &&
-	grep -q "^modified$" renamed
+	test_grep -q "^modified$" renamed
 '
 
 test_expect_success 'advice from failed revert' '
@@ -253,7 +253,7 @@
 test_expect_success 'cherry-pick is unaware of --reference (for now)' '
 	test_when_finished "git reset --hard" &&
 	test_must_fail git cherry-pick --reference HEAD 2>actual &&
-	grep "^usage: git cherry-pick" actual
+	test_grep "^usage: git cherry-pick" actual
 '
 
 test_done
diff --git a/t/t3504-cherry-pick-rerere.sh b/t/t3504-cherry-pick-rerere.sh
index 18aeba1..462973f 100755
--- a/t/t3504-cherry-pick-rerere.sh
+++ b/t/t3504-cherry-pick-rerere.sh
@@ -98,9 +98,9 @@
 test_expect_success 'cherry-pick conflict without rerere' '
 	test_config rerere.enabled false &&
 	test_must_fail git cherry-pick foo-main &&
-	grep ===== foo &&
-	grep foo-dev foo &&
-	grep foo-main foo
+	test_grep ===== foo &&
+	test_grep foo-dev foo &&
+	test_grep foo-main foo
 '
 
 test_done
diff --git a/t/t3510-cherry-pick-sequence.sh b/t/t3510-cherry-pick-sequence.sh
index 66ff9db..5777dff 100755
--- a/t/t3510-cherry-pick-sequence.sh
+++ b/t/t3510-cherry-pick-sequence.sh
@@ -580,10 +580,10 @@
 	git cat-file commit HEAD~1 >picked_msg &&
 	git cat-file commit HEAD~2 >unrelatedpick_msg &&
 	git cat-file commit HEAD~3 >initial_msg &&
-	! grep "cherry picked from" initial_msg &&
-	grep "cherry picked from" unrelatedpick_msg &&
-	grep "cherry picked from" picked_msg &&
-	grep "cherry picked from" anotherpick_msg
+	test_grep ! "cherry picked from" initial_msg &&
+	test_grep "cherry picked from" unrelatedpick_msg &&
+	test_grep "cherry picked from" picked_msg &&
+	test_grep "cherry picked from" anotherpick_msg
 '
 
 test_expect_success '--continue of single-pick respects -x' '
@@ -594,7 +594,7 @@
 	git cherry-pick --continue &&
 	test_path_is_missing .git/sequencer &&
 	git cat-file commit HEAD >msg &&
-	grep "cherry picked from" msg
+	test_grep "cherry picked from" msg
 '
 
 test_expect_success '--continue respects -x in first commit in multi-pick' '
@@ -606,7 +606,7 @@
 	test_path_is_missing .git/sequencer &&
 	git cat-file commit HEAD^ >msg &&
 	picked=$(git rev-parse --verify picked) &&
-	grep "cherry picked from.*$picked" msg
+	test_grep "cherry picked from.*$picked" msg
 '
 
 test_expect_failure '--signoff is automatically propagated to resolved conflict' '
@@ -621,10 +621,10 @@
 	git cat-file commit HEAD~1 >picked_msg &&
 	git cat-file commit HEAD~2 >unrelatedpick_msg &&
 	git cat-file commit HEAD~3 >initial_msg &&
-	! grep "Signed-off-by:" initial_msg &&
-	grep "Signed-off-by:" unrelatedpick_msg &&
-	! grep "Signed-off-by:" picked_msg &&
-	grep "Signed-off-by:" anotherpick_msg
+	test_grep ! "Signed-off-by:" initial_msg &&
+	test_grep "Signed-off-by:" unrelatedpick_msg &&
+	test_grep ! "Signed-off-by:" picked_msg &&
+	test_grep "Signed-off-by:" anotherpick_msg
 '
 
 test_expect_failure '--signoff dropped for implicit commit of resolution, multi-pick case' '
@@ -637,7 +637,7 @@
 	git diff --exit-code HEAD &&
 	test_cmp_rev initial HEAD^^ &&
 	git cat-file commit HEAD^ >msg &&
-	! grep Signed-off-by: msg
+	test_grep ! Signed-off-by: msg
 '
 
 test_expect_failure 'sign-off needs to be reaffirmed after conflict resolution, single-pick case' '
@@ -650,7 +650,7 @@
 	git diff --exit-code HEAD &&
 	test_cmp_rev initial HEAD^ &&
 	git cat-file commit HEAD >msg &&
-	! grep Signed-off-by: msg
+	test_grep ! Signed-off-by: msg
 '
 
 test_expect_success 'malformed instruction sheet 1' '
diff --git a/t/t3602-rm-sparse-checkout.sh b/t/t3602-rm-sparse-checkout.sh
index 02c7acd..252df28 100755
--- a/t/t3602-rm-sparse-checkout.sh
+++ b/t/t3602-rm-sparse-checkout.sh
@@ -79,8 +79,8 @@
 	git reset --hard &&
 	git sparse-checkout set a &&
 	test_must_fail git rm nonexistent 2>stderr &&
-	grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
-	! grep -F -f sparse_error_header stderr
+	test_grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
+	test_grep ! -F -f sparse_error_header stderr
 '
 
 test_expect_success 'do not warn about sparse entries when pathspec matches dense entries' '
diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
index 53a4782..64ad7a2 100755
--- a/t/t3705-add-sparse-checkout.sh
+++ b/t/t3705-add-sparse-checkout.sh
@@ -149,8 +149,8 @@
 test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
 	setup_sparse_entry &&
 	test_must_fail git add nonexistent 2>stderr &&
-	grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
-	! grep -F -f sparse_error_header stderr
+	test_grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
+	test_grep ! -F -f sparse_error_header stderr
 '
 
 test_expect_success 'do not warn when pathspec matches dense entries' '
@@ -184,19 +184,19 @@
 	git -c core.autocrlf=input add --sparse sparse_entry 2>stderr &&
 	test_must_be_empty stderr &&
 	git ls-files --stage >actual &&
-	grep "^100644 .*sparse_entry\$" actual &&
+	test_grep "^100644 .*sparse_entry\$" actual &&
 
 	git add --sparse --chmod=+x sparse_entry 2>stderr &&
 	test_must_be_empty stderr &&
 	git ls-files --stage >actual &&
-	grep "^100755 .*sparse_entry\$" actual &&
+	test_grep "^100755 .*sparse_entry\$" actual &&
 
 	git reset &&
 
 	# This will print a message over stderr on Windows.
 	git add --sparse --renormalize sparse_entry &&
 	git status --porcelain >actual &&
-	grep "^M  sparse_entry\$" actual
+	test_grep "^M  sparse_entry\$" actual
 '
 
 test_expect_success 'add obeys advice.updateSparsePath' '
diff --git a/t/t3800-mktag.sh b/t/t3800-mktag.sh
index e3cf0ff..d3ddf07 100755
--- a/t/t3800-mktag.sh
+++ b/t/t3800-mktag.sh
@@ -535,9 +535,9 @@
 
 	git fsck &&
 	git -c fsck.extraHeaderEntry=warn fsck 2>err &&
-	grep "warning .*extraHeaderEntry:" err &&
+	test_grep "warning .*extraHeaderEntry:" err &&
 	test_must_fail git -c fsck.extraHeaderEntry=error 2>err fsck &&
-	grep "error .* extraHeaderEntry:" err
+	test_grep "error .* extraHeaderEntry:" err
 '
 
 cat >tag.sig <<EOF
diff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh
index ef7d7e1..b2f9301 100755
--- a/t/t3901-i18n-patch.sh
+++ b/t/t3901-i18n-patch.sh
@@ -81,10 +81,10 @@
 
 	git format-patch --stdout main..HEAD^ >out-l1 &&
 	git format-patch --stdout HEAD^ >out-l2 &&
-	grep "^Content-Type: text/plain; charset=ISO8859-1" out-l1 &&
-	grep "^From: =?ISO8859-1?q?=C1=E9=ED=20=F3=FA?=" out-l1 &&
-	grep "^Content-Type: text/plain; charset=ISO8859-1" out-l2 &&
-	grep "^From: =?ISO8859-1?q?=C1=E9=ED=20=F3=FA?=" out-l2
+	test_grep "^Content-Type: text/plain; charset=ISO8859-1" out-l1 &&
+	test_grep "^From: =?ISO8859-1?q?=C1=E9=ED=20=F3=FA?=" out-l1 &&
+	test_grep "^Content-Type: text/plain; charset=ISO8859-1" out-l2 &&
+	test_grep "^From: =?ISO8859-1?q?=C1=E9=ED=20=F3=FA?=" out-l2
 '
 
 test_expect_success 'format-patch output (UTF-8)' '
@@ -92,10 +92,10 @@
 
 	git format-patch --stdout main..HEAD^ >out-u1 &&
 	git format-patch --stdout HEAD^ >out-u2 &&
-	grep "^Content-Type: text/plain; charset=UTF-8" out-u1 &&
-	grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" out-u1 &&
-	grep "^Content-Type: text/plain; charset=UTF-8" out-u2 &&
-	grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" out-u2
+	test_grep "^Content-Type: text/plain; charset=UTF-8" out-u1 &&
+	test_grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" out-u1 &&
+	test_grep "^Content-Type: text/plain; charset=UTF-8" out-u2 &&
+	test_grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD=20=C3=B3=C3=BA?=" out-u2
 '
 
 test_expect_success 'rebase (U/U)' '
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ecc35aa..da27a65 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -20,21 +20,21 @@
 
 test_expect_success 'usage on cmd and subcommand invalid option' '
 	test_expect_code 129 git stash --invalid-option 2>usage &&
-	grep "or: git stash" usage &&
+	test_grep "or: git stash" usage &&
 
 	test_expect_code 129 git stash push --invalid-option 2>usage &&
-	! grep "or: git stash" usage
+	test_grep ! "or: git stash" usage
 '
 
 test_expect_success 'usage on main command -h emits a summary of subcommands' '
-	test_expect_code 129 git stash -h >usage &&
-	grep -F "usage: git stash list" usage &&
-	grep -F "or: git stash show" usage
+	git stash -h >usage &&
+	test_grep -F "usage: git stash list" usage &&
+	test_grep -F "or: git stash show" usage
 '
 
 test_expect_success 'usage for subcommands should emit subcommand usage' '
-	test_expect_code 129 git stash push -h >usage &&
-	grep -F "usage: git stash [push" usage
+	git stash push -h >usage &&
+	test_grep -F "usage: git stash [push" usage
 '
 
 diff_cmp () {
@@ -965,7 +965,7 @@
 	test $(git rev-parse stash) = $STASH_ID &&
 	git reflog --format=%H stash| grep $STASH_ID &&
 	git stash pop &&
-	grep quux bazzy
+	test_grep quux bazzy
 '
 
 test_expect_success 'handle stash specification with spaces' '
@@ -977,7 +977,7 @@
 	echo cow >file &&
 	git stash &&
 	git stash apply "stash@{$stamp}" &&
-	grep pig file
+	test_grep pig file
 '
 
 test_expect_success 'setup stash with index and worktree changes' '
@@ -1500,9 +1500,9 @@
 
 test_expect_success 'stash export rejects invalid arguments' '
 	test_must_fail git stash export --print --to-ref refs/heads/invalid 2>err &&
-	grep "exactly one of --print and --to-ref is required" err &&
+	test_grep "exactly one of --print and --to-ref is required" err &&
 	test_must_fail git stash export 2>err2 &&
-	grep "exactly one of --print and --to-ref is required" err2
+	test_grep "exactly one of --print and --to-ref is required" err2
 '
 
 test_expect_success 'stash can import and export zero stashes' '
@@ -1519,7 +1519,7 @@
 	git stash import foo &&
 	test_must_fail git stash import HEAD 2>output &&
 	oid=$(git rev-parse HEAD) &&
-	grep "$oid is not a valid exported stash commit" output &&
+	test_grep "$oid is not a valid exported stash commit" output &&
 	test_cmp_rev stash@{0} t-stash0 &&
 
 	git checkout --orphan orphan &&
@@ -1527,7 +1527,7 @@
 	git update-ref refs/heads/orphan "$(cat fake-commit)" &&
 	oid=$(git rev-parse HEAD) &&
 	test_must_fail git stash import orphan 2>output &&
-	grep "found stash commit $oid without expected prefix" output &&
+	test_grep "found stash commit $oid without expected prefix" output &&
 	test_cmp_rev stash@{0} t-stash0 &&
 
 	git checkout --orphan orphan2 &&
@@ -1535,7 +1535,7 @@
 	git update-ref refs/heads/orphan2 "$(cat fake-commit)" &&
 	oid=$(git rev-parse HEAD) &&
 	test_must_fail git stash import orphan2 2>output &&
-	grep "found root commit $oid with invalid data" output &&
+	test_grep "found root commit $oid with invalid data" output &&
 	test_cmp_rev stash@{0} t-stash0
 '
 
@@ -1741,7 +1741,7 @@
 		git stash push -m "custom stash for work_branch" &&
 
 		git stash list >../actual_stash_list.txt &&
-		grep "On work_branch: custom stash for work_branch" ../actual_stash_list.txt
+		test_grep "On work_branch: custom stash for work_branch" ../actual_stash_list.txt
 	)
 '
 
@@ -1751,7 +1751,7 @@
 
 test_expect_success 'controlled error return on unrecognized option' '
 	test_expect_code 129 git stash show -p --invalid 2>usage &&
-	grep -e "^usage: git stash show" usage
+	test_grep -e "^usage: git stash show" usage
 '
 
 test_expect_success 'stash.index=true implies --index' '
diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh
index 90a4ff2..adc45c7 100755
--- a/t/t3904-stash-patch.sh
+++ b/t/t3904-stash-patch.sh
@@ -103,8 +103,8 @@
 	printf "%s\n" s n y q |
 	git stash -p 2>error &&
 	test_must_be_empty error &&
-	grep "added line 1" test &&
-	! grep "added line 2" test
+	test_grep "added line 1" test &&
+	test_grep ! "added line 2" test
 '
 
 test_expect_success 'stash -p not confused by GIT_PAGER_IN_USE' '
diff --git a/t/t3908-stash-in-worktree.sh b/t/t3908-stash-in-worktree.sh
index 2b2b366..e7ae838 100755
--- a/t/t3908-stash-in-worktree.sh
+++ b/t/t3908-stash-in-worktree.sh
@@ -21,7 +21,7 @@
 		git stash &&
 		git stash apply >out
 	) &&
-	grep "\.\.\/initial\.t" wt/subdir/out
+	test_grep "\.\.\/initial\.t" wt/subdir/out
 '
 
 test_done
diff --git a/t/t3910-mac-os-precompose.sh b/t/t3910-mac-os-precompose.sh
index 6d5918c..ea75fb4 100755
--- a/t/t3910-mac-os-precompose.sh
+++ b/t/t3910-mac-os-precompose.sh
@@ -207,6 +207,22 @@
 	git commit -m "Long filename"
 '
 
+test_expect_success "status with long non-ASCII filename" '
+	test_when_finished "rm -rf long-utf8-status" &&
+	git init long-utf8-status &&
+	(
+		cd long-utf8-status &&
+		test "$(git config --bool core.precomposeunicode)" = true &&
+		long_utf8_name=$(
+			printf "%253s\342\200\224" "" |
+			tr " " a
+		) &&
+		test "$(printf "%s" "$long_utf8_name" | wc -c | tr -d " ")" = 256 &&
+		printf "content\n" >"$long_utf8_name" &&
+		git status --porcelain=v1 >actual
+	)
+'
+
 test_expect_failure 'handle existing decomposed filenames' '
 	echo content >"verbatim.$Adiarnfd" &&
 	git -c core.precomposeunicode=false add "verbatim.$Adiarnfd" &&
diff --git a/t/t4000-diff-format.sh b/t/t4000-diff-format.sh
index 32b14e3..74dec7d 100755
--- a/t/t4000-diff-format.sh
+++ b/t/t4000-diff-format.sh
@@ -84,7 +84,7 @@
 
 test_expect_success 'git diff-files --no-patch --patch-with-raw shows the patch and raw data' '
 	git diff-files --no-patch --patch-with-raw >actual &&
-	grep -q "^:100644 100755 .* $ZERO_OID M	path0\$" actual &&
+	test_grep -q "^:100644 100755 .* $ZERO_OID M	path0\$" actual &&
 	tail -n +4 actual >actual-patch &&
 	compare_diff_patch expected actual-patch
 '
diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh
index 4f520d6..ad47410 100755
--- a/t/t4001-diff-rename.sh
+++ b/t/t4001-diff-rename.sh
@@ -189,7 +189,7 @@
 	M	path1
 	EOF
 	test_cmp expect actual.munged &&
-	grep warning actual.err
+	test_grep warning actual.err
 '
 
 test_expect_success 'rename pretty print with nothing in common' '
@@ -258,7 +258,7 @@
 
 	git diff-tree -M -l0 HEAD HEAD^ >actual &&
 	# Verify that a rename from myotherfile to myfile was detected
-	grep "myotherfile.*myfile" actual
+	test_grep "myotherfile.*myfile" actual
 '
 
 test_expect_success 'basename similarity vs best similarity' '
diff --git a/t/t4011-diff-symlink.sh b/t/t4011-diff-symlink.sh
index ac837b6..4103bd6 100755
--- a/t/t4011-diff-symlink.sh
+++ b/t/t4011-diff-symlink.sh
@@ -140,7 +140,7 @@
 	ln -s narf pinky &&
 	ln -s take\ over brain &&
 	test_must_fail git diff --no-index pinky brain >output 2>output.err &&
-	grep narf output &&
+	test_grep narf output &&
 	test_must_be_empty output.err
 '
 
diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh
index d35695f..b7a382c 100755
--- a/t/t4013-diff-various.sh
+++ b/t/t4013-diff-various.sh
@@ -778,7 +778,7 @@
 test_expect_success 'diff --no-renames cannot be abbreviated' '
 	test_expect_code 129 git diff --no-rename >actual 2>error &&
 	test_must_be_empty actual &&
-	grep "invalid option: --no-rename" error
+	test_grep "invalid option: --no-rename" error
 '
 
 test_done
diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh
index 0b89d12..4afcd85 100755
--- a/t/t4014-format-patch.sh
+++ b/t/t4014-format-patch.sh
@@ -119,17 +119,17 @@
 
 test_expect_success 'commit did not screw up the log message' '
 	git cat-file commit side >actual &&
-	grep "^Side .* with .* backslash-n" actual
+	test_grep "^Side .* with .* backslash-n" actual
 '
 
 test_expect_success 'format-patch did not screw up the log message' '
-	grep "^Subject: .*Side changes #3 with .* backslash-n" patch0 &&
-	grep "^Subject: .*Side changes #3 with .* backslash-n" patch1
+	test_grep "^Subject: .*Side changes #3 with .* backslash-n" patch0 &&
+	test_grep "^Subject: .*Side changes #3 with .* backslash-n" patch1
 '
 
 test_expect_success 'replay did not screw up the log message' '
 	git cat-file commit rebuild-1 >actual &&
-	grep "^Side .* with .* backslash-n" actual
+	test_grep "^Side .* with .* backslash-n" actual
 '
 
 test_expect_success 'format-patch empty commit' '
@@ -145,8 +145,8 @@
 " &&
 	git format-patch --stdout main..side >patch2 &&
 	sed -e "/^\$/q" patch2 >hdrs2 &&
-	grep "^To: R E Cipient <rcipient@example.com>\$" hdrs2 &&
-	grep "^Cc: S E Cipient <scipient@example.com>\$" hdrs2
+	test_grep "^To: R E Cipient <rcipient@example.com>\$" hdrs2 &&
+	test_grep "^Cc: S E Cipient <scipient@example.com>\$" hdrs2
 '
 
 test_expect_success 'extra headers without newlines' '
@@ -154,8 +154,8 @@
 	git config --add format.headers "Cc: S E Cipient <scipient@example.com>" &&
 	git format-patch --stdout main..side >patch3 &&
 	sed -e "/^\$/q" patch3 >hdrs3 &&
-	grep "^To: R E Cipient <rcipient@example.com>\$" hdrs3 &&
-	grep "^Cc: S E Cipient <scipient@example.com>\$" hdrs3
+	test_grep "^To: R E Cipient <rcipient@example.com>\$" hdrs3 &&
+	test_grep "^Cc: S E Cipient <scipient@example.com>\$" hdrs3
 '
 
 test_expect_success 'extra headers with multiple To:s' '
@@ -163,79 +163,79 @@
 	git config --add format.headers "To: S E Cipient <scipient@example.com>" &&
 	git format-patch --stdout main..side >patch4 &&
 	sed -e "/^\$/q" patch4 >hdrs4 &&
-	grep "^To: R E Cipient <rcipient@example.com>,\$" hdrs4 &&
-	grep "^ *S E Cipient <scipient@example.com>\$" hdrs4
+	test_grep "^To: R E Cipient <rcipient@example.com>,\$" hdrs4 &&
+	test_grep "^ *S E Cipient <scipient@example.com>\$" hdrs4
 '
 
 test_expect_success 'additional command line cc (ascii)' '
 	git config --replace-all format.headers "Cc: R E Cipient <rcipient@example.com>" &&
 	git format-patch --cc="S E Cipient <scipient@example.com>" --stdout main..side >patch5 &&
 	sed -e "/^\$/q" patch5 >hdrs5 &&
-	grep "^Cc: R E Cipient <rcipient@example.com>,\$" hdrs5 &&
-	grep "^ *S E Cipient <scipient@example.com>\$" hdrs5
+	test_grep "^Cc: R E Cipient <rcipient@example.com>,\$" hdrs5 &&
+	test_grep "^ *S E Cipient <scipient@example.com>\$" hdrs5
 '
 
 test_expect_failure 'additional command line cc (rfc822)' '
 	git config --replace-all format.headers "Cc: R E Cipient <rcipient@example.com>" &&
 	git format-patch --cc="S. E. Cipient <scipient@example.com>" --stdout main..side >patch5 &&
 	sed -e "/^\$/q" patch5 >hdrs5 &&
-	grep "^Cc: R E Cipient <rcipient@example.com>,\$" hdrs5 &&
-	grep "^ *\"S. E. Cipient\" <scipient@example.com>\$" hdrs5
+	test_grep "^Cc: R E Cipient <rcipient@example.com>,\$" hdrs5 &&
+	test_grep "^ *\"S. E. Cipient\" <scipient@example.com>\$" hdrs5
 '
 
 test_expect_success 'command line headers' '
 	git config --unset-all format.headers &&
 	git format-patch --add-header="Cc: R E Cipient <rcipient@example.com>" --stdout main..side >patch6 &&
 	sed -e "/^\$/q" patch6 >hdrs6 &&
-	grep "^Cc: R E Cipient <rcipient@example.com>\$" hdrs6
+	test_grep "^Cc: R E Cipient <rcipient@example.com>\$" hdrs6
 '
 
 test_expect_success 'configuration headers and command line headers' '
 	git config --replace-all format.headers "Cc: R E Cipient <rcipient@example.com>" &&
 	git format-patch --add-header="Cc: S E Cipient <scipient@example.com>" --stdout main..side >patch7 &&
 	sed -e "/^\$/q" patch7 >hdrs7 &&
-	grep "^Cc: R E Cipient <rcipient@example.com>,\$" hdrs7 &&
-	grep "^ *S E Cipient <scipient@example.com>\$" hdrs7
+	test_grep "^Cc: R E Cipient <rcipient@example.com>,\$" hdrs7 &&
+	test_grep "^ *S E Cipient <scipient@example.com>\$" hdrs7
 '
 
 test_expect_success 'command line To: header (ascii)' '
 	git config --unset-all format.headers &&
 	git format-patch --to="R E Cipient <rcipient@example.com>" --stdout main..side >patch8 &&
 	sed -e "/^\$/q" patch8 >hdrs8 &&
-	grep "^To: R E Cipient <rcipient@example.com>\$" hdrs8
+	test_grep "^To: R E Cipient <rcipient@example.com>\$" hdrs8
 '
 
 test_expect_failure 'command line To: header (rfc822)' '
 	git format-patch --to="R. E. Cipient <rcipient@example.com>" --stdout main..side >patch8 &&
 	sed -e "/^\$/q" patch8 >hdrs8 &&
-	grep "^To: \"R. E. Cipient\" <rcipient@example.com>\$" hdrs8
+	test_grep "^To: \"R. E. Cipient\" <rcipient@example.com>\$" hdrs8
 '
 
 test_expect_failure 'command line To: header (rfc2047)' '
 	git format-patch --to="R Ä Cipient <rcipient@example.com>" --stdout main..side >patch8 &&
 	sed -e "/^\$/q" patch8 >hdrs8 &&
-	grep "^To: =?UTF-8?q?R=20=C3=84=20Cipient?= <rcipient@example.com>\$" hdrs8
+	test_grep "^To: =?UTF-8?q?R=20=C3=84=20Cipient?= <rcipient@example.com>\$" hdrs8
 '
 
 test_expect_success 'configuration To: header (ascii)' '
 	git config format.to "R E Cipient <rcipient@example.com>" &&
 	git format-patch --stdout main..side >patch9 &&
 	sed -e "/^\$/q" patch9 >hdrs9 &&
-	grep "^To: R E Cipient <rcipient@example.com>\$" hdrs9
+	test_grep "^To: R E Cipient <rcipient@example.com>\$" hdrs9
 '
 
 test_expect_failure 'configuration To: header (rfc822)' '
 	git config format.to "R. E. Cipient <rcipient@example.com>" &&
 	git format-patch --stdout main..side >patch9 &&
 	sed -e "/^\$/q" patch9 >hdrs9 &&
-	grep "^To: \"R. E. Cipient\" <rcipient@example.com>\$" hdrs9
+	test_grep "^To: \"R. E. Cipient\" <rcipient@example.com>\$" hdrs9
 '
 
 test_expect_failure 'configuration To: header (rfc2047)' '
 	git config format.to "R Ä Cipient <rcipient@example.com>" &&
 	git format-patch --stdout main..side >patch9 &&
 	sed -e "/^\$/q" patch9 >hdrs9 &&
-	grep "^To: =?UTF-8?q?R=20=C3=84=20Cipient?= <rcipient@example.com>\$" hdrs9
+	test_grep "^To: =?UTF-8?q?R=20=C3=84=20Cipient?= <rcipient@example.com>\$" hdrs9
 '
 
 # check_patch <patch>: Verify that <patch> looks like a half-sane
@@ -250,35 +250,35 @@
 	git -c format.from=false format-patch --stdout main..side >patch &&
 	sed -e "/^\$/q" patch >hdrs &&
 	check_patch patch &&
-	! grep "^From: C O Mitter <committer@example.com>\$" hdrs
+	test_grep ! "^From: C O Mitter <committer@example.com>\$" hdrs
 '
 
 test_expect_success 'format.from=true' '
 	git -c format.from=true format-patch --stdout main..side >patch &&
 	sed -e "/^\$/q" patch >hdrs &&
 	check_patch hdrs &&
-	grep "^From: C O Mitter <committer@example.com>\$" hdrs
+	test_grep "^From: C O Mitter <committer@example.com>\$" hdrs
 '
 
 test_expect_success 'format.from with address' '
 	git -c format.from="F R Om <from@example.com>" format-patch --stdout main..side >patch &&
 	sed -e "/^\$/q" patch >hdrs &&
 	check_patch hdrs &&
-	grep "^From: F R Om <from@example.com>\$" hdrs
+	test_grep "^From: F R Om <from@example.com>\$" hdrs
 '
 
 test_expect_success '--no-from overrides format.from' '
 	git -c format.from="F R Om <from@example.com>" format-patch --no-from --stdout main..side >patch &&
 	sed -e "/^\$/q" patch >hdrs &&
 	check_patch hdrs &&
-	! grep "^From: F R Om <from@example.com>\$" hdrs
+	test_grep ! "^From: F R Om <from@example.com>\$" hdrs
 '
 
 test_expect_success '--from overrides format.from' '
 	git -c format.from="F R Om <from@example.com>" format-patch --from --stdout main..side >patch &&
 	sed -e "/^\$/q" patch >hdrs &&
 	check_patch hdrs &&
-	! grep "^From: F R Om <from@example.com>\$" hdrs
+	test_grep ! "^From: F R Om <from@example.com>\$" hdrs
 '
 
 test_expect_success '--no-to overrides config.to' '
@@ -287,7 +287,7 @@
 	git format-patch --no-to --stdout main..side >patch10 &&
 	sed -e "/^\$/q" patch10 >hdrs10 &&
 	check_patch hdrs10 &&
-	! grep "^To: R E Cipient <rcipient@example.com>\$" hdrs10
+	test_grep ! "^To: R E Cipient <rcipient@example.com>\$" hdrs10
 '
 
 test_expect_success '--no-to and --to replaces config.to' '
@@ -297,8 +297,8 @@
 		--stdout main..side >patch11 &&
 	sed -e "/^\$/q" patch11 >hdrs11 &&
 	check_patch hdrs11 &&
-	! grep "^To: Someone <someone@out.there>\$" hdrs11 &&
-	grep "^To: Someone Else <else@out.there>\$" hdrs11
+	test_grep ! "^To: Someone <someone@out.there>\$" hdrs11 &&
+	test_grep "^To: Someone Else <else@out.there>\$" hdrs11
 '
 
 test_expect_success '--no-cc overrides config.cc' '
@@ -307,7 +307,7 @@
 	git format-patch --no-cc --stdout main..side >patch12 &&
 	sed -e "/^\$/q" patch12 >hdrs12 &&
 	check_patch hdrs12 &&
-	! grep "^Cc: C E Cipient <rcipient@example.com>\$" hdrs12
+	test_grep ! "^Cc: C E Cipient <rcipient@example.com>\$" hdrs12
 '
 
 test_expect_success '--no-add-header overrides config.headers' '
@@ -316,7 +316,7 @@
 	git format-patch --no-add-header --stdout main..side >patch13 &&
 	sed -e "/^\$/q" patch13 >hdrs13 &&
 	check_patch hdrs13 &&
-	! grep "^Header1: B E Cipient <rcipient@example.com>\$" hdrs13
+	test_grep ! "^Header1: B E Cipient <rcipient@example.com>\$" hdrs13
 '
 
 test_expect_success 'multiple files' '
@@ -508,41 +508,41 @@
 test_expect_success 'reroll count' '
 	rm -fr patches &&
 	git format-patch -o patches --cover-letter --reroll-count 4 main..side >list &&
-	! grep -v "^patches/v4-000[0-3]-" list &&
+	test_grep ! -v "^patches/v4-000[0-3]-" list &&
 	sed -n -e "/^Subject: /p" $(cat list) >subjects &&
-	! grep -v "^Subject: \[PATCH v4 [0-3]/3\] " subjects
+	test_grep ! -v "^Subject: \[PATCH v4 [0-3]/3\] " subjects
 '
 
 test_expect_success 'reroll count (-v)' '
 	rm -fr patches &&
 	git format-patch -o patches --cover-letter -v4 main..side >list &&
-	! grep -v "^patches/v4-000[0-3]-" list &&
+	test_grep ! -v "^patches/v4-000[0-3]-" list &&
 	sed -n -e "/^Subject: /p" $(cat list) >subjects &&
-	! grep -v "^Subject: \[PATCH v4 [0-3]/3\] " subjects
+	test_grep ! -v "^Subject: \[PATCH v4 [0-3]/3\] " subjects
 '
 
 test_expect_success 'reroll count (-v) with a fractional number' '
 	rm -fr patches &&
 	git format-patch -o patches --cover-letter -v4.4 main..side >list &&
-	! grep -v "^patches/v4.4-000[0-3]-" list &&
+	test_grep ! -v "^patches/v4.4-000[0-3]-" list &&
 	sed -n -e "/^Subject: /p" $(cat list) >subjects &&
-	! grep -v "^Subject: \[PATCH v4.4 [0-3]/3\] " subjects
+	test_grep ! -v "^Subject: \[PATCH v4.4 [0-3]/3\] " subjects
 '
 
 test_expect_success 'reroll (-v) count with a non number' '
 	rm -fr patches &&
 	git format-patch -o patches --cover-letter -v4rev2 main..side >list &&
-	! grep -v "^patches/v4rev2-000[0-3]-" list &&
+	test_grep ! -v "^patches/v4rev2-000[0-3]-" list &&
 	sed -n -e "/^Subject: /p" $(cat list) >subjects &&
-	! grep -v "^Subject: \[PATCH v4rev2 [0-3]/3\] " subjects
+	test_grep ! -v "^Subject: \[PATCH v4rev2 [0-3]/3\] " subjects
 '
 
 test_expect_success 'reroll (-v) count with a non-pathname character' '
 	rm -fr patches &&
 	git format-patch -o patches --cover-letter -v4---..././../--1/.2//  main..side >list &&
-	! grep -v "patches/v4-\.-\.-\.-1-\.2-000[0-3]-" list &&
+	test_grep ! -v "patches/v4-\.-\.-\.-1-\.2-000[0-3]-" list &&
 	sed -n -e "/^Subject: /p" $(cat list) >subjects &&
-	! grep -v "^Subject: \[PATCH v4---\.\.\./\./\.\./--1/\.2// [0-3]/3\] " subjects
+	test_grep ! -v "^Subject: \[PATCH v4---\.\.\./\./\.\./--1/\.2// [0-3]/3\] " subjects
 '
 
 check_threading () {
@@ -813,9 +813,9 @@
 	git commit -m foo &&
 	git format-patch --no-renames --cover-letter -1 &&
 	check_patch 0000-cover-letter.patch &&
-	! grep "file => foo .* 0 *\$" 0000-cover-letter.patch &&
+	test_grep ! "file => foo .* 0 *\$" 0000-cover-letter.patch &&
 	git format-patch --cover-letter -1 -M &&
-	grep "file => foo .* 0 *\$" 0000-cover-letter.patch
+	test_grep "file => foo .* 0 *\$" 0000-cover-letter.patch
 '
 
 cat >expect <<EOF
@@ -924,13 +924,13 @@
 
 test_expect_success 'format-patch --in-reply-to' '
 	git format-patch -1 --stdout --in-reply-to "baz@foo.bar" >patch8 &&
-	grep "^In-Reply-To: <baz@foo.bar>" patch8 &&
-	grep "^References: <baz@foo.bar>" patch8
+	test_grep "^In-Reply-To: <baz@foo.bar>" patch8 &&
+	test_grep "^References: <baz@foo.bar>" patch8
 '
 
 test_expect_success 'format-patch --signoff' '
 	git format-patch -1 --signoff --stdout >out &&
-	grep "^Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" out
+	test_grep "^Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" out
 '
 
 test_expect_success 'format-patch --notes --signoff' '
@@ -949,27 +949,27 @@
 	git notes add -m "notes config message" HEAD &&
 
 	git format-patch -1 --stdout >out &&
-	! grep "notes config message" out &&
+	test_grep ! "notes config message" out &&
 	git format-patch -1 --stdout --notes >out &&
-	grep "notes config message" out &&
+	test_grep "notes config message" out &&
 	git format-patch -1 --stdout --no-notes >out &&
-	! grep "notes config message" out &&
+	test_grep ! "notes config message" out &&
 	git format-patch -1 --stdout --notes --no-notes >out &&
-	! grep "notes config message" out &&
+	test_grep ! "notes config message" out &&
 	git format-patch -1 --stdout --no-notes --notes >out &&
-	grep "notes config message" out &&
+	test_grep "notes config message" out &&
 
 	test_config format.notes true &&
 	git format-patch -1 --stdout >out &&
-	grep "notes config message" out &&
+	test_grep "notes config message" out &&
 	git format-patch -1 --stdout --notes >out &&
-	grep "notes config message" out &&
+	test_grep "notes config message" out &&
 	git format-patch -1 --stdout --no-notes >out &&
-	! grep "notes config message" out &&
+	test_grep ! "notes config message" out &&
 	git format-patch -1 --stdout --notes --no-notes >out &&
-	! grep "notes config message" out &&
+	test_grep ! "notes config message" out &&
 	git format-patch -1 --stdout --no-notes --notes >out &&
-	grep "notes config message" out
+	test_grep "notes config message" out
 '
 
 test_expect_success 'format-patch with multiple notes refs' '
@@ -979,39 +979,39 @@
 	git notes --ref note2 add -m "this is note 2" HEAD &&
 
 	git format-patch -1 --stdout >out &&
-	! grep "this is note 1" out &&
-	! grep "this is note 2" out &&
+	test_grep ! "this is note 1" out &&
+	test_grep ! "this is note 2" out &&
 	git format-patch -1 --stdout --notes=note1 >out &&
-	grep "this is note 1" out &&
-	! grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep ! "this is note 2" out &&
 	git format-patch -1 --stdout --notes=note2 >out &&
-	! grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep ! "this is note 1" out &&
+	test_grep "this is note 2" out &&
 	git format-patch -1 --stdout --notes=note1 --notes=note2 >out &&
-	grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep "this is note 2" out &&
 
 	test_config format.notes note1 &&
 	git format-patch -1 --stdout >out &&
-	grep "this is note 1" out &&
-	! grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep ! "this is note 2" out &&
 	git format-patch -1 --stdout --no-notes >out &&
-	! grep "this is note 1" out &&
-	! grep "this is note 2" out &&
+	test_grep ! "this is note 1" out &&
+	test_grep ! "this is note 2" out &&
 	git format-patch -1 --stdout --notes=note2 >out &&
-	grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep "this is note 2" out &&
 	git format-patch -1 --stdout --no-notes --notes=note2 >out &&
-	! grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep ! "this is note 1" out &&
+	test_grep "this is note 2" out &&
 
 	git config --add format.notes note2 &&
 	git format-patch -1 --stdout >out &&
-	grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep "this is note 2" out &&
 	git format-patch -1 --stdout --no-notes >out &&
-	! grep "this is note 1" out &&
-	! grep "this is note 2" out
+	test_grep ! "this is note 1" out &&
+	test_grep ! "this is note 2" out
 '
 
 test_expect_success 'format-patch with multiple notes refs in config' '
@@ -1024,26 +1024,26 @@
 
 	git config format.notes note1 &&
 	git format-patch -1 --stdout >out &&
-	grep "this is note 1" out &&
-	! grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep ! "this is note 2" out &&
 	git config format.notes note2 &&
 	git format-patch -1 --stdout >out &&
-	! grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep ! "this is note 1" out &&
+	test_grep "this is note 2" out &&
 	git config --add format.notes note1 &&
 	git format-patch -1 --stdout >out &&
-	grep "this is note 1" out &&
-	grep "this is note 2" out &&
+	test_grep "this is note 1" out &&
+	test_grep "this is note 2" out &&
 
 	git config --replace-all format.notes note1 &&
 	git config --add format.notes false &&
 	git format-patch -1 --stdout >out &&
-	! grep "this is note 1" out &&
-	! grep "this is note 2" out &&
+	test_grep ! "this is note 1" out &&
+	test_grep ! "this is note 2" out &&
 	git config --add format.notes note2 &&
 	git format-patch -1 --stdout >out &&
-	! grep "this is note 1" out &&
-	grep "this is note 2" out
+	test_grep ! "this is note 1" out &&
+	test_grep "this is note 2" out
 '
 
 echo "fatal: --name-only does not make sense" >expect.name-only
@@ -1095,7 +1095,7 @@
 
 	git format-patch main..pathspec -- file_a >output &&
 	test_cmp expect output &&
-	! grep file_b *.patch
+	test_grep ! file_b *.patch
 '
 
 test_expect_success 'format-patch --ignore-if-in-upstream HEAD' '
@@ -1129,14 +1129,14 @@
 test_expect_success 'format-patch with format.signature config' '
 	git config format.signature "config sig" &&
 	git format-patch --stdout -1 >output &&
-	grep "config sig" output
+	test_grep "config sig" output
 '
 
 test_expect_success 'format-patch --signature overrides format.signature' '
 	git config format.signature "config sig" &&
 	git format-patch --stdout --signature="overrides" -1 >output &&
-	! grep "config sig" output &&
-	grep "overrides" output
+	test_grep ! "config sig" output &&
+	test_grep "overrides" output
 '
 
 test_expect_success 'format-patch --no-signature ignores format.signature' '
@@ -1144,9 +1144,9 @@
 	git format-patch --stdout --signature="my sig" --no-signature \
 		-1 >output &&
 	check_patch output &&
-	! grep "config sig" output &&
-	! grep "my sig" output &&
-	! grep "^-- \$" output
+	test_grep ! "config sig" output &&
+	test_grep ! "my sig" output &&
+	test_grep ! "^-- \$" output
 '
 
 test_expect_success 'format-patch --signature --cover-letter' '
@@ -1161,20 +1161,20 @@
 	git config format.signature "" &&
 	git format-patch --stdout -1 >output &&
 	check_patch output &&
-	! grep "^-- \$" output
+	test_grep ! "^-- \$" output
 '
 
 test_expect_success 'format-patch --no-signature suppresses signatures' '
 	git config --unset-all format.signature &&
 	git format-patch --stdout --no-signature -1 >output &&
 	check_patch output &&
-	! grep "^-- \$" output
+	test_grep ! "^-- \$" output
 '
 
 test_expect_success 'format-patch --signature="" suppresses signatures' '
 	git format-patch --stdout --signature="" -1 >output &&
 	check_patch output &&
-	! grep "^-- \$" output
+	test_grep ! "^-- \$" output
 '
 
 test_expect_success 'prepare mail-signature input' '
@@ -1213,7 +1213,7 @@
 	test_config format.signaturefile mail-signature &&
 	git format-patch --stdout --no-signature -1 >output &&
 	check_patch output &&
-	! grep "^-- \$" output
+	test_grep ! "^-- \$" output
 '
 
 test_expect_success '--signature-file overrides format.signaturefile' '
@@ -1235,7 +1235,7 @@
 	test_config format.signaturefile mail-signature &&
 	git format-patch --stdout --signature="my sig" -1 >output &&
 	check_patch output &&
-	grep "my sig" output
+	test_grep "my sig" output
 '
 
 test_expect_success TTY 'format-patch --stdout paginates' '
@@ -2001,10 +2001,10 @@
 	test_config format.coverFromDescription default &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description default' '
@@ -2013,10 +2013,10 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description default main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with format.coverFromDescription = none' '
@@ -2026,10 +2026,10 @@
 	test_config format.coverFromDescription none &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	! grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep ! "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description none' '
@@ -2038,10 +2038,10 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description none main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	! grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep ! "^body$" actual
 '
 
 test_expect_success 'cover letter with format.coverFromDescription = message' '
@@ -2051,10 +2051,10 @@
 	test_config format.coverFromDescription message &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description message' '
@@ -2063,10 +2063,10 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description message main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with format.coverFromDescription = subject' '
@@ -2076,10 +2076,10 @@
 	test_config format.coverFromDescription subject &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description subject' '
@@ -2088,10 +2088,10 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description subject main >actual &&
-	grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description subject (UTF-8 subject line)' '
@@ -2100,8 +2100,8 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description subject --encode-email-headers main >actual &&
-	grep "^Subject: \[PATCH 0/2\] =?UTF-8?q?Caf=C3=A9=3F?=$" actual &&
-	! grep "Café" actual
+	test_grep "^Subject: \[PATCH 0/2\] =?UTF-8?q?Caf=C3=A9=3F?=$" actual &&
+	test_grep ! "Café" actual
 '
 
 test_expect_success 'cover letter with format.coverFromDescription = auto (short subject line)' '
@@ -2111,10 +2111,10 @@
 	test_config format.coverFromDescription auto &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description auto (short subject line)' '
@@ -2123,10 +2123,10 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description auto main >actual &&
-	grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with format.coverFromDescription = auto (long subject line)' '
@@ -2136,10 +2136,10 @@
 	test_config format.coverFromDescription auto &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	grep "^this is a really long first line and it is over 100 characters long which is the threshold for long subjects$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep "^this is a really long first line and it is over 100 characters long which is the threshold for long subjects$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with --cover-from-description auto (long subject line)' '
@@ -2148,10 +2148,10 @@
 body" &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description auto main >actual &&
-	grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	grep "^this is a really long first line and it is over 100 characters long which is the threshold for long subjects$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] \*\*\* SUBJECT HERE \*\*\*$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep "^this is a really long first line and it is over 100 characters long which is the threshold for long subjects$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter with command-line --cover-from-description overrides config' '
@@ -2161,52 +2161,52 @@
 	test_config format.coverFromDescription none &&
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description subject main >actual &&
-	grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
-	! grep "^\*\*\* BLURB HERE \*\*\*$" actual &&
-	! grep "^config subject$" actual &&
-	grep "^body$" actual
+	test_grep "^Subject: \[PATCH 0/2\] config subject$" actual &&
+	test_grep ! "^\*\*\* BLURB HERE \*\*\*$" actual &&
+	test_grep ! "^config subject$" actual &&
+	test_grep "^body$" actual
 '
 
 test_expect_success 'cover letter using branch description (1)' '
 	git checkout rebuild-1 &&
 	test_config branch.rebuild-1.description hello &&
 	git format-patch --stdout --cover-letter main >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'cover letter using branch description (2)' '
 	git checkout rebuild-1 &&
 	test_config branch.rebuild-1.description hello &&
 	git format-patch --stdout --cover-letter rebuild-1~2..rebuild-1 >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'cover letter using branch description (3)' '
 	git checkout rebuild-1 &&
 	test_config branch.rebuild-1.description hello &&
 	git format-patch --stdout --cover-letter ^main rebuild-1 >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'cover letter using branch description (4)' '
 	git checkout rebuild-1 &&
 	test_config branch.rebuild-1.description hello &&
 	git format-patch --stdout --cover-letter main.. >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'cover letter using branch description (5)' '
 	git checkout rebuild-1 &&
 	test_config branch.rebuild-1.description hello &&
 	git format-patch --stdout --cover-letter -2 HEAD >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'cover letter using branch description (6)' '
 	git checkout rebuild-1 &&
 	test_config branch.rebuild-1.description hello &&
 	git format-patch --stdout --cover-letter -2 >actual &&
-	grep hello actual
+	test_grep hello actual
 '
 
 test_expect_success 'cover letter with --description-file' '
@@ -2219,8 +2219,8 @@
 	git checkout rebuild-1 &&
 	git format-patch --stdout --cover-letter --cover-from-description auto \
 		--description-file description.txt main >actual &&
-	grep "^Subject: \[PATCH 0/2\] subject from file$" actual &&
-	grep "^body from file$" actual
+	test_grep "^Subject: \[PATCH 0/2\] subject from file$" actual &&
+	test_grep "^body from file$" actual
 '
 
 test_expect_success 'cover letter with nothing' '
@@ -2467,7 +2467,7 @@
 test_expect_success 'format-patch format.useAutoBase whenAble history involves criss-cross' '
 	test_config format.useAutoBase whenAble &&
 	git format-patch -1 >patch &&
-	! grep "^base-commit:" patch
+	test_grep ! "^base-commit:" patch
 '
 
 test_expect_success 'format-patch format.useAutoBase option' '
@@ -2502,13 +2502,13 @@
 test_expect_success 'format-patch --no-base overrides format.useAutoBase' '
 	test_config format.useAutoBase true &&
 	git format-patch --stdout --no-base -1 >patch &&
-	! grep "^base-commit:" patch
+	test_grep ! "^base-commit:" patch
 '
 
 test_expect_success 'format-patch --no-base overrides format.useAutoBase whenAble' '
 	test_config format.useAutoBase whenAble &&
 	git format-patch --stdout --no-base -1 >patch &&
-	! grep "^base-commit:" patch
+	test_grep ! "^base-commit:" patch
 '
 
 test_expect_success 'format-patch --base with --attach' '
@@ -2521,7 +2521,7 @@
 test_expect_success 'format-patch --attach cover-letter only is non-multipart' '
 	test_when_finished "rm -fr patches" &&
 	git format-patch -o patches --cover-letter --attach=mimemime --base=HEAD~ -1 &&
-	! grep -E "^--+mimemime" patches/0000*.patch &&
+	test_grep ! -E "^--+mimemime" patches/0000*.patch &&
 	grep -E "^--+mimemime$" patches/0001*.patch >output &&
 	test_line_count = 2 output &&
 	grep -E "^--+mimemime--$" patches/0001*.patch >output &&
@@ -2533,7 +2533,7 @@
 	separator=attachment-separator &&
 	test_config format.attach "$separator" &&
 	filename=$(git format-patch -o patches -1) &&
-	grep "^Content-Type: multipart/.*$separator" "$filename"
+	test_grep "^Content-Type: multipart/.*$separator" "$filename"
 '
 
 test_expect_success 'format-patch with format.attach=disabled' '
@@ -2543,7 +2543,7 @@
 	test_config format.attach "" &&
 	filename=$(git format-patch -o patches -1) &&
 	# The output should not even declare content type for text/plain.
-	! grep "^Content-Type: multipart/" "$filename"
+	test_grep ! "^Content-Type: multipart/" "$filename"
 '
 
 test_expect_success '-c format.mboxrd format-patch' '
@@ -2674,23 +2674,23 @@
 
 test_expect_success 'format-patch does not respect diff.noprefix' '
 	git -c diff.noprefix format-patch -1 --stdout >actual &&
-	grep "^--- a/blorp" actual
+	test_grep "^--- a/blorp" actual
 '
 
 test_expect_success 'format-patch respects format.noprefix' '
 	git -c format.noprefix format-patch -1 --stdout >actual &&
-	grep "^--- blorp" actual
+	test_grep "^--- blorp" actual
 '
 
 test_expect_success 'format.noprefix=false' '
 	git -c format.noprefix=false format-patch -1 --stdout >actual &&
-	grep "^--- a/blorp" actual
+	test_grep "^--- a/blorp" actual
 '
 
 test_expect_success 'format-patch --default-prefix overrides format.noprefix' '
 	git -c format.noprefix \
 		format-patch -1 --default-prefix --stdout >actual &&
-	grep "^--- a/blorp" actual
+	test_grep "^--- a/blorp" actual
 '
 
 test_expect_success 'errors on format.noprefix which is not boolean' '
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index b691d29..6e7ba8a 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -638,14 +638,14 @@
 	# This is indented with SP HT SP.
 	echo " 	 foo();" >x &&
 	test_must_fail git diff --check >check &&
-	grep "space before tab in indent" check
+	test_grep "space before tab in indent" check
 '
 
 test_expect_success 'check mixed tabs and spaces in indent' '
 	# This is indented with HT SP HT.
 	echo "	 	foo();" >x &&
 	test_must_fail git diff --check >check &&
-	grep "space before tab in indent" check
+	test_grep "space before tab in indent" check
 '
 
 test_expect_success 'check with no whitespace errors' '
@@ -891,14 +891,14 @@
 	echo "" >x &&
 	echo "foo(); " >>x &&
 	test_must_fail git diff --check >check &&
-	grep "x:2:" check
+	test_grep "x:2:" check
 '
 
 test_expect_success 'checkdiff detects new trailing blank lines (1)' '
 	echo "foo();" >x &&
 	echo "" >>x &&
 	test_must_fail git diff --check >check &&
-	grep "new blank line" check
+	test_grep "new blank line" check
 '
 
 test_expect_success 'checkdiff detects new trailing blank lines (2)' '
@@ -906,7 +906,7 @@
 	git add x &&
 	test_write_lines a "" "" "" "" >x &&
 	test_must_fail git diff --check >check &&
-	grep "new blank line" check
+	test_grep "new blank line" check
 '
 
 test_expect_success 'checkdiff allows new blank lines' '
@@ -1018,7 +1018,7 @@
 
 	git diff >actual.raw &&
 	sed -e "1,/^@@@/d" actual.raw >actual &&
-	! grep "^-" actual
+	test_grep ! "^-" actual
 
 '
 
@@ -2166,8 +2166,8 @@
 
 	# no move detection as the moved line is across repository boundaries.
 	test_decode_color <actual >decoded_actual &&
-	! grep BGREEN decoded_actual &&
-	! grep BRED decoded_actual &&
+	test_grep ! BGREEN decoded_actual &&
+	test_grep ! BRED decoded_actual &&
 
 	# nor did we mess with it another way
 	git diff --submodule=diff --color >expect.raw &&
diff --git a/t/t4017-diff-retval.sh b/t/t4017-diff-retval.sh
index c2863c9..5ec6f5c 100755
--- a/t/t4017-diff-retval.sh
+++ b/t/t4017-diff-retval.sh
@@ -139,7 +139,7 @@
 
 test_expect_success 'option errors are not confused by --exit-code' '
 	test_must_fail git diff --exit-code --nonsense 2>err &&
-	grep '^usage:' err
+	test_grep '^usage:' err
 '
 
 for option in --exit-code --quiet
diff --git a/t/t4018-diff-funcname.sh b/t/t4018-diff-funcname.sh
index e026fac..b538b71 100755
--- a/t/t4018-diff-funcname.sh
+++ b/t/t4018-diff-funcname.sh
@@ -115,7 +115,7 @@
 do
 	test_expect_success "hunk header: $i" "
 		git diff -U1 $i >actual &&
-		grep '@@ .* @@.*RIGHT' actual
+		test_grep '@@ .* @@.*RIGHT' actual
 	"
 done
 
diff --git a/t/t4018/swift-actor b/t/t4018/swift-actor
new file mode 100644
index 0000000..e4852f4
--- /dev/null
+++ b/t/t4018/swift-actor
@@ -0,0 +1,5 @@
+actor RIGHT {
+    let a = 1
+    // a comment
+    let b = ChangeMe
+}
diff --git a/t/t4018/swift-attribute-with-args b/t/t4018/swift-attribute-with-args
new file mode 100644
index 0000000..22b1ee3
--- /dev/null
+++ b/t/t4018/swift-attribute-with-args
@@ -0,0 +1,7 @@
+struct View {
+    @available(iOS 13, *) public func RIGHT() {
+        let a = 1
+        // a comment
+        print(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-class b/t/t4018/swift-class
new file mode 100644
index 0000000..c3a9336
--- /dev/null
+++ b/t/t4018/swift-class
@@ -0,0 +1,5 @@
+class RIGHT {
+    let a = 1
+    // a comment
+    let b = ChangeMe
+}
diff --git a/t/t4018/swift-enum b/t/t4018/swift-enum
new file mode 100644
index 0000000..0a84302
--- /dev/null
+++ b/t/t4018/swift-enum
@@ -0,0 +1,5 @@
+enum RIGHT {
+    case first
+    // a comment
+    case ChangeMe
+}
diff --git a/t/t4018/swift-extension b/t/t4018/swift-extension
new file mode 100644
index 0000000..cbc18ab
--- /dev/null
+++ b/t/t4018/swift-extension
@@ -0,0 +1,5 @@
+extension RIGHT {
+    static let a = 1
+    // a comment
+    static let b = ChangeMe
+}
diff --git a/t/t4018/swift-failable-init b/t/t4018/swift-failable-init
new file mode 100644
index 0000000..4bbd621
--- /dev/null
+++ b/t/t4018/swift-failable-init
@@ -0,0 +1,7 @@
+class Bar {
+    init?(RIGHT: Int) {
+        let x = 0
+        // a comment
+        print(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-func b/t/t4018/swift-func
new file mode 100644
index 0000000..1fecae0
--- /dev/null
+++ b/t/t4018/swift-func
@@ -0,0 +1,5 @@
+func RIGHT(x: Int) -> Int {
+    let y = x
+    // a comment
+    return ChangeMe
+}
diff --git a/t/t4018/swift-generic-subscript b/t/t4018/swift-generic-subscript
new file mode 100644
index 0000000..423cb58
--- /dev/null
+++ b/t/t4018/swift-generic-subscript
@@ -0,0 +1,7 @@
+struct Container {
+    subscript<RIGHT>(index: Int) -> Int {
+        let a = 0
+        // a comment
+        return ChangeMe
+    }
+}
diff --git a/t/t4018/swift-init b/t/t4018/swift-init
new file mode 100644
index 0000000..dc7a298
--- /dev/null
+++ b/t/t4018/swift-init
@@ -0,0 +1,7 @@
+class Foo {
+    init(RIGHT: Int) {
+        let x = 0
+        // a comment
+        print(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-inline-attribute b/t/t4018/swift-inline-attribute
new file mode 100644
index 0000000..2374c4b
--- /dev/null
+++ b/t/t4018/swift-inline-attribute
@@ -0,0 +1,7 @@
+class Service {
+    @objc func RIGHT() {
+        let path = "/api"
+        // a comment
+        log(ChangeMe)
+    }
+}
diff --git a/t/t4018/swift-modifiers b/t/t4018/swift-modifiers
new file mode 100644
index 0000000..9d80685
--- /dev/null
+++ b/t/t4018/swift-modifiers
@@ -0,0 +1,4 @@
+public static func RIGHT() -> Int {
+    // a comment
+    return ChangeMe
+}
diff --git a/t/t4018/swift-protocol b/t/t4018/swift-protocol
new file mode 100644
index 0000000..07c39ec
--- /dev/null
+++ b/t/t4018/swift-protocol
@@ -0,0 +1,5 @@
+protocol RIGHT {
+    var first: Int { get }
+    // a comment
+    var second: ChangeMe { get }
+}
diff --git a/t/t4018/swift-struct b/t/t4018/swift-struct
new file mode 100644
index 0000000..e399ed7
--- /dev/null
+++ b/t/t4018/swift-struct
@@ -0,0 +1,5 @@
+struct RIGHT {
+    let a = 1
+    // a comment
+    let b = ChangeMe
+}
diff --git a/t/t4019-diff-wserror.sh b/t/t4019-diff-wserror.sh
index 4001dac..939ec8a 100755
--- a/t/t4019-diff-wserror.sh
+++ b/t/t4019-diff-wserror.sh
@@ -251,8 +251,8 @@
 
 	rm -f .gitattributes &&
 	test_must_fail git diff --check >output &&
-	grep "new blank line at" output &&
-	grep "trailing whitespace" output
+	test_grep "new blank line at" output &&
+	test_grep "trailing whitespace" output
 
 '
 
@@ -280,8 +280,8 @@
 	git add G &&
 	echo BBBQ | tr Q "\015" >>G &&
 	git diff --color G | tr "\015" Q >output &&
-	grep "BBB.*${blue_grep}Q" output &&
-	grep "AAA.*\[mQ" output
+	test_grep "BBB.*${blue_grep}Q" output &&
+	test_grep "AAA.*\[mQ" output
 
 '
 
diff --git a/t/t4020-diff-external.sh b/t/t4020-diff-external.sh
index 7ec5854..93d3723 100755
--- a/t/t4020-diff-external.sh
+++ b/t/t4020-diff-external.sh
@@ -34,13 +34,13 @@
 
 test_expect_success 'GIT_EXTERNAL_DIFF environment should apply only to diff' '
 	GIT_EXTERNAL_DIFF=echo git log -p -1 HEAD >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 
 '
 
 test_expect_success 'GIT_EXTERNAL_DIFF environment and --no-ext-diff' '
 	GIT_EXTERNAL_DIFF=echo git diff --no-ext-diff >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 
 '
 
@@ -86,13 +86,13 @@
 test_expect_success 'diff.external should apply only to diff' '
 	test_config diff.external echo &&
 	git log -p -1 HEAD >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 '
 
 test_expect_success 'diff.external and --no-ext-diff' '
 	test_config diff.external echo &&
 	git diff --no-ext-diff >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 '
 
 test_expect_success 'diff attribute' '
@@ -113,13 +113,13 @@
 
 test_expect_success 'diff attribute should apply only to diff' '
 	git log -p -1 HEAD >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 
 '
 
 test_expect_success 'diff attribute and --no-ext-diff' '
 	git diff --no-ext-diff >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 
 '
 
@@ -140,13 +140,13 @@
 
 test_expect_success 'diff attribute should apply only to diff' '
 	git log -p -1 HEAD >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 
 '
 
 test_expect_success 'diff attribute and --no-ext-diff' '
 	git diff --no-ext-diff >out &&
-	grep "^diff --git a/file b/file" out
+	test_grep "^diff --git a/file b/file" out
 
 '
 
@@ -178,7 +178,7 @@
 test_expect_success 'no diff with -diff' '
 	echo >.gitattributes "file -diff" &&
 	git diff >out &&
-	grep Binary out
+	test_grep Binary out
 '
 
 check_external_diff () {
diff --git a/t/t4021-format-patch-numbered.sh b/t/t4021-format-patch-numbered.sh
index 9be65fd..204851f 100755
--- a/t/t4021-format-patch-numbered.sh
+++ b/t/t4021-format-patch-numbered.sh
@@ -94,7 +94,7 @@
 test_expect_success 'format.numbered && --keep-subject' '
 
 	git format-patch --keep-subject --stdout HEAD^ >patch4a &&
-	grep "^Subject: Third" patch4a
+	test_grep "^Subject: Third" patch4a
 
 '
 
@@ -123,7 +123,7 @@
 test_expect_success '--start-number && --numbered' '
 
 	git format-patch --start-number 3 --numbered --stdout HEAD~1 > patch8 &&
-	grep "^Subject: \[PATCH 3/3\]" patch8
+	test_grep "^Subject: \[PATCH 3/3\]" patch8
 '
 
 test_expect_success 'single patch with cover-letter defaults to numbers' '
diff --git a/t/t4022-diff-rewrite.sh b/t/t4022-diff-rewrite.sh
index 6fed993..b5c1b68 100755
--- a/t/t4022-diff-rewrite.sh
+++ b/t/t4022-diff-rewrite.sh
@@ -58,13 +58,13 @@
 test_expect_success 'show deletion diff with -B' '
 
 	git diff -B -- test >actual &&
-	grep "Linus Torvalds" actual
+	test_grep "Linus Torvalds" actual
 '
 
 test_expect_success 'suppress deletion diff with -B -D' '
 
 	git diff -B -D -- test >actual &&
-	grep -v "Linus Torvalds" actual
+	test_grep -v "Linus Torvalds" actual
 '
 
 test_expect_success 'prepare a file that ends with an incomplete line' '
@@ -82,19 +82,19 @@
 
 test_expect_success 'confirm that sequence file is considered a rewrite' '
 	git diff -B seq >res &&
-	grep "dissimilarity index" res
+	test_grep "dissimilarity index" res
 '
 
 test_expect_success 'no newline at eof is on its own line without -B' '
 	git diff seq >res &&
-	grep "^\\\\ " res &&
-	! grep "^..*\\\\ " res
+	test_grep "^\\\\ " res &&
+	test_grep ! "^..*\\\\ " res
 '
 
 test_expect_success 'no newline at eof is on its own line with -B' '
 	git diff -B seq >res &&
-	grep "^\\\\ " res &&
-	! grep "^..*\\\\ " res
+	test_grep "^\\\\ " res &&
+	test_grep ! "^..*\\\\ " res
 '
 
 test_done
diff --git a/t/t4028-format-patch-mime-headers.sh b/t/t4028-format-patch-mime-headers.sh
index a06a747..39344cf 100755
--- a/t/t4028-format-patch-mime-headers.sh
+++ b/t/t4028-format-patch-mime-headers.sh
@@ -17,15 +17,15 @@
 test_expect_success 'patch has mime headers' '
 	rm -f 0001-two.patch &&
 	git format-patch HEAD^ &&
-	grep -i "content-type: text/plain; charset=utf-8" 0001-two.patch
+	test_grep -i "content-type: text/plain; charset=utf-8" 0001-two.patch
 '
 
 test_expect_success 'patch has mime and extra headers' '
 	rm -f 0001-two.patch &&
 	git config format.headers "x-foo: bar" &&
 	git format-patch HEAD^ &&
-	grep -i "x-foo: bar" 0001-two.patch &&
-	grep -i "content-type: text/plain; charset=utf-8" 0001-two.patch
+	test_grep -i "x-foo: bar" 0001-two.patch &&
+	test_grep -i "content-type: text/plain; charset=utf-8" 0001-two.patch
 '
 
 test_done
diff --git a/t/t4031-diff-rewrite-binary.sh b/t/t4031-diff-rewrite-binary.sh
index 15e012c..5b31ca0 100755
--- a/t/t4031-diff-rewrite-binary.sh
+++ b/t/t4031-diff-rewrite-binary.sh
@@ -29,32 +29,32 @@
 
 test_expect_success 'vanilla diff is binary' '
 	git diff >diff &&
-	grep "Binary files a/file and b/file differ" diff
+	test_grep "Binary files a/file and b/file differ" diff
 '
 
 test_expect_success 'rewrite diff is binary' '
 	git diff -B >diff &&
-	grep "dissimilarity index" diff &&
-	grep "Binary files a/file and b/file differ" diff
+	test_grep "dissimilarity index" diff &&
+	test_grep "Binary files a/file and b/file differ" diff
 '
 
 test_expect_success 'rewrite diff can show binary patch' '
 	git diff -B --binary >diff &&
-	grep "dissimilarity index" diff &&
-	grep "GIT binary patch" diff
+	test_grep "dissimilarity index" diff &&
+	test_grep "GIT binary patch" diff
 '
 
 test_expect_success 'rewrite diff --numstat shows binary changes' '
 	git diff -B --numstat --summary >diff &&
-	grep -e "-	-	" diff &&
-	grep " rewrite file" diff
+	test_grep -e "-	-	" diff &&
+	test_grep " rewrite file" diff
 '
 
 test_expect_success 'diff --stat counts binary rewrite as 0 lines' '
 	git diff -B --stat --summary >diff &&
-	grep "Bin" diff &&
+	test_grep "Bin" diff &&
 	test_grep "0 insertions.*0 deletions" diff &&
-	grep " rewrite file" diff
+	test_grep " rewrite file" diff
 '
 
 test_expect_success 'setup textconv' '
diff --git a/t/t4033-diff-patience.sh b/t/t4033-diff-patience.sh
index 113304d..8002c0f 100755
--- a/t/t4033-diff-patience.sh
+++ b/t/t4033-diff-patience.sh
@@ -10,7 +10,7 @@
 	printf "a\nbX\nc\n" >post &&
 	test_must_fail git diff --no-index \
 		--patience --ignore-space-at-eol pre post >diff &&
-	grep "^+.*X" diff
+	test_grep "^+.*X" diff
 '
 
 test_diff_frobnitz "patience"
diff --git a/t/t4036-format-patch-signer-mime.sh b/t/t4036-format-patch-signer-mime.sh
index 98d9713..f57d3dc 100755
--- a/t/t4036-format-patch-signer-mime.sh
+++ b/t/t4036-format-patch-signer-mime.sh
@@ -19,14 +19,14 @@
 test_expect_success 'format normally' '
 
 	git format-patch --stdout -1 >output &&
-	! grep Content-Type output
+	test_grep ! Content-Type output
 
 '
 
 test_expect_success 'format with signoff without funny signer name' '
 
 	git format-patch -s --stdout -1 >output &&
-	! grep Content-Type output
+	test_grep ! Content-Type output
 
 '
 
@@ -34,7 +34,7 @@
 
 	GIT_COMMITTER_NAME="はまの ふにおう" \
 	git format-patch -s --stdout -1 >output &&
-	grep Content-Type output
+	test_grep Content-Type output
 
 '
 
diff --git a/t/t4038-diff-combined.sh b/t/t4038-diff-combined.sh
index 2ce26e5..e11b711 100755
--- a/t/t4038-diff-combined.sh
+++ b/t/t4038-diff-combined.sh
@@ -100,7 +100,7 @@
 	git add file &&
 	git commit --amend -C HEAD &&
 	git show >out &&
-	grep "diff --cc file" out
+	test_grep "diff --cc file" out
 '
 
 test_expect_success 'setup for --cc --raw' '
@@ -118,13 +118,13 @@
 	four_trees=$(echo "$trees" | sed -e 4q) &&
 	git diff --cc --raw $four_trees $base_tree >out &&
 	# Check for four leading colons in the output:
-	grep "^::::[^:]" out
+	test_grep "^::::[^:]" out
 '
 
 test_expect_success 'check --cc --raw with forty trees' '
 	git diff --cc --raw $trees $base_tree >out &&
 	# Check for forty leading colons in the output:
-	grep "^::::::::::::::::::::::::::::::::::::::::[^:]" out
+	test_grep "^::::::::::::::::::::::::::::::::::::::::[^:]" out
 '
 
 test_expect_success 'setup combined ignore spaces' '
diff --git a/t/t4045-diff-relative.sh b/t/t4045-diff-relative.sh
index 2c8493f..167be0b 100755
--- a/t/t4045-diff-relative.sh
+++ b/t/t4045-diff-relative.sh
@@ -245,4 +245,13 @@
 	test_cmp expected out
 '
 
+test_expect_success 'diff --relative --cached with change in subdir' '
+	git switch br3 &&
+	test_when_finished "git merge --abort" &&
+	test_must_fail git merge sub1 &&
+	echo file0 >expected &&
+	git -C subdir diff --relative --name-only --cached >out &&
+	test_cmp expected out
+'
+
 test_done
diff --git a/t/t4051-diff-function-context.sh b/t/t4051-diff-function-context.sh
index 4838a1d..117eaed 100755
--- a/t/t4051-diff-function-context.sh
+++ b/t/t4051-diff-function-context.sh
@@ -86,15 +86,15 @@
 check_diff changed_hello 'changed function'
 
 test_expect_success ' context includes comment' '
-	grep "^ .*Hello comment" changed_hello.diff
+	test_grep "^ .*Hello comment" changed_hello.diff
 '
 
 test_expect_success ' context includes begin' '
-	grep "^ .*Begin of hello" changed_hello.diff
+	test_grep "^ .*Begin of hello" changed_hello.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^ .*End of hello" changed_hello.diff
+	test_grep "^ .*End of hello" changed_hello.diff
 '
 
 test_expect_success ' context does not include other functions' '
@@ -112,11 +112,11 @@
 check_diff changed_includes 'changed includes'
 
 test_expect_success ' context includes begin' '
-	grep "^ .*Begin.h" changed_includes.diff
+	test_grep "^ .*Begin.h" changed_includes.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^ .*End.h" changed_includes.diff
+	test_grep "^ .*End.h" changed_includes.diff
 '
 
 test_expect_success ' context does not include other functions' '
@@ -130,11 +130,11 @@
 check_diff appended 'appended function'
 
 test_expect_success ' context includes begin' '
-	grep "^[+].*Begin of first part" appended.diff
+	test_grep "^[+].*Begin of first part" appended.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^[+].*End of first part" appended.diff
+	test_grep "^[+].*End of first part" appended.diff
 '
 
 test_expect_success ' context does not include other functions' '
@@ -144,11 +144,11 @@
 check_diff extended 'appended function part'
 
 test_expect_success ' context includes begin' '
-	grep "^ .*Begin of first part" extended.diff
+	test_grep "^ .*Begin of first part" extended.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^[+].*End of second part" extended.diff
+	test_grep "^[+].*End of second part" extended.diff
 '
 
 test_expect_success ' context does not include other functions' '
@@ -162,11 +162,11 @@
 check_diff long_common_tail 'change with long common tail and no context' -U0
 
 test_expect_success ' context includes begin' '
-	grep "^ .*Begin of first part" long_common_tail.diff
+	test_grep "^ .*Begin of first part" long_common_tail.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^ .*End of second part" long_common_tail.diff
+	test_grep "^ .*End of second part" long_common_tail.diff
 '
 
 test_expect_success ' context does not include other functions' '
@@ -180,13 +180,13 @@
 check_diff changed_hello_appended 'changed function plus appended function'
 
 test_expect_success ' context includes begin' '
-	grep "^ .*Begin of hello" changed_hello_appended.diff &&
-	grep "^[+].*Begin of first part" changed_hello_appended.diff
+	test_grep "^ .*Begin of hello" changed_hello_appended.diff &&
+	test_grep "^[+].*Begin of first part" changed_hello_appended.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^ .*End of hello" changed_hello_appended.diff &&
-	grep "^[+].*End of first part" changed_hello_appended.diff
+	test_grep "^ .*End of hello" changed_hello_appended.diff &&
+	test_grep "^[+].*End of first part" changed_hello_appended.diff
 '
 
 test_expect_success ' context does not include other functions' '
@@ -196,13 +196,13 @@
 check_diff changed_hello_dummy 'changed two consecutive functions'
 
 test_expect_success ' context includes begin' '
-	grep "^ .*Begin of hello" changed_hello_dummy.diff &&
-	grep "^ .*Begin of dummy" changed_hello_dummy.diff
+	test_grep "^ .*Begin of hello" changed_hello_dummy.diff &&
+	test_grep "^ .*Begin of dummy" changed_hello_dummy.diff
 '
 
 test_expect_success ' context includes end' '
-	grep "^ .*End of hello" changed_hello_dummy.diff &&
-	grep "^ .*End of dummy" changed_hello_dummy.diff
+	test_grep "^ .*End of hello" changed_hello_dummy.diff &&
+	test_grep "^ .*End of dummy" changed_hello_dummy.diff
 '
 
 test_expect_success ' overlapping hunks are merged' '
diff --git a/t/t4053-diff-no-index.sh b/t/t4053-diff-no-index.sh
index 15076df..8e0394c 100755
--- a/t/t4053-diff-no-index.sh
+++ b/t/t4053-diff-no-index.sh
@@ -283,14 +283,14 @@
 
 test_expect_success 'diff --no-index refuses to diff stdin and a directory' '
 	test_must_fail git diff --no-index -- - a </dev/null 2>err &&
-	grep "fatal: cannot compare stdin to a directory" err
+	test_grep "fatal: cannot compare stdin to a directory" err
 '
 
 test_expect_success PIPE 'diff --no-index refuses to diff a named pipe and a directory' '
 	test_when_finished "rm -f pipe" &&
 	mkfifo pipe &&
 	test_must_fail git diff --no-index -- pipe a 2>err &&
-	grep "fatal: cannot compare a named pipe to a directory" err
+	test_grep "fatal: cannot compare a named pipe to a directory" err
 '
 
 test_expect_success PIPE,SYMLINKS 'diff --no-index reads from pipes' '
diff --git a/t/t4063-diff-blobs.sh b/t/t4063-diff-blobs.sh
index 50fdb5e..1318cce 100755
--- a/t/t4063-diff-blobs.sh
+++ b/t/t4063-diff-blobs.sh
@@ -47,7 +47,7 @@
 	check_paths $sha1_one $sha1_two
 '
 test_expect_success 'sha1 diff has no mode change' '
-	! grep mode diff
+	test_grep ! mode diff
 '
 
 test_expect_success 'diff by tree:path (run)' '
diff --git a/t/t4065-diff-anchored.sh b/t/t4065-diff-anchored.sh
index b3f510f..9f06ac6 100755
--- a/t/t4065-diff-anchored.sh
+++ b/t/t4065-diff-anchored.sh
@@ -10,11 +10,11 @@
 
 	# normally, c is moved to produce the smallest diff
 	test_expect_code 1 git diff --no-index pre post >diff &&
-	grep "^+c" diff &&
+	test_grep "^+c" diff &&
 
 	# with anchor, a is moved
 	test_expect_code 1 git diff --no-index --anchored=c pre post >diff &&
-	grep "^+a" diff
+	test_grep "^+a" diff
 '
 
 test_expect_success '--anchored multiple' '
@@ -23,13 +23,13 @@
 
 	# with 1 anchor, c is not moved, but f is moved
 	test_expect_code 1 git diff --no-index --anchored=c pre post >diff &&
-	grep "^+a" diff && # a is moved instead of c
-	grep "^+f" diff &&
+	test_grep "^+a" diff && # a is moved instead of c
+	test_grep "^+f" diff &&
 
 	# with 2 anchors, c and f are not moved
 	test_expect_code 1 git diff --no-index --anchored=c --anchored=f pre post >diff &&
-	grep "^+a" diff &&
-	grep "^+d" diff # d is moved instead of f
+	test_grep "^+a" diff &&
+	test_grep "^+d" diff # d is moved instead of f
 '
 
 test_expect_success '--anchored with nonexistent line has no effect' '
@@ -37,7 +37,7 @@
 	printf "c\na\nb\n" >post &&
 
 	test_expect_code 1 git diff --no-index --anchored=x pre post >diff &&
-	grep "^+c" diff
+	test_grep "^+c" diff
 '
 
 test_expect_success '--anchored with non-unique line has no effect' '
@@ -45,7 +45,7 @@
 	printf "c\na\nb\nc\nd\ne\n" >post &&
 
 	test_expect_code 1 git diff --no-index --anchored=c pre post >diff &&
-	grep "^+c" diff
+	test_grep "^+c" diff
 '
 
 test_expect_success 'diff still produced with impossible multiple --anchored' '
@@ -66,16 +66,16 @@
 	printf "c\na\nb\n" >post &&
 
 	test_expect_code 1 git diff --no-index --patience --anchored=c pre post >diff &&
-	grep "^+a" diff &&
+	test_grep "^+a" diff &&
 
 	test_expect_code 1 git diff --no-index --anchored=c --patience pre post >diff &&
-	grep "^+c" diff &&
+	test_grep "^+c" diff &&
 
 	test_expect_code 1 git diff --no-index --histogram --anchored=c pre post >diff &&
-	grep "^+a" diff &&
+	test_grep "^+a" diff &&
 
 	test_expect_code 1 git diff --no-index --anchored=c --histogram pre post >diff &&
-	grep "^+c" diff
+	test_grep "^+c" diff
 '
 
 test_expect_success '--anchored works with other commands like "git show"' '
@@ -88,7 +88,7 @@
 
 	# with anchor, a is moved
 	git show --patience --anchored=c >diff &&
-	grep "^+a" diff
+	test_grep "^+a" diff
 '
 
 test_done
diff --git a/t/t4067-diff-partial-clone.sh b/t/t4067-diff-partial-clone.sh
index a9dec84..f510d8d 100755
--- a/t/t4067-diff-partial-clone.sh
+++ b/t/t4067-diff-partial-clone.sh
@@ -70,9 +70,9 @@
 
 	# Ensure that only a and another-a are fetched.
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client diff HEAD^ HEAD &&
-	grep "want $(cat hash-old-a)" trace &&
-	grep "want $(cat hash-new-a)" trace &&
-	! grep "want $(cat hash-b)" trace
+	test_grep "want $(cat hash-old-a)" trace &&
+	test_grep "want $(cat hash-new-a)" trace &&
+	test_grep ! "want $(cat hash-b)" trace
 '
 
 test_expect_success 'when fetching missing objects, diff skips GITLINKs' '
@@ -103,8 +103,8 @@
 	# Ensure that a and another-a are fetched, and check (by successful
 	# execution of the diff) that no invalid OIDs are sent.
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client diff HEAD^ HEAD &&
-	grep "want $(cat hash-old-a)" trace &&
-	grep "want $(cat hash-new-a)" trace
+	test_grep "want $(cat hash-old-a)" trace &&
+	test_grep "want $(cat hash-new-a)" trace
 '
 
 test_expect_success 'diff with rename detection batches blobs' '
@@ -127,7 +127,7 @@
 	# Ensure that there is exactly 1 negotiation by checking that there is
 	# only 1 "done" line sent. ("done" marks the end of negotiation.)
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client diff --raw -M HEAD^ HEAD >out &&
-	grep ":100644 100644.*R[0-9][0-9][0-9].*b.*c" out &&
+	test_grep ":100644 100644.*R[0-9][0-9][0-9].*b.*c" out &&
 	grep "fetch> done" trace >done_lines &&
 	test_line_count = 1 done_lines
 '
diff --git a/t/t4073-diff-stat-name-width.sh b/t/t4073-diff-stat-name-width.sh
index ec5d3c3..2412420 100755
--- a/t/t4073-diff-stat-name-width.sh
+++ b/t/t4073-diff-stat-name-width.sh
@@ -21,41 +21,41 @@
 
 test_expect_success 'test name-width long enough for filepath' '
 	git diff HEAD~1 HEAD --stat --stat-name-width=12 >out &&
-	grep "d你好/f再见 |" out &&
+	test_grep "d你好/f再见 |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=11 >out &&
-	grep "d你好/f再见 |" out
+	test_grep "d你好/f再见 |" out
 '
 
 test_expect_success 'test name-width not long enough for dir name' '
 	git diff HEAD~1 HEAD --stat --stat-name-width=10 >out &&
-	grep ".../f再见  |" out &&
+	test_grep ".../f再见  |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=9 >out &&
-	grep ".../f再见 |" out
+	test_grep ".../f再见 |" out
 '
 
 test_expect_success 'test name-width not long enough for slash' '
 	git diff HEAD~1 HEAD --stat --stat-name-width=8 >out &&
-	grep "...f再见 |" out
+	test_grep "...f再见 |" out
 '
 
 test_expect_success 'test name-width not long enough for file name' '
 	git diff HEAD~1 HEAD --stat --stat-name-width=7 >out &&
-	grep "...再见 |" out &&
+	test_grep "...再见 |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=6 >out &&
-	grep "...见  |" out &&
+	test_grep "...见  |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=5 >out &&
-	grep "...见 |" out &&
+	test_grep "...见 |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=4 >out &&
-	grep "...  |" out
+	test_grep "...  |" out
 '
 
 test_expect_success 'test name-width minimum length' '
 	git diff HEAD~1 HEAD --stat --stat-name-width=3 >out &&
-	grep "... |" out &&
+	test_grep "... |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=2 >out &&
-	grep "... |" out &&
+	test_grep "... |" out &&
 	git diff HEAD~1 HEAD --stat --stat-name-width=1 >out &&
-	grep "... |" out
+	test_grep "... |" out
 '
 
 test_done
diff --git a/t/t4103-apply-binary.sh b/t/t4103-apply-binary.sh
index f2d41e0..bdd4ccf 100755
--- a/t/t4103-apply-binary.sh
+++ b/t/t4103-apply-binary.sh
@@ -181,7 +181,7 @@
 	do_reset &&
 	test_must_fail git apply patch.trunc 2>err &&
 	line=$(awk "END { print NR + 1 }" patch.trunc) &&
-	grep "error: corrupt binary patch at patch.trunc:$line: " err
+	test_grep "error: corrupt binary patch at patch.trunc:$line: " err
 '
 
 test_expect_success 'reject unrecognized binary diff' '
diff --git a/t/t4120-apply-popt.sh b/t/t4120-apply-popt.sh
index c960fdf..c7801ae 100755
--- a/t/t4120-apply-popt.sh
+++ b/t/t4120-apply-popt.sh
@@ -18,7 +18,7 @@
 	git mv sub süb &&
 	echo B >süb/file1 &&
 	git diff >patch.escaped &&
-	grep "[\]" patch.escaped &&
+	test_grep "[\]" patch.escaped &&
 	rm süb/file1 &&
 	rmdir süb
 '
diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh
index 205d86d..4c487fb 100755
--- a/t/t4124-apply-ws-rule.sh
+++ b/t/t4124-apply-ws-rule.sh
@@ -222,8 +222,8 @@
 	>target &&
 	create_patch >patch-file &&
 	git apply --whitespace=fix patch-file &&
-	grep "newline$" target &&
-	grep "^$" target
+	test_grep "newline$" target &&
+	test_grep "^$" target
 '
 
 test_expect_success 'blank at EOF with --whitespace=fix (1)' '
@@ -287,7 +287,7 @@
 	git checkout one &&
 	git apply --whitespace=warn patch 2>error &&
 	test_cmp expect one &&
-	grep "new blank line at EOF" error
+	test_grep "new blank line at EOF" error
 '
 
 test_expect_success 'blank at EOF with --whitespace=error' '
@@ -300,7 +300,7 @@
 	git checkout one &&
 	test_must_fail git apply --whitespace=error patch 2>error &&
 	test_cmp expect one &&
-	grep "new blank line at EOF" error
+	test_grep "new blank line at EOF" error
 '
 
 test_expect_success 'blank but not empty at EOF' '
@@ -313,7 +313,7 @@
 	git checkout one &&
 	git apply --whitespace=warn patch 2>error &&
 	test_cmp expect one &&
-	grep "new blank line at EOF" error
+	test_grep "new blank line at EOF" error
 '
 
 test_expect_success 'applying beyond EOF requires one non-blank context line' '
diff --git a/t/t4128-apply-root.sh b/t/t4128-apply-root.sh
index 5eba15f..c772fae 100755
--- a/t/t4128-apply-root.sh
+++ b/t/t4128-apply-root.sh
@@ -138,7 +138,7 @@
 	git add some/sub/dir/delfile &&
 	git apply --directory=some/sub/dir/ --index patch &&
 	git ls-files >out &&
-	! grep delfile out
+	test_grep ! delfile out
 '
 
 cat > patch << 'EOF'
diff --git a/t/t4140-apply-ita.sh b/t/t4140-apply-ita.sh
index 0b11a8a..49ebb51 100755
--- a/t/t4140-apply-ita.sh
+++ b/t/t4140-apply-ita.sh
@@ -14,11 +14,11 @@
 	cat blueprint >test-file &&
 	git add -N test-file &&
 	git diff >creation-patch &&
-	grep "new file mode 100644" creation-patch &&
+	test_grep "new file mode 100644" creation-patch &&
 
 	rm -f test-file &&
 	git diff >deletion-patch &&
-	grep "deleted file mode 100644" deletion-patch &&
+	test_grep "deleted file mode 100644" deletion-patch &&
 
 	git rm -f test-file &&
 	test_write_lines 6 >>committed-file &&
diff --git a/t/t4141-apply-too-large.sh b/t/t4141-apply-too-large.sh
index eac6f7e..6bc1c7a 100755
--- a/t/t4141-apply-too-large.sh
+++ b/t/t4141-apply-too-large.sh
@@ -5,7 +5,6 @@
 . ./test-lib.sh
 
 test_expect_success EXPENSIVE 'git apply rejects patches that are too large' '
-	sz=$((1024 * 1024 * 1023)) &&
 	{
 		cat <<-\EOF &&
 		diff --git a/file b/file
@@ -14,9 +13,9 @@
 		+++ b/file
 		@@ -0,0 +1 @@
 		EOF
-		test-tool genzeros
-	} | test_copy_bytes $sz | test_must_fail git apply 2>err &&
-	grep "patch too large" err
+		test-tool genzeros $((1024 * 1024 * 1023))
+	} | test_must_fail git apply 2>err &&
+	test_grep "patch too large" err
 '
 
 test_done
diff --git a/t/t4150-am.sh b/t/t4150-am.sh
index 699a81a..61c3ce9 100755
--- a/t/t4150-am.sh
+++ b/t/t4150-am.sh
@@ -580,7 +580,7 @@
 	git am --keep patch4 &&
 	test_path_is_missing .git/rebase-apply &&
 	git cat-file commit HEAD >actual &&
-	grep "Re: Re: Re: \[PATCH 1/5 v2\] \[foo\] third" actual
+	test_grep "Re: Re: Re: \[PATCH 1/5 v2\] \[foo\] third" actual
 '
 
 test_expect_success 'am --keep-non-patch really keeps the non-patch part' '
@@ -590,7 +590,7 @@
 	git am --keep-non-patch patch4 &&
 	test_path_is_missing .git/rebase-apply &&
 	git cat-file commit HEAD >actual &&
-	grep "^\[foo\] third" actual
+	test_grep "^\[foo\] third" actual
 '
 
 test_expect_success 'setup am -3' '
@@ -642,7 +642,7 @@
 '
 
 test_expect_success 'am can rename a file' '
-	grep "^rename from" rename.patch &&
+	test_grep "^rename from" rename.patch &&
 	rm -fr .git/rebase-apply &&
 	git reset --hard &&
 	git checkout lorem^0 &&
@@ -653,7 +653,7 @@
 '
 
 test_expect_success 'am -3 can rename a file' '
-	grep "^rename from" rename.patch &&
+	test_grep "^rename from" rename.patch &&
 	rm -fr .git/rebase-apply &&
 	git reset --hard &&
 	git checkout lorem^0 &&
@@ -664,7 +664,7 @@
 '
 
 test_expect_success 'am -3 can rename a file after falling back to 3-way merge' '
-	grep "^rename from" rename-add.patch &&
+	test_grep "^rename from" rename-add.patch &&
 	rm -fr .git/rebase-apply &&
 	git reset --hard &&
 	git checkout lorem^0 &&
@@ -884,7 +884,7 @@
 	git am --ignore-date patch1 &&
 	git cat-file commit HEAD | sed -e "/^\$/q" >head1 &&
 	sed -ne "/^author /s/.*> //p" head1 >at &&
-	grep "+0000" at
+	test_grep "+0000" at
 '
 
 test_expect_success 'am into an unborn branch' '
@@ -1066,7 +1066,7 @@
 	INPUT_END
 	git commit -F msg &&
 	git -c format.mboxrd format-patch --stdout -1 >mboxrd1 &&
-	grep "^>From could trip up a loose mbox parser" mboxrd1 &&
+	test_grep "^>From could trip up a loose mbox parser" mboxrd1 &&
 	git checkout -f first &&
 	git am --patch-format=mboxrd mboxrd1 &&
 	git cat-file commit HEAD | tail -n4 >out &&
@@ -1144,21 +1144,21 @@
 		git format-patch --stdout main..HEAD >patches &&
 		git reset --hard main &&
 		git am patches &&
-		grep "smudged" a.txt &&
+		test_grep "smudged" a.txt &&
 
 		git checkout removal &&
 		git reset --hard &&
 		git format-patch --stdout main..HEAD >patches &&
 		git reset --hard main &&
 		git am patches &&
-		grep "clean" a.txt &&
+		test_grep "clean" a.txt &&
 
 		git checkout conflict &&
 		git reset --hard &&
 		git format-patch --stdout main..HEAD >patches &&
 		git reset --hard fourth &&
 		test_must_fail git am -3 patches &&
-		grep "<<<<<<<<<<" a.txt
+		test_grep "<<<<<<<<<<" a.txt
 	)
 '
 
@@ -1196,13 +1196,13 @@
 test_expect_success 'a message without a patch is an error (default)' '
 	test_when_finished "git am --abort || :" &&
 	test_must_fail git am empty-commit.patch >err &&
-	grep "Patch is empty" err
+	test_grep "Patch is empty" err
 '
 
 test_expect_success 'a message without a patch is an error where an explicit "--empty=stop" is given' '
 	test_when_finished "git am --abort || :" &&
 	test_must_fail git am --empty=stop empty-commit.patch >err &&
-	grep "Patch is empty." err
+	test_grep "Patch is empty." err
 '
 
 test_expect_success 'a message without a patch will be skipped when "--empty=drop" is given' '
@@ -1210,7 +1210,7 @@
 	git rev-parse empty-commit^ >expected &&
 	git rev-parse HEAD >actual &&
 	test_cmp expected actual &&
-	grep "Skipping: empty commit" output
+	test_grep "Skipping: empty commit" output
 '
 
 test_expect_success 'record as an empty commit when meeting e-mail message that lacks a patch' '
@@ -1218,15 +1218,15 @@
 	test_path_is_missing .git/rebase-apply &&
 	git show empty-commit --format="%B" >expected &&
 	git show HEAD --format="%B" >actual &&
-	grep -f actual expected &&
-	grep "Creating an empty commit: empty commit" output
+	test_grep -f actual expected &&
+	test_grep "Creating an empty commit: empty commit" output
 '
 
 test_expect_success 'skip an empty patch in the middle of an am session' '
 	git checkout empty-commit^ &&
 	test_must_fail git am empty-commit.patch >out 2>err &&
-	grep "Patch is empty." out &&
-	grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
+	test_grep "Patch is empty." out &&
+	test_grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
 	git am --skip &&
 	test_path_is_missing .git/rebase-apply &&
 	git rev-parse empty-commit^ >expected &&
@@ -1237,14 +1237,14 @@
 test_expect_success 'record an empty patch as an empty commit in the middle of an am session' '
 	git checkout empty-commit^ &&
 	test_must_fail git am empty-commit.patch >out 2>err &&
-	grep "Patch is empty." out &&
-	grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
+	test_grep "Patch is empty." out &&
+	test_grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
 	git am --allow-empty >output &&
-	grep "No changes - recorded it as an empty commit." output &&
+	test_grep "No changes - recorded it as an empty commit." output &&
 	test_path_is_missing .git/rebase-apply &&
 	git show empty-commit --format="%B" >expected &&
 	git show HEAD --format="%B" >actual &&
-	grep -f actual expected
+	test_grep -f actual expected
 '
 
 test_expect_success 'create an non-empty commit when the index IS changed though "--allow-empty" is given' '
@@ -1255,7 +1255,7 @@
 	git am --allow-empty &&
 	git show empty-commit --format="%B" >expected &&
 	git show HEAD --format="%B" >actual &&
-	grep -f actual expected &&
+	test_grep -f actual expected &&
 	git diff HEAD^..HEAD --name-only
 '
 
@@ -1264,7 +1264,7 @@
 	git rev-parse HEAD >expected &&
 	test_must_fail git am seq.patch &&
 	test_must_fail git am --allow-empty >err &&
-	! grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
+	test_grep ! "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
 	git rev-parse HEAD >actual &&
 	test_cmp actual expected
 '
@@ -1274,7 +1274,7 @@
 	git rev-parse HEAD >expected &&
 	test_must_fail git am -3 seq.patch &&
 	test_must_fail git am --allow-empty >err &&
-	! grep "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
+	test_grep ! "To record the empty patch as an empty commit, run \"git am --allow-empty\"." err &&
 	git rev-parse HEAD >actual &&
 	test_cmp actual expected
 '
diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh
index 1717f40..7bb601e 100755
--- a/t/t4200-rerere.sh
+++ b/t/t4200-rerere.sh
@@ -83,7 +83,7 @@
 
 	sha1=$(sed "s/	.*//" .git/MERGE_RR) &&
 	rr=.git/rr-cache/$sha1 &&
-	grep "^=======\$" $rr/preimage &&
+	test_grep "^=======\$" $rr/preimage &&
 	test_path_is_missing $rr/postimage &&
 	test_path_is_missing $rr/thisimage
 '
@@ -96,7 +96,7 @@
 
 	sha1=$(sed "s/	.*//" .git/MERGE_RR) &&
 	rr=.git/rr-cache/$sha1 &&
-	grep ^=======$ $rr/preimage
+	test_grep ^=======$ $rr/preimage
 '
 
 test_expect_success 'set up rr-cache' '
@@ -177,7 +177,7 @@
 
 	test_must_fail git merge first &&
 	# rerere kicked in
-	! grep "^=======\$" a1 &&
+	test_grep ! "^=======\$" a1 &&
 	test_cmp expect a1
 '
 
@@ -438,7 +438,7 @@
 '
 
 test_expect_success 'rerere -h' '
-	test_must_fail git rerere -h >help &&
+	git rerere -h >help &&
 	test_grep [Uu]sage help
 '
 
diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh
index 9f41d56..023fbff 100755
--- a/t/t4201-shortlog.sh
+++ b/t/t4201-shortlog.sh
@@ -267,7 +267,7 @@
 
 test_expect_success 'shortlog bogus --group' '
 	test_must_fail git shortlog --group=bogus HEAD 2>err &&
-	grep "unknown group type" err
+	test_grep "unknown group type" err
 '
 
 test_expect_success 'trailer idents are split' '
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 75edb0e..06552a6 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -668,12 +668,12 @@
 
 test_expect_success 'log --raw --graph -m with merge' '
 	git log --raw --graph --oneline -m main | head -n 500 >actual &&
-	grep "initial" actual
+	test_grep "initial" actual
 '
 
 test_expect_success 'diff-tree --graph' '
 	git diff-tree --graph main^ | head -n 500 >actual &&
-	grep "one" actual
+	test_grep "one" actual
 '
 
 cat > expect <<\EOF
@@ -1082,13 +1082,13 @@
 test_expect_success 'decorate-refs-exclude HEAD' '
 	git log --decorate=full --oneline \
 		--decorate-refs-exclude="HEAD" >actual &&
-	! grep HEAD actual
+	test_grep ! HEAD actual
 '
 
 test_expect_success 'decorate-refs focus from default' '
 	git log --decorate=full --oneline \
 		--decorate-refs="refs/heads" >actual &&
-	! grep HEAD actual
+	test_grep ! HEAD actual
 '
 
 test_expect_success '--clear-decorations overrides defaults' '
@@ -2095,45 +2095,45 @@
 
 test_expect_success GPG 'log --graph --show-signature' '
 	git log --graph --show-signature -n1 signed >actual &&
-	grep "^| gpg: Signature made" actual &&
-	grep "^| gpg: Good signature" actual
+	test_grep "^| gpg: Signature made" actual &&
+	test_grep "^| gpg: Good signature" actual
 '
 
 test_expect_success GPGSM 'log --graph --show-signature x509' '
 	git log --graph --show-signature -n1 signed-x509 >actual &&
-	grep "^| gpgsm: Signature made" actual &&
-	grep "^| gpgsm: Good signature" actual
+	test_grep "^| gpgsm: Signature made" actual &&
+	test_grep "^| gpgsm: Good signature" actual
 '
 
 test_expect_success GPGSSH 'log --graph --show-signature ssh' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git log --graph --show-signature -n1 signed-ssh >actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on expired signature key' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git log --graph --show-signature -n1 expired-signed >actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on not yet valid signature key' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git log --graph --show-signature -n1 notyetvalid-signed >actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log show success with commit date and key validity matching' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git log --graph --show-signature -n1 timeboxedvalid-signed >actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure with commit date outside of key validity' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git log --graph --show-signature -n1 timeboxedinvalid-signed >actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPG 'log --graph --show-signature for merged tag' '
@@ -2150,9 +2150,9 @@
 	git checkout plain &&
 	git merge --no-ff -m msg signed_tag &&
 	git log --graph --show-signature -n1 plain >actual &&
-	grep "^|\\\  merged tag" actual &&
-	grep "^| | gpg: Signature made" actual &&
-	grep "^| | gpg: Good signature" actual
+	test_grep "^|\\\  merged tag" actual &&
+	test_grep "^| | gpg: Signature made" actual &&
+	test_grep "^| | gpg: Good signature" actual
 '
 
 test_expect_success GPG 'log --graph --show-signature for merged tag in shallow clone' '
@@ -2172,7 +2172,7 @@
 	git clone --depth 1 --no-local . shallow &&
 	test_when_finished "rm -rf shallow" &&
 	git -C shallow log --graph --show-signature -n1 plain-shallow >actual &&
-	grep "tag signed_tag_shallow names a non-parent $hash" actual
+	test_grep "tag signed_tag_shallow names a non-parent $hash" actual
 '
 
 test_expect_success GPG 'log --graph --show-signature for merged tag with missing key' '
@@ -2189,9 +2189,9 @@
 	git checkout plain-nokey &&
 	git merge --no-ff -m msg signed_tag_nokey &&
 	GNUPGHOME=. git log --graph --show-signature -n1 plain-nokey >actual &&
-	grep "^|\\\  merged tag" actual &&
-	grep "^| | gpg: Signature made" actual &&
-	grep -E "^| | gpg: Can'"'"'t check signature: (public key not found|No public key)" actual
+	test_grep "^|\\\  merged tag" actual &&
+	test_grep "^| | gpg: Signature made" actual &&
+	test_grep -E "^| | gpg: Can'"'"'t check signature: (public key not found|No public key)" actual
 '
 
 test_expect_success GPG 'log --graph --show-signature for merged tag with bad signature' '
@@ -2211,9 +2211,9 @@
 	git checkout plain-bad &&
 	git merge --no-ff -m msg "$(cat forged.tag)" &&
 	git log --graph --show-signature -n1 plain-bad >actual &&
-	grep "^|\\\  merged tag" actual &&
-	grep "^| | gpg: Signature made" actual &&
-	grep "^| | gpg: BAD signature from" actual
+	test_grep "^|\\\  merged tag" actual &&
+	test_grep "^| | gpg: Signature made" actual &&
+	test_grep "^| | gpg: BAD signature from" actual
 '
 
 test_expect_success GPG 'log --show-signature for merged tag with GPG failure' '
@@ -2232,9 +2232,9 @@
 	if ! test_have_prereq VALGRIND
 	then
 		TMPDIR="$(pwd)/bogus" git log --show-signature -n1 plain-fail >actual &&
-		grep "^merged tag" actual &&
-		grep "^No signature" actual &&
-		! grep "^gpg: Signature made" actual
+		test_grep "^merged tag" actual &&
+		test_grep "^No signature" actual &&
+		test_grep ! "^gpg: Signature made" actual
 	fi
 '
 
@@ -2254,9 +2254,9 @@
 	git checkout plain-x509 &&
 	git merge --no-ff -m msg signed_tag_x509 &&
 	git log --graph --show-signature -n1 plain-x509 >actual &&
-	grep "^|\\\  merged tag" actual &&
-	grep "^| | gpgsm: Signature made" actual &&
-	grep "^| | gpgsm: Good signature" actual
+	test_grep "^|\\\  merged tag" actual &&
+	test_grep "^| | gpgsm: Signature made" actual &&
+	test_grep "^| | gpgsm: Good signature" actual
 '
 
 test_expect_success GPGSM 'log --graph --show-signature for merged tag x509 missing key' '
@@ -2275,8 +2275,8 @@
 	git checkout plain-x509-nokey &&
 	git merge --no-ff -m msg signed_tag_x509_nokey &&
 	GNUPGHOME=. git log --graph --show-signature -n1 plain-x509-nokey >actual &&
-	grep "^|\\\  merged tag" actual &&
-	grep -e "^| | gpgsm: certificate not found" \
+	test_grep "^|\\\  merged tag" actual &&
+	test_grep -e "^| | gpgsm: certificate not found" \
 	     -e "^| | gpgsm: failed to find the certificate: Not found" actual
 '
 
@@ -2299,35 +2299,35 @@
 	git checkout plain-x509-bad &&
 	git merge --no-ff -m msg "$(cat forged.tag)" &&
 	git log --graph --show-signature -n1 plain-x509-bad >actual &&
-	grep "^|\\\  merged tag" actual &&
-	grep "^| | gpgsm: Signature made" actual &&
-	grep "^| | gpgsm: invalid signature" actual
+	test_grep "^|\\\  merged tag" actual &&
+	test_grep "^| | gpgsm: Signature made" actual &&
+	test_grep "^| | gpgsm: invalid signature" actual
 '
 
 
 test_expect_success GPG '--no-show-signature overrides --show-signature' '
 	git log -1 --show-signature --no-show-signature signed >actual &&
-	! grep "^gpg:" actual
+	test_grep ! "^gpg:" actual
 '
 
 test_expect_success GPG 'log.showsignature=true behaves like --show-signature' '
 	test_config log.showsignature true &&
 	git log -1 signed >actual &&
-	grep "gpg: Signature made" actual &&
-	grep "gpg: Good signature" actual
+	test_grep "gpg: Signature made" actual &&
+	test_grep "gpg: Good signature" actual
 '
 
 test_expect_success GPG '--no-show-signature overrides log.showsignature=true' '
 	test_config log.showsignature true &&
 	git log -1 --no-show-signature signed >actual &&
-	! grep "^gpg:" actual
+	test_grep ! "^gpg:" actual
 '
 
 test_expect_success GPG '--show-signature overrides log.showsignature=false' '
 	test_config log.showsignature false &&
 	git log -1 --show-signature signed >actual &&
-	grep "gpg: Signature made" actual &&
-	grep "gpg: Good signature" actual
+	test_grep "gpg: Signature made" actual &&
+	test_grep "gpg: Good signature" actual
 '
 
 test_expect_success 'log --graph --no-walk is forbidden' '
@@ -2423,7 +2423,7 @@
 	git log --decorate=full --oneline >actual &&
 
 	# None of the refs are visible:
-	! grep /fake actual
+	test_grep ! /fake actual
 '
 
 test_expect_success 'log --end-of-options' '
diff --git a/t/t4204-patch-id.sh b/t/t4204-patch-id.sh
index 605faea..90e661f 100755
--- a/t/t4204-patch-id.sh
+++ b/t/t4204-patch-id.sh
@@ -44,7 +44,7 @@
 test_expect_success 'patch-id output is well-formed' '
 	git log -p -1 >log.output &&
 	git patch-id <log.output >output &&
-	grep "^$OID_REGEX $(git rev-parse HEAD)$" output
+	test_grep "^$OID_REGEX $(git rev-parse HEAD)$" output
 '
 
 #calculate patch id. Make sure output is not empty.
diff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh
index 3865f6a..4be5c51 100755
--- a/t/t4205-log-pretty-formats.sh
+++ b/t/t4205-log-pretty-formats.sh
@@ -535,7 +535,7 @@
 	git log --color --pretty="tformat:%<(10,trunc)%s%>>(10,ltrunc)%C(auto)%d" |
 		test_decode_color |
 		nul_to_q >actual &&
-	! grep Q actual
+	test_grep ! Q actual
 '
 
 # --date=[XXX] and corresponding %a[X] %c[X] format equivalency
diff --git a/t/t4209-log-pickaxe.sh b/t/t4209-log-pickaxe.sh
index 0e2f80a..802597e 100755
--- a/t/t4209-log-pickaxe.sh
+++ b/t/t4209-log-pickaxe.sh
@@ -64,21 +64,21 @@
 	test_grep "switch.*requires a value" err &&
 
 	test_expect_code 128 git log -Gregex -Sstring 2>err &&
-	grep "cannot be used together" err &&
+	test_grep "cannot be used together" err &&
 
 	test_expect_code 128 git log -Gregex --find-object=HEAD 2>err &&
-	grep "cannot be used together" err &&
+	test_grep "cannot be used together" err &&
 
 	test_expect_code 128 git log -Sstring --find-object=HEAD 2>err &&
-	grep "cannot be used together" err &&
+	test_grep "cannot be used together" err &&
 
 	test_expect_code 128 git log --pickaxe-all --find-object=HEAD 2>err &&
-	grep "cannot be used together" err
+	test_grep "cannot be used together" err
 '
 
 test_expect_success 'usage: --pickaxe-regex' '
 	test_expect_code 128 git log -Gregex --pickaxe-regex 2>err &&
-	grep "cannot be used together" err
+	test_grep "cannot be used together" err
 '
 
 test_expect_success 'usage: --no-pickaxe-regex' '
diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh
index ca4eb7b..e61ef9b 100755
--- a/t/t4211-line-log.sh
+++ b/t/t4211-line-log.sh
@@ -393,14 +393,14 @@
 	git log -L:func2:file.c --format= >actual &&
 
 	# Output should contain index headers (not present in old code path)
-	grep "^index $head_blob_old\.\.$head_blob_new 100644" actual &&
+	test_grep "^index $head_blob_old\.\.$head_blob_new 100644" actual &&
 
 	# Root commit should show new file mode and null index
-	grep "^new file mode 100644" actual &&
-	grep "^index $null_blob\.\.$root_blob$" actual &&
+	test_grep "^new file mode 100644" actual &&
+	test_grep "^index $null_blob\.\.$root_blob$" actual &&
 
 	# Hunk headers should include funcname context
-	grep "^@@ .* @@ int func1()" actual
+	test_grep "^@@ .* @@ int func1()" actual
 '
 
 test_expect_success '-L with --word-diff' '
@@ -431,15 +431,15 @@
 
 test_expect_success '-L with --no-prefix' '
 	git log -L:func2:file.c --no-prefix --format= >actual &&
-	grep "^diff --git file.c file.c" actual &&
-	grep "^--- file.c" actual &&
-	! grep "^--- a/" actual
+	test_grep "^diff --git file.c file.c" actual &&
+	test_grep "^--- file.c" actual &&
+	test_grep ! "^--- a/" actual
 '
 
 test_expect_success '-L with --full-index' '
 	git log -L:func2:file.c --full-index --format= >actual &&
-	grep "^index $head_blob_old_full\.\.$head_blob_new_full 100644" actual &&
-	grep "^index $null_blob_full\.\.$root_blob_full$" actual
+	test_grep "^index $head_blob_old_full\.\.$head_blob_new_full 100644" actual &&
+	test_grep "^index $null_blob_full\.\.$root_blob_full$" actual
 '
 
 test_expect_success 'setup -L with whitespace change' '
@@ -548,29 +548,29 @@
 	git log -L:func2:file.c --word-diff \
 		--word-diff-regex="[a-zA-Z0-9_]+" --format= >actual &&
 	# Word-diff markers must be present
-	grep "{+" actual &&
-	grep "+}" actual &&
+	test_grep "{+" actual &&
+	test_grep "+}" actual &&
 	# No line-level +/- markers (word-diff replaces them);
 	# exclude --- header lines from the check
-	! grep "^+[^+]" actual &&
-	! grep "^-[^-]" actual
+	test_grep ! "^+[^+]" actual &&
+	test_grep ! "^-[^-]" actual
 '
 
 test_expect_success '-L with --src-prefix and --dst-prefix' '
 	git checkout parent-oids &&
 	git log -L:func2:file.c --src-prefix=old/ --dst-prefix=new/ \
 		--format= >actual &&
-	grep "^diff --git old/file.c new/file.c" actual &&
-	grep "^--- old/file.c" actual &&
-	grep "^+++ new/file.c" actual &&
-	! grep "^--- a/" actual
+	test_grep "^diff --git old/file.c new/file.c" actual &&
+	test_grep "^--- old/file.c" actual &&
+	test_grep "^+++ new/file.c" actual &&
+	test_grep ! "^--- a/" actual
 '
 
 test_expect_success '-L with --abbrev' '
 	git checkout parent-oids &&
 	git log -L:func2:file.c --abbrev=4 --format= -1 >actual &&
 	# 4-char abbreviated hashes on index line
-	grep "^index [0-9a-f]\{4\}\.\.[0-9a-f]\{4\}" actual
+	test_grep "^index [0-9a-f]\{4\}\.\.[0-9a-f]\{4\}" actual
 '
 
 test_expect_success '-L with -b suppresses whitespace-only diff' '
@@ -586,24 +586,24 @@
 	git log -L:func2:file.c --output-indicator-new=">" \
 		--output-indicator-old="<" --output-indicator-context="|" \
 		--format= -1 >actual &&
-	grep "^>" actual &&
-	grep "^<" actual &&
-	grep "^|" actual &&
+	test_grep "^>" actual &&
+	test_grep "^<" actual &&
+	test_grep "^|" actual &&
 	# No standard +/-/space content markers; exclude ---/+++ headers
-	! grep "^+[^+]" actual &&
-	! grep "^-[^-]" actual &&
-	! grep "^ " actual
+	test_grep ! "^+[^+]" actual &&
+	test_grep ! "^-[^-]" actual &&
+	test_grep ! "^ " actual
 '
 
 test_expect_success '-L with -R reverses diff' '
 	git checkout parent-oids &&
 	git log -L:func2:file.c -R --format= -1 >actual &&
-	grep "^diff --git b/file.c a/file.c" actual &&
-	grep "^--- b/file.c" actual &&
-	grep "^+++ a/file.c" actual &&
+	test_grep "^diff --git b/file.c a/file.c" actual &&
+	test_grep "^--- b/file.c" actual &&
+	test_grep "^+++ a/file.c" actual &&
 	# The modification added "F2 + 2", so reversed it is removed
-	grep "^-.*F2 + 2" actual &&
-	grep "^+.*return F2;" actual
+	test_grep "^-.*F2 + 2" actual &&
+	test_grep "^+.*return F2;" actual
 '
 
 test_expect_success 'setup for color-moved test' '
@@ -629,8 +629,8 @@
 		--color=always --format= -1 >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	# Old moved lines: bold magenta; new moved lines: bold cyan
-	grep "BOLD;MAGENTA" actual &&
-	grep "BOLD;CYAN" actual
+	test_grep "BOLD;MAGENTA" actual &&
+	test_grep "BOLD;CYAN" actual
 '
 
 test_expect_success 'setup for no-newline-at-eof tests' '
@@ -650,14 +650,14 @@
 # newline, the "\ No newline at end of file" marker should appear.
 test_expect_success '-L no-newline-at-eof appears in tracked range' '
 	git log -L:bot:noeol.c --format= -1 HEAD~1 >actual &&
-	grep "No newline at end of file" actual
+	test_grep "No newline at end of file" actual
 '
 
 # When tracking a function that ends before the no-newline content,
 # the marker should not appear in the output.
 test_expect_success '-L no-newline-at-eof suppressed outside range' '
 	git log -L:top:noeol.c --format= >actual &&
-	! grep "No newline at end of file" actual
+	test_grep ! "No newline at end of file" actual
 '
 
 # When a commit removes a no-newline last line and replaces it with
@@ -665,7 +665,7 @@
 # old side of the diff).
 test_expect_success '-L no-newline-at-eof marker with deleted line' '
 	git log -L:bot:noeol.c --format= -1 >actual &&
-	grep "No newline at end of file" actual
+	test_grep "No newline at end of file" actual
 '
 
 test_expect_success 'setup for range boundary deletion test' '
@@ -725,7 +725,7 @@
 	# combined with the -L range walk, this selects commits that
 	# both touch func2 and change the count of "F2 + 2" in the file.
 	test $(grep -c "^diff --git" actual) = 1 &&
-	grep "F2 + 2" actual
+	test_grep "F2 + 2" actual
 '
 
 test_expect_success '-L with -G filters to diff-text matches' '
@@ -735,7 +735,7 @@
 	# combined with -L, this selects commits that both touch func2
 	# and have "F2 + 2" in their diff.
 	test $(grep -c "^diff --git" actual) = 1 &&
-	grep "F2 + 2" actual
+	test_grep "F2 + 2" actual
 '
 
 test_expect_success '-L with --diff-filter=M excludes root commit' '
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 1612f05..eebab71 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -5,11 +5,6 @@
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/lib-log-graph.sh
 
-check_graph () {
-	cat >expect &&
-	lib_test_cmp_graph --format=%s "$@"
-}
-
 test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
 	git checkout --orphan _p &&
 	test_commit A &&
@@ -21,7 +16,7 @@
 	git checkout _p && git merge --no-ff _r -m G &&
 	git checkout @^^ && git merge --no-ff _p -m H &&
 
-	check_graph <<-\EOF
+	lib_test_check_graph <<-\EOF
 	*   H
 	|\
 	| *   G
@@ -49,7 +44,7 @@
 	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
 	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
 
-	check_graph <<-\EOF
+	lib_test_check_graph <<-\EOF
 	*-----.   0_H
 	|\ \ \ \
 	| | | | * 0_G
@@ -83,7 +78,7 @@
 	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
 	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
 
-	check_graph <<-\EOF
+	lib_test_check_graph <<-\EOF
 	*   1_H
 	|\
 	| *   1_G
@@ -115,7 +110,7 @@
 	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
 	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
 
-	check_graph <<-\EOF
+	lib_test_check_graph <<-\EOF
 	*   2_K
 	|\
 	| *   2_J
@@ -151,7 +146,7 @@
 	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
 	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
 
-	check_graph <<-\EOF
+	lib_test_check_graph <<-\EOF
 	*   3_J
 	|\
 	| *   3_H
@@ -182,7 +177,7 @@
 	git merge --no-ff 4_p -m 4_G &&
 	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
 
-	check_graph --date-order <<-\EOF
+	lib_test_check_graph --date-order <<-\EOF
 	*   4_H
 	|\
 	| *   4_G
@@ -218,7 +213,7 @@
 	git checkout 5_r &&
 	git merge --no-ff 5_s -m 5_H &&
 
-	check_graph <<-\EOF
+	lib_test_check_graph <<-\EOF
 	*   5_H
 	|\
 	| *-.   5_G
@@ -257,7 +252,7 @@
 	git checkout 6_1 &&
 	git merge --no-ff 6_2 -m 6_I &&
 
-	check_graph 6_1 6_3 6_5 <<-\EOF
+	lib_test_check_graph 6_1 6_3 6_5 <<-\EOF
 	*   6_I
 	|\
 	| | *   6_H
@@ -334,7 +329,7 @@
 	git checkout -b M_7 7_1 &&
 	git merge --no-ff 7_2 7_3 -m 7_M4 &&
 
-	check_graph M_1 M_3 M_5 M_7 <<-\EOF
+	lib_test_check_graph M_1 M_3 M_5 M_7 <<-\EOF
 	*   7_M1
 	|\
 	| | *   7_M2
@@ -371,7 +366,7 @@
 '
 
 test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '
-	check_graph --graph-lane-limit=2 M_7 <<-\EOF
+	lib_test_check_graph --graph-lane-limit=2 M_7 <<-\EOF
 	*-.   7_M4
 	|\ \
 	| | * 7_G
@@ -388,7 +383,7 @@
 '
 
 test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '
-	check_graph --graph-lane-limit=1 M_7 <<-\EOF
+	lib_test_check_graph --graph-lane-limit=1 M_7 <<-\EOF
 	*-~  7_M4
 	|\~
 	| ~ 7_G
@@ -405,7 +400,7 @@
 '
 
 test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '
-	check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\EOF
+	lib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\EOF
 	*   7_M1
 	|\
 	| | *   7_M2
@@ -441,7 +436,7 @@
 '
 
 test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '
-	check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\EOF
+	lib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\EOF
 	*   7_M1
 	|\
 	| | *   7_M2
@@ -478,7 +473,7 @@
 '
 
 test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '
-	check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\EOF
+	lib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\EOF
 	*   7_M1
 	|\
 	| | *   7_M2
diff --git a/t/t4216-log-bloom.sh b/t/t4216-log-bloom.sh
index 16bc39c..ad26866 100755
--- a/t/t4216-log-bloom.sh
+++ b/t/t4216-log-bloom.sh
@@ -224,10 +224,10 @@
 		GIT_TEST_BLOOM_SETTINGS_NUM_HASHES=9 \
 		GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY=15 \
 		git commit-graph write --reachable --changed-paths &&
-	grep "{\"hash_version\":1,\"num_hashes\":9,\"bits_per_entry\":15,\"max_changed_paths\":512" trace2.txt &&
+	test_grep "{\"hash_version\":1,\"num_hashes\":9,\"bits_per_entry\":15,\"max_changed_paths\":512" trace2.txt &&
 	GIT_TRACE2_EVENT="$(pwd)/trace2-auto.txt" \
 		git commit-graph write --reachable --changed-paths &&
-	grep "{\"hash_version\":1,\"num_hashes\":9,\"bits_per_entry\":15,\"max_changed_paths\":512" trace2-auto.txt
+	test_grep "{\"hash_version\":1,\"num_hashes\":9,\"bits_per_entry\":15,\"max_changed_paths\":512" trace2-auto.txt
 '
 
 test_max_changed_paths () {
@@ -494,7 +494,7 @@
 		>expect 2>err &&
 	git -C $repo log --oneline --no-decorate -- file >actual 2>err &&
 	test_cmp expect actual &&
-	grep "disabling Bloom filters for commit-graph layer .$layer." err
+	test_grep "disabling Bloom filters for commit-graph layer .$layer." err
 '
 
 test_expect_success 'merge graph layers with incompatible Bloom settings' '
@@ -503,8 +503,8 @@
 	>trace2.txt &&
 	GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
 		git -C $repo commit-graph write --reachable --changed-paths 2>err &&
-	grep "disabling Bloom filters for commit-graph layer .$layer." err &&
-	grep "{\"hash_version\":1,\"num_hashes\":7,\"bits_per_entry\":10,\"max_changed_paths\":512" trace2.txt &&
+	test_grep "disabling Bloom filters for commit-graph layer .$layer." err &&
+	test_grep "{\"hash_version\":1,\"num_hashes\":7,\"bits_per_entry\":10,\"max_changed_paths\":512" trace2.txt &&
 
 	test_path_is_file $repo/$graph &&
 	test_dir_is_empty $repo/$graphdir &&
@@ -516,7 +516,7 @@
 		git -C $repo log --oneline --no-decorate -- file >actual 2>err &&
 
 	test_cmp expect actual &&
-	grep "statistics:{\"filter_not_present\":0," trace.perf &&
+	test_grep "statistics:{\"filter_not_present\":0," trace.perf &&
 	test_must_be_empty err
 '
 
@@ -554,8 +554,8 @@
 	>trace2.txt &&
 	GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
 		git -C $repo -c commitGraph.changedPathsVersion=2 commit-graph write --reachable --changed-paths 2>err &&
-	grep "disabling Bloom filters for commit-graph layer .$layer." err &&
-	grep "{\"hash_version\":2,\"num_hashes\":7,\"bits_per_entry\":10,\"max_changed_paths\":512" trace2.txt
+	test_grep "disabling Bloom filters for commit-graph layer .$layer." err &&
+	test_grep "{\"hash_version\":2,\"num_hashes\":7,\"bits_per_entry\":10,\"max_changed_paths\":512" trace2.txt
 '
 
 get_first_changed_path_filter () {
@@ -755,7 +755,7 @@
 test_expect_success PERL_TEST_HELPERS 'Bloom reader notices out-of-bounds filter offsets' '
 	check_corrupt_graph BIDX 12 FFFFFFFF &&
 	# use grep to avoid depending on exact chunk size
-	grep "warning: ignoring out-of-range offset (4294967295) for changed-path filter at pos 3 of .git/objects/info/commit-graph" err
+	test_grep "warning: ignoring out-of-range offset (4294967295) for changed-path filter at pos 3 of .git/objects/info/commit-graph" err
 '
 
 test_expect_success PERL_TEST_HELPERS 'Bloom reader notices too-small index chunk' '
diff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh
new file mode 100755
index 0000000..24dc9b4
--- /dev/null
+++ b/t/t4218-log-graph-indentation.sh
@@ -0,0 +1,596 @@
+#!/bin/sh
+
+test_description='git log --graph visual root indentations'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-log-graph.sh
+
+check_graph_with_description () {
+	cat >expect &&
+	lib_test_cmp_graph --format="%s%ndescription%nsecond-line" "$@"
+}
+
+create_orphan () {
+	git checkout --orphan "$1" &&
+	test_might_fail git rm -rf .
+}
+
+# disable commit-graph topo order to have the graph to render in different
+# ways (used in --first-parent tests to have multiple visual roots while a
+# column is active at the same time).
+unset_commit_graph () {
+	sane_unset GIT_TEST_COMMIT_GRAPH &&
+	rm -f .git/objects/info/commit-graph &&
+	rm -rf .git/objects/info/commit-graphs
+}
+
+test_expect_success 'single root commit is not indented' '
+	create_orphan _1 && test_commit 1_A &&
+	lib_test_check_graph _1 <<-\EOF
+	* 1_A
+	EOF
+'
+
+test_expect_success 'visual root indented before unrelated branch' '
+	create_orphan _2 && test_commit 2_A && test_commit 2_B &&
+	create_orphan _3 && test_commit 3_A &&
+	lib_test_check_graph _2 _3 <<-\EOF
+	  * 3_A
+	* 2_B
+	* 2_A
+	EOF
+'
+
+test_expect_success 'visual root indentation with --left-right' '
+	lib_test_check_graph --left-right _2..._3 <<-\EOF
+	  > 3_A
+	< 2_B
+	< 2_A
+	EOF
+'
+
+# A better case of why indentation is still needed with '--left-right' flag is
+# that unrelated branches can be on the same side, so it's needed to
+# differentiate visual roots on the same side.
+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '
+	lib_test_check_graph --left-right _2..._3 _1 <<-\EOF
+	  > 3_A
+	< 2_B
+	 \
+	  < 2_A
+	> 1_A
+	EOF
+'
+
+test_expect_success 'visual root indents the description also' '
+	check_graph_with_description _2 _3 <<-\EOF
+	  * 3_A
+	    description
+	    second-line
+	* 2_B
+	| description
+	| second-line
+	* 2_A
+	  description
+	  second-line
+	EOF
+'
+
+test_expect_success 'indented visual root parent gets connected to its child' '
+	create_orphan _4 && test_commit 4_A && test_commit 4_B &&
+	create_orphan _5 && test_commit 5_A && test_commit 5_B &&
+	lib_test_check_graph _4 _5 <<-\EOF
+	* 5_B
+	 \
+	  * 5_A
+	* 4_B
+	* 4_A
+	EOF
+'
+
+test_expect_success 'indented visual root parent gets connected to its child with description' '
+	check_graph_with_description _4 _5 <<-\EOF
+	* 5_B
+	| description
+	| second-line
+	 \
+	  * 5_A
+	    description
+	    second-line
+	* 4_B
+	| description
+	| second-line
+	* 4_A
+	  description
+	  second-line
+	EOF
+'
+
+test_expect_success 'visual roots cascade and last root does not' '
+	create_orphan _7 && test_commit 7_A && test_commit 7_B &&
+	create_orphan _8 && test_commit 8_A &&
+	create_orphan _9 && test_commit 9_A &&
+	create_orphan _10 && test_commit 10_A &&
+	lib_test_check_graph _7 _8 _9 _10 <<-\EOF
+	* 10_A
+	  * 9_A
+	    * 8_A
+	* 7_B
+	* 7_A
+	EOF
+'
+
+test_expect_success 'last root does not cascade' '
+	lib_test_check_graph _8 _9 _10 <<-\EOF
+	* 10_A
+	  * 9_A
+	* 8_A
+	EOF
+'
+
+test_expect_success 'merge parents are roots between them but they do not indent' '
+	create_orphan _11 && test_commit 11_A &&
+	create_orphan _12 && test_commit 12_A &&
+	create_orphan _13 && test_commit 13_A &&
+	git checkout _11 &&
+	TREE=$(git write-tree) &&
+	MERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&
+	git reset --hard $MERGE &&
+	lib_test_check_graph _11 <<-\EOF
+	*-.   11_octopus
+	|\ \
+	| | * 13_A
+	| * 12_A
+	* 11_A
+	EOF
+'
+
+# The last parent of a merge can be indented if nothing related to it needs to
+# be rendered after, if it's another visual root, merge parent must not get
+# indented but rather activate cascading.
+test_expect_success 'merge then unrelated visual root and unrelated branch' '
+	create_orphan _16 && test_commit 16_A && test_commit 16_B &&
+	create_orphan _17 && test_commit 17_A &&
+	create_orphan _18 && test_commit 18_A &&
+	create_orphan _19 && test_commit 19_A &&
+	create_orphan _20 && test_commit 20_A &&
+	git checkout _18 &&
+	TREE=$(git write-tree) &&
+	MERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&
+	git reset --hard $MERGE &&
+	lib_test_check_graph _18 _17 _16 <<-\EOF
+	*-.   18_octopus
+	|\ \
+	| | * 20_A
+	| * 19_A
+	* 18_A
+	  * 17_A
+	* 16_B
+	* 16_A
+	EOF
+'
+
+# The last commit root does not get indented, if the next thing after the root
+# merge parent is the last commit, indent the merge parent.
+test_expect_success 'merge then unrelated root indents merge parent' '
+	lib_test_check_graph _18 _17 <<-\EOF
+	*-.   18_octopus
+	|\ \
+	| | * 20_A
+	| * 19_A
+	 \
+	  * 18_A
+	* 17_A
+	EOF
+'
+
+test_expect_success 'merge then unrelated branch indents merge parent' '
+	lib_test_check_graph _18 _16 <<-\EOF
+	*-.   18_octopus
+	|\ \
+	| | * 20_A
+	| * 19_A
+	 \
+	  * 18_A
+	* 16_B
+	* 16_A
+	EOF
+'
+
+test_expect_success 'two-parent merge of orphans' '
+	create_orphan _21 && test_commit 21_A &&
+	create_orphan _22 && test_commit 22_A &&
+	git checkout _21 &&
+	TREE=$(git write-tree) &&
+	MERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&
+	git reset --hard $MERGE &&
+	lib_test_check_graph _21 <<-\EOF
+	*   21_merge
+	|\
+	| * 22_A
+	* 21_A
+	EOF
+'
+
+test_expect_success 'commit with filtered parent becomes a visual root' '
+	create_orphan _23 &&
+	echo test >other.txt &&
+	git add other.txt &&
+	git commit -m "23_A" &&
+	echo test >foo.txt &&
+	git add foo.txt &&
+	git commit -m "23_B" &&
+	create_orphan _24 &&
+	echo test >foo.txt &&
+	git add foo.txt &&
+	git commit -m "24_A" &&
+	lib_test_check_graph _23 _24 -- foo.txt <<-\EOF
+	  * 23_B
+	* 24_A
+	EOF
+'
+
+test_expect_success 'filtered parent cascading edge case' '
+	create_orphan _27 &&
+	echo test >foo.txt &&
+	git add foo.txt &&
+	test_tick &&
+	git commit -m "D (last)" &&
+
+	create_orphan _25 &&
+	echo test >other.txt &&
+	git add other.txt &&
+	test_tick &&
+	git commit -m "C-filtered" &&
+
+	echo test >foo.txt &&
+	git add foo.txt &&
+	test_tick &&
+	git commit -m "B (child of filtered)" &&
+
+	create_orphan _26 &&
+	echo test >foo.txt &&
+	git add foo.txt &&
+	test_tick &&
+	git commit -m "A (visual root)" &&
+
+	lib_test_check_graph _25 _26 _27 -- foo.txt <<-\EOF
+	* A (visual root)
+	  * B (child of filtered)
+	* D (last)
+	EOF
+'
+
+test_expect_success 'multiple filtered parents in sequence' '
+	create_orphan _44 &&
+	echo a >other.txt && git add other.txt && git commit -m "44_F" &&
+	echo b >foo.txt && git add foo.txt && git commit -m "44_C" &&
+
+	create_orphan _45 &&
+	echo c >other.txt && git add other.txt && git commit -m "45_F" &&
+	echo d >foo.txt && git add foo.txt && git commit -m "45_C" &&
+
+	create_orphan _46 &&
+	echo e >foo.txt && git add foo.txt && git commit -m "46_A" &&
+
+	lib_test_check_graph _44 _45 _46 -- foo.txt <<-\EOF
+	* 44_C
+	  * 45_C
+	* 46_A
+	EOF
+'
+
+# These tests prove why there is no need to have indentation for boundary
+# commits.
+#
+# Boundary commits rather than starting a column they 'inherit' the one of
+# its child so there will always be an edge that connects it removing the
+# ambiguity.
+test_expect_success 'unrelated boundaries are not ambiguous' '
+	create_orphan _28 && test_commit 28_A && test_commit 28_B &&
+	test_commit 28_C &&
+	create_orphan _29 && test_commit 29_A && test_commit 29_B &&
+	lib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\EOF
+	* 29_B
+	| * 28_C
+	| * 28_B
+	| o 28_A
+	o 29_A
+	EOF
+'
+
+# Same structure as t6016
+test_expect_success 'boundary commits big test' '
+	# 3 commits on branch _30
+	create_orphan _30 &&
+	test_commit 30_A &&
+	test_commit 30_B &&
+	test_commit 30_C &&
+
+	# 2 commits on branch _31, started from 30_A
+	git checkout -b _31 30_A &&
+	test_commit 31_A &&
+	test_commit 31_B &&
+
+	# 2 commits on branch _32, started from 30_B
+	git checkout -b _32 30_B &&
+	test_commit 32_A &&
+	test_commit 32_B &&
+
+	# Octopus merge _31 and _32 into -30
+	git checkout _30 &&
+	git merge _31 _32 -m 30_D &&
+	git tag 30_D &&
+	test_commit 30_E &&
+
+	# More commits on _32, then merge _32 into _30
+	git checkout _32 &&
+	test_commit 32_C &&
+	test_commit 32_D &&
+	git checkout _30 &&
+	git merge -s ours _32 -m 30_F &&
+	git tag 30_F &&
+	test_commit 30_G &&
+	lib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\EOF
+	* 30_G
+	*   30_F
+	|\
+	| * 32_D
+	* | 30_E
+	| |
+	|  \
+	*-. \   30_D
+	|\ \ \
+	| * | | 31_B
+	| * | | 31_A
+	* | | | 30_C
+	o | | | 30_B
+	|/ / /
+	o / / 30_A
+	 / /
+	| o 32_C
+	|/
+	o 32_B
+	EOF
+'
+
+# Filter by --first-parent and then forcing the filtered parents to be shown.
+test_expect_success '--first-parent flag with the filtered parents' '
+	(
+		unset_commit_graph &&
+		create_orphan _35 && test_commit 35_A && test_commit 35_B &&
+		create_orphan _36 && test_commit 36_A &&
+		create_orphan _37 && test_commit 37_A &&
+		git checkout _35 &&
+		TREE=$(git write-tree) &&
+		MERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&
+		git reset --hard $MERGE &&
+		lib_test_check_graph --first-parent _35 _36 _37 <<-\EOF
+		* 35_octopus
+		| * 37_A
+		|   * 36_A
+		* 35_B
+		* 35_A
+		EOF
+	)
+'
+
+test_expect_success '--first-parent with filtered parents but one has a child' '
+	(
+		unset_commit_graph &&
+		create_orphan _38 && test_commit 38_A && test_commit 38_B &&
+		create_orphan _39 && test_commit 39_A &&
+		create_orphan _40 && test_commit 40_A && test_commit 40_B &&
+		git checkout _38 &&
+		TREE=$(git write-tree) &&
+		MERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&
+		git reset --hard $MERGE &&
+		lib_test_check_graph --first-parent _38 _39 _40 <<-\EOF
+		* 38_octopus
+		| * 40_B
+		| * 40_A
+		|   * 39_A
+		* 38_B
+		* 38_A
+		EOF
+	)
+'
+
+test_expect_success '--first-parent with filtered parents but both have children' '
+	(
+		unset_commit_graph &&
+		create_orphan _41 && test_commit 41_A && test_commit 41_B &&
+		create_orphan _42 && test_commit 42_A && test_commit 42_B &&
+		create_orphan _43 && test_commit 43_A && test_commit 43_B &&
+		git checkout _41 &&
+		TREE=$(git write-tree) &&
+		MERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&
+		git reset --hard $MERGE &&
+		lib_test_check_graph --first-parent _41 _42 _43 <<-\EOF
+		* 41_octopus
+		| * 43_B
+		|  \
+		|   * 43_A
+		| * 42_B
+		| * 42_A
+		* 41_B
+		* 41_A
+		EOF
+	)
+'
+
+test_expect_success 'two unrelated merges' '
+	create_orphan _50 && test_commit 50_A &&
+	git checkout -b _51 &&
+	test_commit 51_A && test_commit 51_B &&
+	git checkout _50 &&
+	git merge --no-ff _51 -m 50_B &&
+
+	create_orphan _52 && test_commit 52_A &&
+	git checkout -b _53 &&
+	test_commit 53_A && test_commit 53_B &&
+	git checkout _52 &&
+	git merge --no-ff _53 -m 52_B &&
+
+	lib_test_check_graph _52 _50 <<-\EOF
+	*   52_B
+	|\
+	| * 53_B
+	| * 53_A
+	|/
+	 \
+	  * 52_A
+	*   50_B
+	|\
+	| * 51_B
+	| * 51_A
+	|/
+	* 50_A
+	EOF
+'
+
+test_expect_success '--max-count treats the last visible commit as the last commit' '
+	lib_test_check_graph --max-count=2 _8 _9 _10 <<-\EOF
+	  * 10_A
+	* 9_A
+	EOF
+'
+
+test_expect_success '--max-count=1 shows a single root without indentation' '
+	lib_test_check_graph --max-count=1 _8 _9 _10 <<-\EOF
+	* 10_A
+	EOF
+'
+
+test_expect_success '--max-count-oldest indents visual roots' '
+	lib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\EOF
+	* 10_A
+	  * 9_A
+	* 8_A
+	EOF
+'
+
+# when the graph commits are filtered with regex options like --author, the
+# commit parents do not come NULL so it is needed to check if the parents are
+# interesting.
+test_expect_success '--author skipped parent makes a visual root' '
+	create_orphan _55 &&
+	test_tick &&
+	git commit --allow-empty -m 55_A &&
+	create_orphan _54 &&
+	test_tick &&
+	git commit --allow-empty --author="Other <other@example.com>" -m 54_A &&
+	test_tick &&
+	git commit --allow-empty -m 54_B &&
+	test_tick &&
+	git commit --allow-empty -m 54_C &&
+	lib_test_check_graph --author="A U Thor" _54 _55 <<-\EOF
+	* 54_C
+	 \
+	  * 54_B
+	* 55_A
+	EOF
+'
+
+test_expect_success '--grep skipped parent makes a visual root' '
+	create_orphan _57 &&
+	test_tick &&
+	git commit --allow-empty -m 57_keep_A &&
+	create_orphan _56 &&
+	test_tick &&
+	git commit --allow-empty -m 56_skip &&
+	test_tick &&
+	git commit --allow-empty -m 56_keep_A &&
+	test_tick &&
+	git commit --allow-empty -m 56_keep_B &&
+	lib_test_check_graph --grep=keep _56 _57 <<-\EOF
+	* 56_keep_B
+	 \
+	  * 56_keep_A
+	* 57_keep_A
+	EOF
+'
+
+# The cascading wraps after 4 columns and when wraping (column % 4 == 0) if the
+# next is a non visual-root, force indentation to avoid an ambiguous graph
+# (commit 59_A is forcefully indented)
+test_expect_success 'visual root cascading gets wrapped after 4 columns' '
+	create_orphan _58 && test_commit 58_A && test_commit 58_B &&
+	create_orphan _59 && test_commit 59_A &&
+	create_orphan _60 && test_commit 60_A &&
+	create_orphan _61 && test_commit 61_A &&
+	create_orphan _62 && test_commit 62_A &&
+	create_orphan _63 && test_commit 63_A &&
+	create_orphan _64 && test_commit 64_A &&
+	create_orphan _65 && test_commit 65_A &&
+	create_orphan _66 && test_commit 66_A &&
+	create_orphan _67 && test_commit 67_A &&
+	lib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF
+	* 67_A
+	  * 66_A
+	    * 65_A
+	      * 64_A
+	* 63_A
+	  * 62_A
+	    * 61_A
+	      * 60_A
+	  * 59_A
+	* 58_B
+	* 58_A
+	EOF
+'
+
+test_expect_success '--no-graph-indent disables indentation' '
+	lib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF
+	* 67_A
+	* 66_A
+	* 65_A
+	* 64_A
+	* 63_A
+	* 62_A
+	* 61_A
+	* 60_A
+	* 59_A
+	* 58_B
+	* 58_A
+	EOF
+'
+
+test_expect_success 'log.graphIndent config disables indentation' '
+	test_config log.graphIndent false &&
+	lib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF
+	* 67_A
+	* 66_A
+	* 65_A
+	* 64_A
+	* 63_A
+	* 62_A
+	* 61_A
+	* 60_A
+	* 59_A
+	* 58_B
+	* 58_A
+	EOF
+'
+
+test_expect_success '--graph-indent forces indentation when graph.indent is unset' '
+	test_config log.graphIndent false &&
+	lib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\EOF
+	* 67_A
+	  * 66_A
+	    * 65_A
+	      * 64_A
+	* 63_A
+	  * 62_A
+	    * 61_A
+	      * 60_A
+	  * 59_A
+	* 58_B
+	* 58_A
+	EOF
+'
+
+# log.graphIndent unset and no --option (which activates graph indentation) is
+# the default state.
+
+test_done
diff --git a/t/t4252-am-options.sh b/t/t4252-am-options.sh
index bda8822..c36c7e9 100755
--- a/t/t4252-am-options.sh
+++ b/t/t4252-am-options.sh
@@ -20,8 +20,8 @@
 	git reset --hard initial &&
 	test_must_fail git am --whitespace=fix "$tm"/am-test-1-? &&
 	git am --skip &&
-	grep 3 file-1 &&
-	grep "^Six$" file-2
+	test_grep 3 file-1 &&
+	test_grep "^Six$" file-2
 '
 
 test_expect_success 'interrupted am -C1' '
@@ -29,8 +29,8 @@
 	git reset --hard initial &&
 	test_must_fail git am -C1 "$tm"/am-test-2-? &&
 	git am --skip &&
-	grep 3 file-1 &&
-	grep "^Three$" file-2
+	test_grep 3 file-1 &&
+	test_grep "^Three$" file-2
 '
 
 test_expect_success 'interrupted am -p2' '
@@ -38,8 +38,8 @@
 	git reset --hard initial &&
 	test_must_fail git am -p2 "$tm"/am-test-3-? &&
 	git am --skip &&
-	grep 3 file-1 &&
-	grep "^Three$" file-2
+	test_grep 3 file-1 &&
+	test_grep "^Three$" file-2
 '
 
 test_expect_success 'interrupted am -C1 -p2' '
@@ -47,8 +47,8 @@
 	git reset --hard initial &&
 	test_must_fail git am -p2 -C1 "$tm"/am-test-4-? &&
 	git am --skip &&
-	grep 3 file-1 &&
-	grep "^Three$" file-2
+	test_grep 3 file-1 &&
+	test_grep "^Three$" file-2
 '
 
 test_expect_success 'interrupted am --directory="frotz nitfol"' '
@@ -56,7 +56,7 @@
 	git reset --hard initial &&
 	test_must_fail git am --directory="frotz nitfol" "$tm"/am-test-5-? &&
 	git am --skip &&
-	grep One "frotz nitfol/file-5"
+	test_grep One "frotz nitfol/file-5"
 '
 
 test_expect_success 'apply to a funny path' '
@@ -71,9 +71,9 @@
 	rm -rf .git/rebase-apply &&
 	git reset --hard initial &&
 	test_must_fail git am --reject "$tm"/am-test-6-1 &&
-	grep "@@ -1,3 +1,3 @@" file-2.rej &&
+	test_grep "@@ -1,3 +1,3 @@" file-2.rej &&
 	test_must_fail git diff-files --exit-code --quiet file-2 &&
-	grep "[-]-reject" .git/rebase-apply/apply-opt
+	test_grep "[-]-reject" .git/rebase-apply/apply-opt
 '
 
 test_done
diff --git a/t/t4254-am-corrupt.sh b/t/t4254-am-corrupt.sh
index 96ddf3c..f68b5d3 100755
--- a/t/t4254-am-corrupt.sh
+++ b/t/t4254-am-corrupt.sh
@@ -73,16 +73,16 @@
 	test_when_finished "git am --abort" &&
 	make_mbox_with_nul body >body.patch &&
 	test_must_fail git am body.patch 2>err &&
-	grep "a NUL byte in commit log message not allowed" err
+	test_grep "a NUL byte in commit log message not allowed" err
 '
 
 test_expect_success "NUL in commit message's header" "
 	test_when_finished 'git am --abort' &&
 	make_mbox_with_nul subject >subject.patch &&
 	test_must_fail git mailinfo msg patch <subject.patch 2>err &&
-	grep \"a NUL byte in 'Subject' is not allowed\" err &&
+	test_grep \"a NUL byte in 'Subject' is not allowed\" err &&
 	test_must_fail git am subject.patch 2>err &&
-	grep \"a NUL byte in 'Subject' is not allowed\" err
+	test_grep \"a NUL byte in 'Subject' is not allowed\" err
 "
 
 test_done
diff --git a/t/t4258-am-quoted-cr.sh b/t/t4258-am-quoted-cr.sh
index 201915b..50e81b9 100755
--- a/t/t4258-am-quoted-cr.sh
+++ b/t/t4258-am-quoted-cr.sh
@@ -16,7 +16,7 @@
 test_expect_success 'am warn if quoted-cr is found' '
 	git reset --hard one &&
 	test_must_fail git am "$DATA/mbox" 2>err &&
-	grep "quoted CRLF detected" err
+	test_grep "quoted CRLF detected" err
 '
 
 test_expect_success 'am --quoted-cr=strip' '
diff --git a/t/t4301-merge-tree-write-tree.sh b/t/t4301-merge-tree-write-tree.sh
index 6e117ee..b268491 100755
--- a/t/t4301-merge-tree-write-tree.sh
+++ b/t/t4301-merge-tree-write-tree.sh
@@ -88,7 +88,7 @@
 # Repeat the previous test, but turn off rename detection
 test_expect_success 'Failed merge without rename detection' '
 	test_must_fail git -c diff.renames=false merge-tree --write-tree side1 side3 >out &&
-	grep "CONFLICT (modify/delete): numbers deleted" out
+	test_grep "CONFLICT (modify/delete): numbers deleted" out
 '
 
 test_expect_success  '--quiet on conflicted merge' '
@@ -161,13 +161,13 @@
 	# Mis-spell with single "s" instead of double "s"
 	test_expect_code 129 git merge-tree --write-tree --mesages FOOBAR side1 side2 2>expect &&
 
-	grep "error: unknown option.*mesages" expect
+	test_grep "error: unknown option.*mesages" expect
 '
 
 test_expect_success 'Barf on too many arguments' '
 	test_expect_code 129 git merge-tree --write-tree side1 side2 invalid 2>expect &&
 
-	grep "^usage: git merge-tree" expect
+	test_grep "^usage: git merge-tree" expect
 '
 
 anonymize_hash() {
@@ -352,7 +352,7 @@
 		#
 		hash=$(tr "\0" "\n" <out | head -n 3 | grep 3.bar | cut -f 2 -d " ") &&
 		git rev-list --objects --all >all_blobs &&
-		! grep $hash all_blobs &&
+		test_grep ! $hash all_blobs &&
 
 		#
 		# Second, check anonymized hash output against expectation
@@ -419,7 +419,7 @@
 		#
 		hash=$(tr "\0" "\n" <out | head -n 3 | grep 3.bar | cut -f 2 -d " ") &&
 		git rev-list --objects --all >all_blobs &&
-		! grep $hash all_blobs &&
+		test_grep ! $hash all_blobs &&
 
 		#
 		# Second, check anonymized hash output against expectation
@@ -670,8 +670,8 @@
 		hash1=$(tr "\0" "\n" <out | head | grep 2.four | cut -f 2 -d " ") &&
 		hash2=$(tr "\0" "\n" <out | head | grep 3.two | cut -f 2 -d " ") &&
 		git rev-list --objects --all >all_blobs &&
-		! grep $hash1 all_blobs &&
-		! grep $hash2 all_blobs &&
+		test_grep ! $hash1 all_blobs &&
+		test_grep ! $hash2 all_blobs &&
 
 		#
 		# Now compare anonymized hash output with expectation
@@ -857,7 +857,7 @@
 test_expect_success 'error out by default for unrelated histories' '
 	test_expect_code 128 git merge-tree --write-tree side1 unrelated 2>error &&
 
-	grep "refusing to merge unrelated histories" error
+	test_grep "refusing to merge unrelated histories" error
 '
 
 test_expect_success 'can override merge of unrelated histories' '
@@ -924,7 +924,7 @@
 test_expect_success '--merge-base is incompatible with --stdin' '
 	test_must_fail git merge-tree --merge-base=side1 --stdin 2>expect &&
 
-	grep "^fatal: .*merge-base.*stdin.* cannot be used together" expect
+	test_grep "^fatal: .*merge-base.*stdin.* cannot be used together" expect
 '
 
 # specify merge-base as parent of branch2
diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh
index a8c2853..3ad600c 100755
--- a/t/t5000-tar-tree.sh
+++ b/t/t5000-tar-tree.sh
@@ -323,14 +323,14 @@
 
 test_expect_success 'archive --list mentions user filter' '
 	git archive --list >output &&
-	grep "^tar\.foo\$" output &&
-	grep "^bar\$" output
+	test_grep "^tar\.foo\$" output &&
+	test_grep "^bar\$" output
 '
 
 test_expect_success 'archive --list shows only enabled remote filters' '
 	git archive --list --remote=. >output &&
-	! grep "^tar\.foo\$" output &&
-	grep "^bar\$" output
+	test_grep ! "^tar\.foo\$" output &&
+	test_grep "^bar\$" output
 '
 
 test_expect_success 'invoke tar filter by format' '
@@ -440,7 +440,7 @@
 
 test_expect_success 'reject paths outside the current directory' '
 	test_must_fail git -C a/bin archive HEAD .. >/dev/null 2>err &&
-	grep "outside the current directory" err
+	test_grep "outside the current directory" err
 '
 
 test_expect_success 'allow pathspecs that resolve to the current directory' '
diff --git a/t/t5004-archive-corner-cases.sh b/t/t5004-archive-corner-cases.sh
index df513a4..768b0ff 100755
--- a/t/t5004-archive-corner-cases.sh
+++ b/t/t5004-archive-corner-cases.sh
@@ -205,7 +205,7 @@
 
 	"$GIT_UNZIP" -t big.zip &&
 	"$ZIPINFO" big.zip >big.lst &&
-	grep $size big.lst
+	test_grep $size big.lst
 '
 
 build_tree() {
diff --git a/t/t5100-mailinfo.sh b/t/t5100-mailinfo.sh
index e57e1ae..e01078a 100755
--- a/t/t5100-mailinfo.sh
+++ b/t/t5100-mailinfo.sh
@@ -265,7 +265,7 @@
 	check_quoted_cr_mail quoted-cr/0001 &&
 	test_must_be_empty quoted-cr/0001.err &&
 	check_quoted_cr_mail quoted-cr/0002 &&
-	grep "quoted CRLF detected" quoted-cr/0002.err &&
+	test_grep "quoted CRLF detected" quoted-cr/0002.err &&
 	check_quoted_cr_mail quoted-cr/0001 --quoted-cr=nowarn &&
 	test_must_be_empty quoted-cr/0001.err &&
 	check_quoted_cr_mail quoted-cr/0002 --quoted-cr=nowarn &&
diff --git a/t/t5150-request-pull.sh b/t/t5150-request-pull.sh
index 270ce6e..67c5dc7 100755
--- a/t/t5150-request-pull.sh
+++ b/t/t5150-request-pull.sh
@@ -130,8 +130,8 @@
 		test_must_fail git request-pull initial "$downstream_url" \
 			2>../err
 	) &&
-	grep "No match for commit .*" err &&
-	grep "Are you sure you pushed" err
+	test_grep "No match for commit .*" err &&
+	test_grep "Are you sure you pushed" err
 
 '
 
@@ -230,7 +230,7 @@
 		cd local &&
 		git request-pull initial "$downstream_url" full
 	) >request &&
-	grep " tags/full\$" request
+	test_grep " tags/full\$" request
 '
 
 test_expect_success 'request-pull ignores OPTIONS_KEEPDASHDASH poison' '
@@ -260,8 +260,8 @@
 		test_must_fail git request-pull initial "$downstream_url" tags/v2.0 \
 			2>../err
 	) &&
-	grep "No match for commit .*" err &&
-	grep "Are you sure you pushed" err
+	test_grep "No match for commit .*" err &&
+	test_grep "Are you sure you pushed" err
 
 '
 
@@ -277,8 +277,8 @@
 		test_must_fail git request-pull initial "$downstream_url" tags/full \
 			2>../err
 	) &&
-	grep "points to a different object" err &&
-	grep "Are you sure you pushed" err
+	test_grep "points to a different object" err &&
+	test_grep "Are you sure you pushed" err
 
 '
 
@@ -295,8 +295,8 @@
 		test_must_fail git request-pull initial "$downstream_url" tags/full \
 			2>../err
 	) &&
-	grep "points to a different object" err &&
-	grep "Are you sure you pushed" err
+	test_grep "points to a different object" err &&
+	test_grep "Are you sure you pushed" err
 
 '
 
diff --git a/t/t5200-update-server-info.sh b/t/t5200-update-server-info.sh
index a551e95..a0630cc 100755
--- a/t/t5200-update-server-info.sh
+++ b/t/t5200-update-server-info.sh
@@ -47,7 +47,7 @@
 '
 
 test_expect_success 'update-server-info does not crash with -h' '
-	test_expect_code 129 git update-server-info -h >usage &&
+	git update-server-info -h >usage &&
 	test_grep "[Uu]sage: git update-server-info " usage
 '
 
diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh
index 7344578..9dabb36 100755
--- a/t/t5300-pack-object.sh
+++ b/t/t5300-pack-object.sh
@@ -548,18 +548,18 @@
 	test_must_fail git index-pack --threads=2 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 1 warnings &&
-	grep -F "no threads support, ignoring --threads=2" err &&
+	test_grep -F "no threads support, ignoring --threads=2" err &&
 
 	test_must_fail git -c pack.threads=2 index-pack 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 1 warnings &&
-	grep -F "no threads support, ignoring pack.threads" err &&
+	test_grep -F "no threads support, ignoring pack.threads" err &&
 
 	test_must_fail git -c pack.threads=2 index-pack --threads=4 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 2 warnings &&
-	grep -F "no threads support, ignoring --threads=4" err &&
-	grep -F "no threads support, ignoring pack.threads" err
+	test_grep -F "no threads support, ignoring --threads=4" err &&
+	test_grep -F "no threads support, ignoring pack.threads" err
 '
 
 test_expect_success !PTHREADS,!FAIL_PREREQS \
@@ -567,18 +567,18 @@
 	git pack-objects --threads=2 --stdout --all </dev/null >/dev/null 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 1 warnings &&
-	grep -F "no threads support, ignoring --threads" err &&
+	test_grep -F "no threads support, ignoring --threads" err &&
 
 	git -c pack.threads=2 pack-objects --stdout --all </dev/null >/dev/null 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 1 warnings &&
-	grep -F "no threads support, ignoring pack.threads" err &&
+	test_grep -F "no threads support, ignoring pack.threads" err &&
 
 	git -c pack.threads=2 pack-objects --threads=4 --stdout --all </dev/null >/dev/null 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 2 warnings &&
-	grep -F "no threads support, ignoring --threads" err &&
-	grep -F "no threads support, ignoring pack.threads" err
+	test_grep -F "no threads support, ignoring --threads" err &&
+	test_grep -F "no threads support, ignoring pack.threads" err
 '
 
 test_expect_success 'pack-objects in too-many-packs mode' '
@@ -720,14 +720,14 @@
 
 	# --stdout option silently removes --write-bitmap-index
 	git pack-objects --stdout --all --name-hash-version=2 --write-bitmap-index >out 2>err &&
-	! test_grep "currently, --write-bitmap-index requires --name-hash-version=1" err
+	test_grep ! "currently, --write-bitmap-index requires --name-hash-version=1" err
 '
 
 test_expect_success '--path-walk pack everything' '
 	git -C server rev-parse HEAD >in &&
 	GIT_PROGRESS_DELAY=0 git -C server pack-objects \
 		--stdout --revs --path-walk --progress <in >out.pack 2>err &&
-	grep "Compressing objects by path" err &&
+	test_grep "Compressing objects by path" err &&
 	git -C server index-pack --stdin <out.pack
 '
 
@@ -738,7 +738,7 @@
 	EOF
 	GIT_PROGRESS_DELAY=0 git -C server pack-objects \
 		--thin --stdout --revs --path-walk --progress <in >out.pack 2>err &&
-	grep "Compressing objects by path" err &&
+	test_grep "Compressing objects by path" err &&
 	git -C server index-pack --fix-thin --stdin <out.pack
 '
 
diff --git a/t/t5302-pack-index.sh b/t/t5302-pack-index.sh
index 9697448..735de10 100755
--- a/t/t5302-pack-index.sh
+++ b/t/t5302-pack-index.sh
@@ -270,12 +270,12 @@
 	thirtyeight=${tag#??} &&
 	rm -f .git/objects/${tag%$thirtyeight}/$thirtyeight &&
 	git index-pack --strict tag-test-${pack1}.pack 2>err &&
-	grep "^warning:.* expected .tagger. line" err
+	test_grep "^warning:.* expected .tagger. line" err
 '
 
 test_expect_success 'index-pack --fsck-objects also warns upon missing tagger in tag' '
 	git index-pack --fsck-objects tag-test-${pack1}.pack 2>err &&
-	grep "^warning:.* expected .tagger. line" err
+	test_grep "^warning:.* expected .tagger. line" err
 '
 
 test_expect_success 'index-pack -v --stdin produces progress for both phases' '
@@ -290,7 +290,7 @@
 	sz="$(test_file_size pack-$pack.pack)" &&
 	test "$sz" -gt 20 &&
 	test_must_fail git index-pack --max-input-size=20 pack-$pack.pack 2>err &&
-	grep "maximum allowed size (20 bytes)" err
+	test_grep "maximum allowed size (20 bytes)" err
 '
 
 # git-index-pack(1) uses the default hash algorithm outside of the repository,
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index 2be7cd3..a4ad545 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -120,7 +120,7 @@
 	git checkout --quiet main &&
 	git reflog expire --all &&
 	git prune -v >prune_actual &&
-	grep "$head_oid" prune_actual
+	test_grep "$head_oid" prune_actual
 '
 
 test_expect_success 'prune: do not prune heads listed as an argument' '
@@ -214,7 +214,7 @@
 	>.git/objects/pack/fake2.keep &&
 	>.git/objects/pack/fake3.idx &&
 	git count-objects -v 2>stderr &&
-	grep "index file .git/objects/pack/fake.idx is too small" stderr &&
+	test_grep "index file .git/objects/pack/fake.idx is too small" stderr &&
 	grep "^warning:" stderr | sort >actual &&
 	cat >expected <<\EOF &&
 warning: garbage found: .git/objects/pack/fake.bar
@@ -252,8 +252,8 @@
 	oid=$(echo hi|git commit-tree HEAD^{tree}) &&
 	echo $oid >.git/shallow &&
 	git prune --dry-run >out &&
-	grep $oid .git/shallow &&
-	grep $oid out &&
+	test_grep $oid .git/shallow &&
+	test_grep $oid out &&
 	git prune &&
 	test_path_is_missing .git/shallow
 '
@@ -365,7 +365,7 @@
 '
 
 test_expect_success 'prune does not crash with -h' '
-	test_expect_code 129 git prune -h >usage &&
+	git prune -h >usage &&
 	test_grep "[Uu]sage: git prune " usage
 '
 
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index e243478..b75a357 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -81,8 +81,8 @@
 			git repack -ad &&
 		ls .git/objects/pack/ | grep bitmap >output &&
 		test_line_count = 1 output &&
-		grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
-		grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace
+		test_grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
+		test_grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace
 	'
 
 	basic_bitmap_tests
@@ -532,8 +532,8 @@
 			test_line_count = 2 bitmaps &&
 
 			GIT_TRACE2_EVENT=$(pwd)/trace2.txt git rev-list --use-bitmap-index HEAD &&
-			grep "opened bitmap" trace2.txt &&
-			grep "ignoring extra bitmap" trace2.txt
+			test_grep "opened bitmap" trace2.txt &&
+			test_grep "ignoring extra bitmap" trace2.txt
 		)
 	'
 
@@ -634,7 +634,7 @@
 	do
 		eval "GIT_TRACE2_EVENT=1 $argv rev-list --objects \
 			--use-bitmap-index second..other 2>perf" &&
-		grep "\"region_enter\".*\"label\":\"haves/boundary\"" perf ||
+		test_grep "\"region_enter\".*\"label\":\"haves/boundary\"" perf ||
 			return 1
 	done &&
 
@@ -646,7 +646,7 @@
 	do
 		eval "GIT_TRACE2_EVENT=1 $argv rev-list --objects \
 			--use-bitmap-index second..other 2>perf" &&
-		grep "\"region_enter\".*\"label\":\"haves/classic\"" perf ||
+		test_grep "\"region_enter\".*\"label\":\"haves/classic\"" perf ||
 			return 1
 	done
 '
@@ -668,7 +668,7 @@
 test_expect_success 'verify writing bitmap lookup table when enabled' '
 	GIT_TRACE2_EVENT="$(pwd)/trace2" \
 		git repack -ad &&
-	grep "\"label\":\"writing_lookup_table\"" trace2
+	test_grep "\"label\":\"writing_lookup_table\"" trace2
 '
 
 test_expect_success 'truncated bitmap fails gracefully (lookup table)' '
diff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh
index dddb79b..2948a7c 100755
--- a/t/t5317-pack-objects-filter-objects.sh
+++ b/t/t5317-pack-objects-filter-objects.sh
@@ -49,7 +49,7 @@
 	git -C r1 index-pack ../filter.pack &&
 
 	git -C r1 verify-pack -v ../filter.pack >verify_result &&
-	! grep blob verify_result
+	test_grep ! blob verify_result
 '
 
 test_expect_success 'verify blob:none packfile without --stdout' '
@@ -57,7 +57,7 @@
 	HEAD
 	EOF
 	git -C r1 verify-pack -v "mypackname-$(cat packhash).pack" >verify_result &&
-	! grep blob verify_result
+	test_grep ! blob verify_result
 '
 
 test_expect_success 'verify normal and blob:none packfiles have same commits/trees' '
@@ -85,7 +85,7 @@
 	test_must_fail git -C r5 pack-objects --revs --stdout 2>bad_tree <<-EOF &&
 	HEAD
 	EOF
-	grep "bad tree object" bad_tree
+	test_grep "bad tree object" bad_tree
 '
 
 test_expect_success 'setup for tests of tree:0' '
@@ -101,7 +101,7 @@
 	EOF
 	git -C r1 index-pack ../commitsonly.pack &&
 	git -C r1 verify-pack -v ../commitsonly.pack >objs &&
-	! grep -E "tree|blob" objs
+	test_grep ! -E "tree|blob" objs
 '
 
 test_expect_success 'grab tree directly when using tree:0' '
@@ -156,7 +156,7 @@
 	git -C r2 index-pack ../filter.pack &&
 
 	git -C r2 verify-pack -v ../filter.pack >verify_result &&
-	! grep blob verify_result
+	test_grep ! blob verify_result
 '
 
 test_expect_success 'verify blob:limit=1000' '
@@ -166,7 +166,7 @@
 	git -C r2 index-pack ../filter.pack &&
 
 	git -C r2 verify-pack -v ../filter.pack >verify_result &&
-	! grep blob verify_result
+	test_grep ! blob verify_result
 '
 
 test_expect_success 'verify blob:limit=1001' '
diff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh
index 1c40f90..6cecfbe 100755
--- a/t/t5318-commit-graph.sh
+++ b/t/t5318-commit-graph.sh
@@ -14,7 +14,7 @@
 
 test_expect_success 'usage shown without sub-command' '
 	test_expect_code 129 git commit-graph 2>err &&
-	grep usage: err
+	test_grep usage: err
 '
 
 test_expect_success 'usage shown with an error on unknown sub-command' '
@@ -679,12 +679,12 @@
 
 test_expect_success 'git fsck shows commit-graph output with --progress' '
 	git -C "$TRASH_DIRECTORY/full" fsck --progress 2>err &&
-	grep "Verifying commits in commit graph" err
+	test_grep "Verifying commits in commit graph" err
 '
 
 test_expect_success 'git fsck suppresses commit-graph output with --no-progress' '
 	git -C "$TRASH_DIRECTORY/full" fsck --no-progress 2>err &&
-	! grep "Verifying commits in commit graph" err
+	test_grep ! "Verifying commits in commit graph" err
 '
 
 test_expect_success 'setup non-the_repository tests' '
@@ -962,7 +962,7 @@
 		git rev-parse HEAD~2 &&
 		# ... but fail when we are paranoid.
 		test_must_fail env GIT_COMMIT_GRAPH_PARANOIA=true git rev-parse HEAD~2 2>error &&
-		grep "error: commit $oid exists in commit-graph but not in the object database" error
+		test_grep "error: commit $oid exists in commit-graph but not in the object database" error
 	)
 '
 
diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh
index fa0d404..68143cb 100755
--- a/t/t5319-multi-pack-index.sh
+++ b/t/t5319-multi-pack-index.sh
@@ -130,7 +130,7 @@
 	test_copy_bytes 1064 <backup-$idx >$objdir/pack/$idx &&
 
 	git -c core.multiPackIndex=true rev-list --objects --all 2>err &&
-	grep "index unavailable" err
+	test_grep "index unavailable" err
 '
 
 test_expect_success 'add more objects' '
@@ -326,7 +326,7 @@
 
 		test_must_fail git multi-pack-index write \
 			--preferred-pack=pack-$empty.pack 2>err &&
-		grep "with no objects" err
+		test_grep "with no objects" err
 	)
 '
 
@@ -548,14 +548,14 @@
 
 test_expect_success 'git fsck shows MIDX output with --progress' '
 	git fsck --progress 2>err &&
-	grep "Verifying OID order in multi-pack-index" err &&
-	grep "Verifying object offsets" err
+	test_grep "Verifying OID order in multi-pack-index" err &&
+	test_grep "Verifying object offsets" err
 '
 
 test_expect_success 'git fsck suppresses MIDX output with --no-progress' '
 	git fsck --no-progress 2>err &&
-	! grep "Verifying OID order in multi-pack-index" err &&
-	! grep "Verifying object offsets" err
+	test_grep ! "Verifying OID order in multi-pack-index" err &&
+	test_grep ! "Verifying object offsets" err
 '
 
 test_expect_success 'corrupt MIDX is not reused' '
@@ -1175,12 +1175,12 @@
 
 test_expect_success 'usage shown without sub-command' '
 	test_expect_code 129 git multi-pack-index 2>err &&
-	! test_grep "unrecognized subcommand" err
+	test_grep ! "unrecognized subcommand" err
 '
 
 test_expect_success 'complains when run outside of a repository' '
 	nongit test_must_fail git multi-pack-index write 2>err &&
-	grep "not a git repository" err
+	test_grep "not a git repository" err
 '
 
 test_expect_success 'repack with delta islands' '
diff --git a/t/t5324-split-commit-graph.sh b/t/t5324-split-commit-graph.sh
index 49a057c..bf7ba0e 100755
--- a/t/t5324-split-commit-graph.sh
+++ b/t/t5324-split-commit-graph.sh
@@ -297,7 +297,7 @@
 		echo "garbage" >$base_file &&
 		test_must_fail git commit-graph verify 2>test_err &&
 		grep -v "^+" test_err >err &&
-		grep "commit-graph file is too small" err
+		test_grep "commit-graph file is too small" err
 	)
 '
 
@@ -310,7 +310,7 @@
 		echo "garbage" >$tip_file &&
 		test_must_fail git commit-graph verify 2>test_err &&
 		grep -v "^+" test_err >err &&
-		grep "commit-graph file is too small" err
+		test_grep "commit-graph file is too small" err
 	)
 '
 
@@ -379,7 +379,7 @@
 		echo "garbage" >$graphdir/commit-graph-chain &&
 		test_must_fail git commit-graph verify 2>test_err &&
 		grep -v "^+" test_err >err &&
-		grep "commit-graph chain file too small" err
+		test_grep "commit-graph chain file too small" err
 	)
 '
 
@@ -410,7 +410,7 @@
 		git -c core.commitGraph=false log >expect.out &&
 		git -c core.commitGraph=true log >out 2>err &&
 		test_cmp expect.out out &&
-		grep "commit-graph base graphs chunk is too small" err
+		test_grep "commit-graph base graphs chunk is too small" err
 	)
 '
 
@@ -421,7 +421,7 @@
 	git commit-graph write --reachable --split &&
 	git commit-graph verify --progress 2>err &&
 	test_line_count = 1 err &&
-	grep "Verifying commits in commit graph: 100% (18/18)" err &&
+	test_grep "Verifying commits in commit graph: 100% (18/18)" err &&
 	test_grep ! warning err &&
 	test_line_count = 3 $graphdir/commit-graph-chain
 '
@@ -718,6 +718,30 @@
 	)
 '
 
+test_expect_success 'incremental write reads topo levels from all layers' '
+	git init topo-from-lower &&
+	(
+		cd topo-from-lower &&
+
+		for i in $(test_seq 5)
+		do
+			test_commit base-$i || return 1
+		done &&
+		git commit-graph write --reachable &&
+
+		test_commit extra &&
+		git commit-graph write --reachable --split=no-merge &&
+
+		git checkout base-3 &&
+		test_commit new-branch &&
+
+		GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
+			git commit-graph write --reachable --split=no-merge &&
+
+		test_trace2_data commit-graph generation-dfs-steps 1 <trace.txt
+	)
+'
+
 test_expect_success 'temporary graph layer is discarded upon failure' '
 	git init layer-discard &&
 	(
diff --git a/t/t5325-reverse-index.sh b/t/t5325-reverse-index.sh
index 285c8b4..5493791 100755
--- a/t/t5325-reverse-index.sh
+++ b/t/t5325-reverse-index.sh
@@ -65,7 +65,7 @@
 
 	test_must_fail git index-pack --rev-index --verify \
 		$packdir/pack-$pack.pack 2>err &&
-	grep "validation error" err
+	test_grep "validation error" err
 '
 
 test_expect_success 'index-pack infers reverse index name with -o' '
diff --git a/t/t5326-multi-pack-bitmaps.sh b/t/t5326-multi-pack-bitmaps.sh
index 62bd973..86beab1 100755
--- a/t/t5326-multi-pack-bitmaps.sh
+++ b/t/t5326-multi-pack-bitmaps.sh
@@ -121,7 +121,7 @@
 			EOF
 
 			test_must_fail git multi-pack-index write --bitmap 2>err &&
-			grep "doesn.t have full closure" err &&
+			test_grep "doesn.t have full closure" err &&
 			test_path_is_missing $midx
 		)
 	'
@@ -215,8 +215,8 @@
 			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
 
 			test-tool bitmap list-commits | sort >bitmaps &&
-			grep "$(git rev-parse one)" bitmaps &&
-			grep "$(git rev-parse two)" bitmaps &&
+			test_grep "$(git rev-parse one)" bitmaps &&
+			test_grep "$(git rev-parse two)" bitmaps &&
 
 			rm -fr $midx-$(midx_checksum $objdir).bitmap &&
 			rm -fr $midx &&
@@ -229,8 +229,8 @@
 			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
 
 			test-tool bitmap list-commits | sort >bitmaps &&
-			grep "$(git rev-parse one)" bitmaps &&
-			! grep "$(git rev-parse two)" bitmaps
+			test_grep "$(git rev-parse one)" bitmaps &&
+			test_grep ! "$(git rev-parse two)" bitmaps
 		)
 	'
 
@@ -258,7 +258,7 @@
 			test_line_count = 1 before &&
 
 			(
-				grep -vf before commits.raw &&
+				grep -vf before commits.raw && # lint-ok: data filter
 				# mark missing commits as preferred
 				sed "s/^/+/" before
 			) >snapshot &&
@@ -321,7 +321,7 @@
 			git multi-pack-index write --bitmap --stdin-packs \
 				<packs 2>err &&
 
-			grep "bitmap without any objects" err &&
+			test_grep "bitmap without any objects" err &&
 
 			test_path_is_file $midx &&
 			test_path_is_missing $midx-$(midx_checksum $objdir).bitmap
@@ -344,7 +344,7 @@
 
 			GIT_TEST_MIDX_READ_RIDX=0 \
 				git rev-list --use-bitmap-index HEAD 2>err &&
-			! grep "ignoring extra bitmap file" err
+			test_grep ! "ignoring extra bitmap file" err
 		)
 	'
 }
@@ -364,7 +364,7 @@
 		git repack -ad &&
 		GIT_TRACE2_EVENT="$(pwd)/trace" \
 			git multi-pack-index write --bitmap &&
-		grep "\"label\":\"writing_lookup_table\"" trace
+		test_grep "\"label\":\"writing_lookup_table\"" trace
 	)
 '
 
@@ -432,7 +432,7 @@
 
 		git rev-parse HEAD >want &&
 		test-tool bitmap list-commits >actual &&
-		grep $(cat want) actual
+		test_grep $(cat want) actual
 	)
 '
 
@@ -488,17 +488,17 @@
 
 	corrupt_file "$packbitmap" &&
 	test_must_fail git fsck 2>err &&
-	grep "bitmap file '\''$packbitmap'\'' has invalid checksum" err &&
+	test_grep "bitmap file '\''$packbitmap'\'' has invalid checksum" err &&
 
 	cp "$packbitmap.bak" "$packbitmap" &&
 	corrupt_file "$midxbitmap" &&
 	test_must_fail git fsck 2>err &&
-	grep "bitmap file '\''$midxbitmap'\'' has invalid checksum" err &&
+	test_grep "bitmap file '\''$midxbitmap'\'' has invalid checksum" err &&
 
 	corrupt_file "$packbitmap" &&
 	test_must_fail git fsck 2>err &&
-	grep "bitmap file '\''$midxbitmap'\'' has invalid checksum" err &&
-	grep "bitmap file '\''$packbitmap'\'' has invalid checksum" err
+	test_grep "bitmap file '\''$midxbitmap'\'' has invalid checksum" err &&
+	test_grep "bitmap file '\''$packbitmap'\'' has invalid checksum" err
 '
 
 test_expect_success 'corrupt MIDX with bitmap causes fallback' '
diff --git a/t/t5328-commit-graph-64bit-time.sh b/t/t5328-commit-graph-64bit-time.sh
index bc651b6..0919417 100755
--- a/t/t5328-commit-graph-64bit-time.sh
+++ b/t/t5328-commit-graph-64bit-time.sh
@@ -89,7 +89,7 @@
 	git commit-graph write --reachable &&
 	corrupt_chunk_file $graph GDO2 clear &&
 	test_must_fail git log 2>err &&
-	grep "commit-graph overflow generation data is too small" err
+	test_grep "commit-graph overflow generation data is too small" err
 '
 
 test_done
diff --git a/t/t5329-pack-objects-cruft.sh b/t/t5329-pack-objects-cruft.sh
index 25ddda5..12cda06 100755
--- a/t/t5329-pack-objects-cruft.sh
+++ b/t/t5329-pack-objects-cruft.sh
@@ -468,8 +468,8 @@
 
 		test-tool pack-mtimes "$(basename $(ls $packdir/pack-*.mtimes))" \
 		       >objects &&
-		! grep $object objects &&
-		grep $cruft objects
+		test_grep ! $object objects &&
+		test_grep $cruft objects
 	)
 '
 
@@ -515,7 +515,7 @@
 
 		test_path_is_missing "$loose" &&
 		test-tool pack-mtimes "$(basename "$(ls $packdir/pack-*.mtimes)")" >cruft &&
-		grep "$blob" cruft &&
+		test_grep "$blob" cruft &&
 
 		# write the same object again
 		git hash-object -w -t blob contents &&
@@ -657,7 +657,7 @@
 		# ensure that a dirty exit halts cruft pack generation
 		git config --add gc.recentObjectsHook ./extra-tips.c &&
 		test_must_fail git repack --cruft --cruft-expiration=now -d 2>err &&
-		grep "unable to enumerate additional recent objects" err &&
+		test_grep "unable to enumerate additional recent objects" err &&
 
 		# and that the existing cruft pack is left alone
 		test_path_is_file "$mtimes"
diff --git a/t/t5334-incremental-multi-pack-index.sh b/t/t5334-incremental-multi-pack-index.sh
index 68a103d..f0b82b5 100755
--- a/t/t5334-incremental-multi-pack-index.sh
+++ b/t/t5334-incremental-multi-pack-index.sh
@@ -29,7 +29,7 @@
 	test_path_is_missing $packdir/multi-pack-index &&
 	test_path_is_file $midx_chain &&
 	test_line_count = 2 $midx_chain &&
-	grep $old_hash $midx_chain
+	test_grep $old_hash $midx_chain
 '
 
 compare_results_with_midx 'incremental MIDX'
diff --git a/t/t5335-compact-multi-pack-index.sh b/t/t5335-compact-multi-pack-index.sh
index ec1dafe..0f75ef2 100755
--- a/t/t5335-compact-multi-pack-index.sh
+++ b/t/t5335-compact-multi-pack-index.sh
@@ -321,8 +321,8 @@
 		} >$midx_chain &&
 
 		test-tool read-midx $objdir $layer >midx.data &&
-		grep "^pack-B-.*\.idx" midx.data &&
-		grep "^pack-C-.*\.idx" midx.data
+		test_grep "^pack-B-.*\.idx" midx.data &&
+		test_grep "^pack-C-.*\.idx" midx.data
 
 	)
 '
diff --git a/t/t5351-unpack-large-objects.sh b/t/t5351-unpack-large-objects.sh
index d76eb4b..7f621c5 100755
--- a/t/t5351-unpack-large-objects.sh
+++ b/t/t5351-unpack-large-objects.sh
@@ -32,7 +32,7 @@
 test_expect_success 'unpack-objects failed under memory limitation' '
 	prepare_dest 2m &&
 	test_must_fail git -C dest.git unpack-objects <pack-$PACK.pack 2>err &&
-	grep "fatal: attempting to allocate" err
+	test_grep "fatal: attempting to allocate" err
 '
 
 test_expect_success 'unpack-objects works with memory limitation in dry-run mode' '
diff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh
index b32a0a6..6aa5838 100755
--- a/t/t5400-send-pack.sh
+++ b/t/t5400-send-pack.sh
@@ -56,9 +56,9 @@
 	git log'
 
 test_expect_success 'send-pack does not crash with -h' '
-	test_expect_code 129 git send-pack -h >usage &&
+	git send-pack -h >usage &&
 	test_grep "[Uu]sage: git send-pack " usage &&
-	test_expect_code 129 nongit git send-pack -h >usage &&
+	nongit git send-pack -h >usage &&
 	test_grep "[Uu]sage: git send-pack " usage
 '
 
diff --git a/t/t5402-post-merge-hook.sh b/t/t5402-post-merge-hook.sh
index 915af2d..c77aa56 100755
--- a/t/t5402-post-merge-hook.sh
+++ b/t/t5402-post-merge-hook.sh
@@ -46,7 +46,7 @@
 '
 
 test_expect_success 'post-merge from normal merge receives the right argument ' '
-	grep 0 clone1/.git/post-merge.args
+	test_grep 0 clone1/.git/post-merge.args
 '
 
 test_expect_success 'post-merge from squash merge runs as expected ' '
@@ -55,7 +55,7 @@
 '
 
 test_expect_success 'post-merge from squash merge receives the right argument ' '
-	grep 1 clone2/.git/post-merge.args
+	test_grep 1 clone2/.git/post-merge.args
 '
 
 test_done
diff --git a/t/t5403-post-checkout-hook.sh b/t/t5403-post-checkout-hook.sh
index cb0300b..39009ce 100755
--- a/t/t5403-post-checkout-hook.sh
+++ b/t/t5403-post-checkout-hook.sh
@@ -100,7 +100,7 @@
 		echo untracked >three.t &&
 		test_when_finished "rm three.t" &&
 		test_must_fail git rebase $args HEAD rebase-fast-forward 2>err &&
-		grep "untracked working tree files would be overwritten by checkout" err &&
+		test_grep "untracked working tree files would be overwritten by checkout" err &&
 		test_path_is_missing .git/post-checkout.args
 
 '
diff --git a/t/t5404-tracking-branches.sh b/t/t5404-tracking-branches.sh
index cc07889..a1661d3 100755
--- a/t/t5404-tracking-branches.sh
+++ b/t/t5404-tracking-branches.sh
@@ -59,7 +59,7 @@
 test_expect_success 'already deleted tracking branches ignored' '
 	git branch -d -r origin/b3 &&
 	git push origin :b3 >output 2>&1 &&
-	! grep "^error: " output
+	test_grep ! "^error: " output
 '
 
 test_done
diff --git a/t/t5406-remote-rejects.sh b/t/t5406-remote-rejects.sh
index dcbeb42..bb293b7 100755
--- a/t/t5406-remote-rejects.sh
+++ b/t/t5406-remote-rejects.sh
@@ -19,6 +19,6 @@
 
 test_expect_success 'push reports error' 'test_must_fail git push 2>stderr'
 
-test_expect_success 'individual ref reports error' 'grep rejected stderr'
+test_expect_success 'individual ref reports error' 'test_grep rejected stderr'
 
 test_done
diff --git a/t/t5407-post-rewrite-hook.sh b/t/t5407-post-rewrite-hook.sh
index ad7f8c6..ca8a10f 100755
--- a/t/t5407-post-rewrite-hook.sh
+++ b/t/t5407-post-rewrite-hook.sh
@@ -195,19 +195,19 @@
 		set_replace_editor todo &&
 		test_must_fail git rebase -i D D 2>err
 	) &&
-	grep "would be overwritten" err &&
+	test_grep "would be overwritten" err &&
 	rm bar &&
 
 	test_must_fail git rebase --continue 2>err &&
-	grep "would be overwritten" err &&
+	test_grep "would be overwritten" err &&
 	rm G &&
 
 	test_must_fail git rebase --continue 2>err &&
-	grep "would be overwritten" err &&
+	test_grep "would be overwritten" err &&
 	rm H &&
 
 	test_must_fail git rebase --continue 2>err &&
-	grep "would be overwritten" err &&
+	test_grep "would be overwritten" err &&
 	rm I &&
 
 	git rebase --continue &&
@@ -310,4 +310,27 @@
 	verify_hook_input
 '
 
+test_expect_success 'rebase with commits that become empty' '
+	cat >todo <<-\EOF &&
+	pick H
+	pick E
+	fixup I
+	fixup H
+	pick G
+	pick I
+	EOF
+	(
+		set_replace_editor todo &&
+		git rebase -i --empty=drop A A
+	) &&
+	echo rebase >expected.args &&
+	cat >expected.data <<-EOF &&
+	$(git rev-parse H) $(git rev-parse HEAD~2)
+	$(git rev-parse E) $(git rev-parse HEAD~1)
+	$(git rev-parse I) $(git rev-parse HEAD~1)
+	$(git rev-parse G) $(git rev-parse HEAD)
+	EOF
+	verify_hook_input
+'
+
 test_done
diff --git a/t/t5409-colorize-remote-messages.sh b/t/t5409-colorize-remote-messages.sh
index 3010913..5da8b0a 100755
--- a/t/t5409-colorize-remote-messages.sh
+++ b/t/t5409-colorize-remote-messages.sh
@@ -32,70 +32,70 @@
 test_expect_success 'keywords' '
 	git --git-dir child/.git -c color.remote=always push -f origin HEAD:refs/heads/keywords 2>output &&
 	test_decode_color <output >decoded &&
-	grep "<BOLD;RED>error<RESET>: error" decoded &&
-	grep "<YELLOW>hint<RESET>:" decoded &&
-	grep "<BOLD;GREEN>success<RESET>:" decoded &&
-	grep "<BOLD;GREEN>SUCCESS<RESET>" decoded &&
-	grep "<BOLD;YELLOW>warning<RESET>:" decoded
+	test_grep "<BOLD;RED>error<RESET>: error" decoded &&
+	test_grep "<YELLOW>hint<RESET>:" decoded &&
+	test_grep "<BOLD;GREEN>success<RESET>:" decoded &&
+	test_grep "<BOLD;GREEN>SUCCESS<RESET>" decoded &&
+	test_grep "<BOLD;YELLOW>warning<RESET>:" decoded
 '
 
 test_expect_success 'whole words at line start' '
 	git --git-dir child/.git -c color.remote=always push -f origin HEAD:refs/heads/whole-words 2>output &&
 	test_decode_color <output >decoded &&
-	grep "<YELLOW>hint<RESET>:" decoded &&
-	grep "hinting: not highlighted" decoded &&
-	grep "prefixerror: error" decoded
+	test_grep "<YELLOW>hint<RESET>:" decoded &&
+	test_grep "hinting: not highlighted" decoded &&
+	test_grep "prefixerror: error" decoded
 '
 
 test_expect_success 'short line' '
 	git -C child -c color.remote=always push -f origin HEAD:short-line 2>output &&
 	test_decode_color <output >decoded &&
-	grep "remote: Err" decoded
+	test_grep "remote: Err" decoded
 '
 
 test_expect_success 'case-insensitive' '
 	git --git-dir child/.git -c color.remote=always push -f origin HEAD:refs/heads/case-insensitive 2>output &&
 	test_decode_color <output >decoded &&
-	grep "<BOLD;RED>error<RESET>: error" decoded &&
-	grep "<BOLD;RED>ERROR<RESET>: also highlighted" decoded
+	test_grep "<BOLD;RED>error<RESET>: error" decoded &&
+	test_grep "<BOLD;RED>ERROR<RESET>: also highlighted" decoded
 '
 
 test_expect_success 'leading space' '
 	git --git-dir child/.git -c color.remote=always push -f origin HEAD:refs/heads/leading-space 2>output &&
 	test_decode_color <output >decoded &&
-	grep "  <BOLD;RED>error<RESET>: leading space" decoded
+	test_grep "  <BOLD;RED>error<RESET>: leading space" decoded
 '
 
 test_expect_success 'spaces only' '
 	git -C child -c color.remote=always push -f origin HEAD:only-space 2>output &&
 	test_decode_color <output >decoded &&
-	grep "remote:     " decoded
+	test_grep "remote:     " decoded
 '
 
 test_expect_success 'no coloring for redirected output' '
 	git --git-dir child/.git push -f origin HEAD:refs/heads/redirected-output 2>output &&
 	test_decode_color <output >decoded &&
-	grep "error: error" decoded
+	test_grep "error: error" decoded
 '
 
 test_expect_success 'push with customized color' '
 	git --git-dir child/.git -c color.remote=always -c color.remote.error=blue push -f origin HEAD:refs/heads/customized-color 2>output &&
 	test_decode_color <output >decoded &&
-	grep "<BLUE>error<RESET>:" decoded &&
-	grep "<BOLD;GREEN>success<RESET>:" decoded
+	test_grep "<BLUE>error<RESET>:" decoded &&
+	test_grep "<BOLD;GREEN>success<RESET>:" decoded
 '
 
 
 test_expect_success 'error in customized color' '
 	git --git-dir child/.git -c color.remote=always -c color.remote.error=i-am-not-a-color push -f origin HEAD:refs/heads/error-customized-color 2>output &&
 	test_decode_color <output >decoded &&
-	grep "<BOLD;GREEN>success<RESET>:" decoded
+	test_grep "<BOLD;GREEN>success<RESET>:" decoded
 '
 
 test_expect_success 'fallback to color.ui' '
 	git --git-dir child/.git -c color.ui=always push -f origin HEAD:refs/heads/fallback-color-ui 2>output &&
 	test_decode_color <output >decoded &&
-	grep "<BOLD;RED>error<RESET>: error" decoded
+	test_grep "<BOLD;RED>error<RESET>: error" decoded
 '
 
 test_expect_success 'disallow (color) control sequences in sideband' '
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index 649a615..6c1edf2 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -135,15 +135,15 @@
 	GIT_TRACE2_EVENT="$(pwd)/branch-a/trace2_event" \
 		git clone --single-branch --branch A "file://$(pwd)/." branch-a &&
 	test_must_fail git --git-dir=branch-a/.git rev-parse origin/B &&
-	grep \"fetch-info\".*\"haves\":0 branch-a/trace2_event &&
-	grep \"fetch-info\".*\"wants\":1 branch-a/trace2_event
+	test_grep \"fetch-info\".*\"haves\":0 branch-a/trace2_event &&
+	test_grep \"fetch-info\".*\"wants\":1 branch-a/trace2_event
 '
 
 test_expect_success 'clone shallow depth 1' '
 	GIT_TRACE2_EVENT="$(pwd)/shallow0/trace2_event" \
 		git clone --no-single-branch --depth 1 "file://$(pwd)/." shallow0 &&
 	test "$(git --git-dir=shallow0/.git rev-list --count HEAD)" = 1 &&
-	grep \"fetch-info\".*\"depth\":1 shallow0/trace2_event
+	test_grep \"fetch-info\".*\"depth\":1 shallow0/trace2_event
 '
 
 test_expect_success 'clone shallow depth 1 with fsck' '
@@ -167,7 +167,7 @@
 		cd shallow &&
 		git count-objects -v
 	) > count.shallow &&
-	grep "^in-pack: 12" count.shallow
+	test_grep "^in-pack: 12" count.shallow
 '
 
 test_expect_success 'clone shallow object count (part 2)' '
@@ -230,7 +230,7 @@
 		cd shallow &&
 		git count-objects -v
 	) > count.shallow &&
-	grep "^count: 6" count.shallow
+	test_grep "^count: 6" count.shallow
 '
 
 test_expect_success 'add two more (part 2)' '
@@ -243,8 +243,8 @@
 		cd shallow &&
 		GIT_TRACE2_EVENT="$(pwd)/trace2_event" \
 			git pull --depth 4 .. B &&
-		grep \"fetch-info\".*\"depth\":4 trace2_event &&
-		grep \"fetch-info\".*\"shallows\":2 trace2_event
+		test_grep \"fetch-info\".*\"depth\":4 trace2_event &&
+		test_grep \"fetch-info\".*\"shallows\":2 trace2_event
 	)
 '
 
@@ -253,7 +253,7 @@
 		cd shallow &&
 		git count-objects -v
 	) > count.shallow &&
-	grep "^count: 12" count.shallow
+	test_grep "^count: 12" count.shallow
 '
 
 test_expect_success 'deepening fetch in shallow repo' '
@@ -268,7 +268,7 @@
 		cd shallow &&
 		git count-objects -v
 	) > count.shallow &&
-	grep "^count: 18" count.shallow
+	test_grep "^count: 18" count.shallow
 '
 
 test_expect_success 'pull in shallow repo with missing merge base' '
@@ -298,7 +298,7 @@
 		git prune &&
 		git count-objects -v
 	) > count.shallow &&
-	grep "^count: 54" count.shallow
+	test_grep "^count: 54" count.shallow
 '
 
 test_expect_success 'fetch --no-shallow on full repo' '
@@ -319,8 +319,8 @@
 			git fetch --unshallow &&
 		! test -f .git/shallow &&
 		git fsck --full &&
-		grep \"fetch-info\".*\"shallows\":2 trace2_event &&
-		grep \"fetch-info\".*\"depth\":2147483647 trace2_event
+		test_grep \"fetch-info\".*\"shallows\":2 trace2_event &&
+		test_grep \"fetch-info\".*\"depth\":2147483647 trace2_event
 	)
 '
 
@@ -333,7 +333,7 @@
 		cd shallow2 &&
 		git count-objects -v
 	) > count.shallow2 &&
-	grep "^in-pack: 3" count.shallow2
+	test_grep "^in-pack: 3" count.shallow2
 '
 
 test_expect_success 'clone shallow with --branch' '
@@ -446,7 +446,7 @@
 	# and should reset in_vain. This allows negotiation to continue until
 	# the client reports that first_anotherbranch_commit is common.
 	GIT_TRACE2_EVENT="$(pwd)/trace2" git -C myclient fetch --progress origin main 2>log &&
-	grep \"key\":\"total_rounds\",\"value\":\"6\" trace2 &&
+	test_grep \"key\":\"total_rounds\",\"value\":\"6\" trace2 &&
 	test_grep "Total 3 " log
 '
 
@@ -852,7 +852,7 @@
 	two
 	EOF
 	test_cmp expected actual &&
-	grep \"fetch-info\".*\"deepen-since\":true shallow11/trace2_event
+	test_grep \"fetch-info\".*\"deepen-since\":true shallow11/trace2_event
 '
 
 test_expect_success 'clone shallow since selects no commits' '
@@ -935,7 +935,7 @@
 	test_when_finished rm -f rev err &&
 	git -C shallow-exclude rev-parse one >rev &&
 	test_must_fail git -C shallow12 fetch --shallow-exclude $(cat rev) origin 2>err &&
-	grep "deepen-not is not a ref:" err
+	test_grep "deepen-not is not a ref:" err
 '
 
 test_expect_success 'fetching deepen' '
@@ -1059,9 +1059,9 @@
 	commit=$(git -C server rev-parse HEAD) &&
 	blob=$(git hash-object server/one.t) &&
 	git -C client rev-list --objects --missing=allow-any "$commit" >oids &&
-	! grep "$blob" oids &&
+	test_grep ! "$blob" oids &&
 
-	grep \"fetch-info\".*\"filter\":\"blob:limit\" client/trace2_event
+	test_grep \"fetch-info\".*\"filter\":\"blob:limit\" client/trace2_event
 '
 
 test_expect_success 'filtering by size has no effect if support for it is not advertised' '
@@ -1076,7 +1076,7 @@
 	commit=$(git -C server rev-parse HEAD) &&
 	blob=$(git hash-object server/one.t) &&
 	git -C client rev-list --objects --missing=allow-any "$commit" >oids &&
-	grep "$blob" oids &&
+	test_grep "$blob" oids &&
 
 	test_grep "filtering not recognized by server" err
 '
diff --git a/t/t5504-fetch-receive-strict.sh b/t/t5504-fetch-receive-strict.sh
index 438250c..75b2b87 100755
--- a/t/t5504-fetch-receive-strict.sh
+++ b/t/t5504-fetch-receive-strict.sh
@@ -298,13 +298,13 @@
 	git --git-dir=dst/.git config \
 		receive.fsck.missingEmail warn &&
 	git push --porcelain dst bogus >act 2>&1 &&
-	grep "missingEmail" act &&
+	test_grep "missingEmail" act &&
 	test_grep "skipping unknown msg id.*whatever" act &&
 	git --git-dir=dst/.git branch -D bogus &&
 	git --git-dir=dst/.git config --add \
 		receive.fsck.missingEmail ignore &&
 	git push --porcelain dst bogus >act 2>&1 &&
-	! grep "missingEmail" act
+	test_grep ! "missingEmail" act
 '
 
 test_expect_success 'fetch with fetch.fsck.missingEmail=warn' '
@@ -326,7 +326,7 @@
 	git --git-dir=dst/.git config \
 		fetch.fsck.missingEmail warn &&
 	git --git-dir=dst/.git fetch "file://$(pwd)" $refspec >act 2>&1 &&
-	grep "missingEmail" act &&
+	test_grep "missingEmail" act &&
 	test_grep "Skipping unknown msg id.*whatever" act &&
 	rm -rf dst &&
 	git init dst &&
@@ -334,7 +334,7 @@
 	git --git-dir=dst/.git config \
 		fetch.fsck.missingEmail ignore &&
 	git --git-dir=dst/.git fetch "file://$(pwd)" $refspec >act 2>&1 &&
-	! grep "missingEmail" act
+	test_grep ! "missingEmail" act
 '
 
 test_expect_success \
@@ -345,7 +345,7 @@
 	git --git-dir=dst/.git config \
 		receive.fsck.unterminatedheader warn &&
 	test_must_fail git push --porcelain dst HEAD >act 2>&1 &&
-	grep "Cannot demote unterminatedheader" act
+	test_grep "Cannot demote unterminatedheader" act
 '
 
 test_expect_success \
@@ -356,7 +356,7 @@
 	git --git-dir=dst/.git config \
 		fetch.fsck.unterminatedheader warn &&
 	test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" HEAD &&
-	grep "Cannot demote unterminatedheader" act
+	test_grep "Cannot demote unterminatedheader" act
 '
 
 test_expect_success PERL_TEST_HELPERS 'badFilemode is not a strict error' '
@@ -373,7 +373,7 @@
 	git -C dst.git config transfer.fsckObjects true &&
 
 	git -C badmode.git push ../dst.git $tree:refs/tags/tree 2>err &&
-	grep "$tree: badFilemode" err
+	test_grep "$tree: badFilemode" err
 '
 
 test_done
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index e592c0b..5fbfcb0 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -24,6 +24,28 @@
 	)
 }
 
+setup_url_pushremote () {
+	rm -rf fork.git client &&
+	git clone --bare one fork.git &&
+	git clone one client &&
+	fork_url="file://$TRASH_DIRECTORY/fork.git" &&
+	(
+		cd client &&
+		git checkout -b topic --track origin/main &&
+		git commit --allow-empty -m topic-change &&
+		git config push.default current &&
+		git config status.compareBranches "@{upstream} @{push}" &&
+		git config branch.topic.pushRemote "$fork_url" &&
+		git push
+	)
+}
+
+check_status () {
+	git -C client status >actual &&
+	cat >expected &&
+	test_cmp expected actual
+}
+
 tokens_match () {
 	echo "$1" | tr ' ' '\012' | sort | sed -e '/^$/d' >expect &&
 	echo "$2" | tr ' ' '\012' | sort | sed -e '/^$/d' >actual &&
@@ -91,28 +113,28 @@
 	test_when_finished "rm -rf pc" &&
 	git clone --filter=blob:none "file://$(pwd)/srv.bare" pc &&
 	git -C pc remote -v >out &&
-	grep "srv.bare (fetch) \[blob:none\]" out &&
+	test_grep "srv.bare (fetch) \[blob:none\]" out &&
 
 	git -C pc config remote.origin.partialCloneFilter object:type=commit &&
 	git -C pc remote -v >out &&
-	grep "srv.bare (fetch) \[object:type=commit\]" out
+	test_grep "srv.bare (fetch) \[object:type=commit\]" out
 '
 
 test_expect_success 'filters should not be listed for non promisor remotes (remote -v)' '
 	test_when_finished "rm -rf pc" &&
 	git clone one pc &&
 	git -C pc remote -v >out &&
-	! grep "(fetch) \[.*\]" out
+	test_grep ! "(fetch) \[.*\]" out
 '
 
 test_expect_success 'filters are listed by git remote -v only' '
 	test_when_finished "rm -rf pc" &&
 	git clone --filter=blob:none "file://$(pwd)/srv.bare" pc &&
 	git -C pc remote >out &&
-	! grep "\[blob:none\]" out &&
+	test_grep ! "\[blob:none\]" out &&
 
 	git -C pc remote show >out &&
-	! grep "\[blob:none\]" out
+	test_grep ! "\[blob:none\]" out
 '
 
 test_expect_success 'check remote-tracking' '
@@ -261,7 +283,7 @@
 
 test_expect_success 'without subcommand does not take arguments' '
 	test_expect_code 129 git -C test remote origin 2>err &&
-	grep "^error: unknown subcommand:" err
+	test_grep "^error: unknown subcommand:" err
 '
 
 cat >test/expect <<EOF
@@ -798,7 +820,7 @@
 		cd add-no-tags &&
 		git init &&
 		git remote add -f --no-tags origin ../one &&
-		grep tagOpt .git/config &&
+		test_grep tagOpt .git/config &&
 		git tag -l some-tag >../test/output &&
 		git tag -l foobar-tag >../test/output &&
 		git config remote.origin.tagopt >>../test/output
@@ -972,7 +994,7 @@
 		GIT_TRACE2_EVENT=$(pwd)/trace \
 			git remote rename --progress origin upstream &&
 		test_region progress "Renaming remote references" trace &&
-		grep "pushRemote" .git/config &&
+		test_grep "pushRemote" .git/config &&
 		test -z "$(git for-each-ref refs/remotes/origin)" &&
 		test "$(git symbolic-ref refs/remotes/upstream/HEAD)" = "refs/remotes/upstream/main" &&
 		test "$(git rev-parse upstream/main)" = "$(git rev-parse main)" &&
@@ -989,7 +1011,7 @@
 		cd four.1 &&
 		git config remote.pushDefault origin &&
 		git remote rename origin upstream &&
-		grep pushDefault .git/config &&
+		test_grep pushDefault .git/config &&
 		test "$(git config --local remote.pushDefault)" = "upstream"
 	)
 '
@@ -1018,6 +1040,128 @@
 	)
 '
 
+test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '
+	setup_url_pushremote &&
+
+	check_status <<-EOF
+	On branch topic
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+'
+
+test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' '
+	setup_url_pushremote &&
+
+	(
+		cd client &&
+		git remote rename origin upstream &&
+		git remote add -f origin "$fork_url"
+	) &&
+
+	check_status <<-EOF
+	On branch topic
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+	Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+	nothing to commit, working tree clean
+	EOF
+'
+
+test_expect_success 'configured pushurl makes URL-valued pushRemote trackable' '
+	setup_url_pushremote &&
+
+	(
+		cd client &&
+		git remote rename origin upstream &&
+		git remote add -f origin ../fork.git &&
+		git remote set-url --push origin "$fork_url"
+	) &&
+
+	check_status <<-EOF
+	On branch topic
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+	Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+	nothing to commit, working tree clean
+	EOF
+'
+
+test_expect_success 'pushInsteadOf URL pushRemote is trackable' '
+	setup_url_pushremote &&
+	(
+		cd client &&
+		git remote rename origin upstream &&
+		git remote add -f origin "$fork_url" &&
+		git config "url.$fork_url.pushInsteadOf" fork: &&
+		git config branch.topic.pushRemote fork:
+	) &&
+
+	check_status <<-EOF
+	On branch topic
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+	Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+	nothing to commit, working tree clean
+	EOF
+'
+
+test_expect_success 'up-to-date URL push refreshes stale tracking branch' '
+	setup_url_pushremote &&
+	(
+		cd client &&
+		git remote rename origin upstream &&
+		git remote add -f origin "$fork_url" &&
+		git commit --allow-empty -m another-topic-change &&
+		git -C ../fork.git fetch ../client topic:topic
+	) &&
+
+	check_status <<-EOF &&
+	On branch topic
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.
+
+	Your branch is ahead of ${SQ}origin/topic${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+
+	git -C client push >actual 2>&1 &&
+	test_grep "Everything up-to-date" actual &&
+
+	check_status <<-EOF
+	On branch topic
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.
+
+	Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+	nothing to commit, working tree clean
+	EOF
+'
+
+test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '
+	setup_url_pushremote &&
+	(
+		cd client &&
+		git remote rename origin upstream &&
+		git remote add -f origin "$fork_url" &&
+		git remote add duplicate "$fork_url"
+	) &&
+
+	check_status <<-EOF
+	On branch topic
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+'
+
 test_expect_success 'rename handles remote without fetch refspec' '
 	git clone --bare one no-refspec.git &&
 	# confirm assumption that bare clone does not create refspec
@@ -1198,7 +1342,7 @@
 		cd eight &&
 		git branch -a
 	) 2>err &&
-	! grep "points nowhere" err &&
+	test_grep ! "points nowhere" err &&
 	(
 		cd eight &&
 		test_must_fail git branch nomore origin
@@ -1327,7 +1471,8 @@
 	test_when_finished "rm -f .git/config.lock" &&
 	git config --get-all remote.someremote.url >expect &&
 	>.git/config.lock &&
-	test_must_fail git remote set-url someremote baz &&
+	test_must_fail git -c core.configLockTimeout=0 \
+		remote set-url someremote baz &&
 	git config --get-all remote.someremote.url >actual &&
 	cmp expect actual
 '
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 6f0ae1b..a8d38d9 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -856,7 +856,7 @@
 # the strange name is: a\!'b
 test_expect_success 'quoting of a strangely named repo' '
 	test_must_fail git fetch "a\\!'\''b" > result 2>&1 &&
-	grep "fatal: '\''a\\\\!'\''b'\''" result
+	test_grep "fatal: '\''a\\\\!'\''b'\''" result
 '
 
 test_expect_success 'bundle should record HEAD correctly' '
@@ -1010,14 +1010,14 @@
 	rm -f .git/FETCH_HEAD err &&
 	git fetch --dry-run . 2>err &&
 	! test -f .git/FETCH_HEAD &&
-	grep FETCH_HEAD err
+	test_grep FETCH_HEAD err
 '
 
 test_expect_success '--no-write-fetch-head does not touch FETCH_HEAD, and does not print what would be written' '
 	rm -f .git/FETCH_HEAD err &&
 	git fetch --no-write-fetch-head . 2>err &&
 	! test -f .git/FETCH_HEAD &&
-	! grep FETCH_HEAD err
+	test_grep ! FETCH_HEAD err
 '
 
 test_expect_success '--write-fetch-head gets defeated by --dry-run' '
@@ -1435,7 +1435,7 @@
 		git config maintenance.strategy gc &&
 		GIT_ASK_YESNO="$TRASH_DIRECTORY/askyesno" git fetch --verbose >fetch.out 2>&1 &&
 		test_grep "Auto packing the repository" fetch.out &&
-		! grep "Should I try again" fetch.out
+		test_grep ! "Should I try again" fetch.out
 	)
 '
 
@@ -1444,7 +1444,7 @@
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
-		grep "git rev-list .*--exclude-hidden=fetch" trace
+		test_grep "git rev-list .*--exclude-hidden=fetch" trace
 	'
 done
 
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 5930f55..2e1d4f7 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -86,7 +86,7 @@
 '
 
 test_expect_success 'ls-remote --heads is deprecated and hidden w/o warning' '
-	test_expect_code 129 git ls-remote -h >short-help &&
+	git ls-remote -h >short-help &&
 	test_grep ! -e --head short-help &&
 	git ls-remote --heads self >actual 2>warning &&
 	test_cmp expected.branches actual &&
@@ -245,8 +245,8 @@
 		git config --add $configsection.hiderefs "!refs/tags/magic" &&
 		git config --add $configsection.hiderefs refs/tags/magic/one &&
 		git ls-remote . >actual &&
-		grep refs/tags/magic/two actual &&
-		! grep refs/tags/magic/one actual
+		test_grep refs/tags/magic/two actual &&
+		test_grep ! refs/tags/magic/one actual
 	'
 
 done
@@ -255,13 +255,13 @@
 	test_config uploadpack.hiderefs refs/tags &&
 	test_config transfer.hiderefs "!refs/tags/magic" &&
 	git ls-remote . >actual &&
-	grep refs/tags/magic actual
+	test_grep refs/tags/magic actual
 '
 
 test_expect_success 'protocol v2 supports hiderefs' '
 	test_config uploadpack.hiderefs refs/tags &&
 	git -c protocol.version=2 ls-remote . >actual &&
-	! grep refs/tags actual
+	test_grep ! refs/tags actual
 '
 
 test_expect_success 'ls-remote --symref' '
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 523aff6..5d79472 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -217,7 +217,7 @@
 
 	test_must_fail env GIT_TRACE="$PWD/trace" \
 		git fetch --jobs=2 --multiple one two 2>err &&
-	grep "preparing to run up to 2 tasks" trace &&
+	test_grep "preparing to run up to 2 tasks" trace &&
 	test_grep "could not fetch .one.*128" err &&
 	test_grep "could not fetch .two.*128" err
 '
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 1b98634..f3b3efc 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -124,7 +124,7 @@
 	do
 		test_expect_success "reject 'git $cmd --no-$opt'" '
 			test_must_fail git $cmd --no-$opt 2>err &&
-			grep "unknown option .no-$opt" err
+			test_grep "unknown option .no-$opt" err
 		'
 	done
 done
@@ -212,7 +212,7 @@
 	GIT_TRACE2_EVENT="$(pwd)/event" \
 		git -c protocol.version=2 -c push.negotiate=1 \
 		push testrepo refs/heads/main:refs/remotes/origin/main &&
-	grep \"key\":\"total_rounds\",\"value\":\"1\" event &&
+	test_grep \"key\":\"total_rounds\",\"value\":\"1\" event &&
 	grep_wrote 2 event # 1 commit, 1 tree
 '
 
@@ -250,8 +250,8 @@
 	GIT_TRACE2_EVENT="$(pwd)/event"  git -c submodule.recurse=true \
 		-c protocol.version=2 -c push.negotiate=1 \
 		push testrepo refs/heads/main:refs/remotes/origin/main 2>err &&
-	grep \"key\":\"total_rounds\",\"value\":\"1\" event &&
-	! grep "Fetching submodule" err
+	test_grep \"key\":\"total_rounds\",\"value\":\"1\" event &&
+	test_grep ! "Fetching submodule" err
 '
 
 test_expect_success 'push with negotiation and remote.<name>.negotiationInclude' '
@@ -637,7 +637,7 @@
 	test_config branch.main.remote one &&
 	test_config branch.main.merge refs/heads/main &&
 	test_must_fail git push 2>err &&
-	grep "bad config variable .branch\.\." err
+	test_grep "bad config variable .branch\.\." err
 '
 
 test_expect_success 'push ignores "branch." config without subsection' '
@@ -923,7 +923,7 @@
 		git config receive.denyCurrentBranch warn
 	) &&
 	git push testrepo main 2>stderr &&
-	grep "warning: updating the current branch" stderr
+	test_grep "warning: updating the current branch" stderr
 '
 
 test_expect_success 'deny push to HEAD of non-bare repository' '
@@ -945,7 +945,7 @@
 		git config core.bare true
 	) &&
 	git push testrepo main 2>stderr &&
-	! grep "warning: updating the current branch" stderr
+	test_grep ! "warning: updating the current branch" stderr
 '
 
 test_expect_success 'allow push to HEAD of non-bare repository (config)' '
@@ -956,7 +956,7 @@
 		git config receive.denyCurrentBranch false
 	) &&
 	git push testrepo main 2>stderr &&
-	! grep "warning: updating the current branch" stderr
+	test_grep ! "warning: updating the current branch" stderr
 '
 
 test_expect_success !WITH_BREAKING_CHANGES 'fetch with branches' '
@@ -1068,7 +1068,7 @@
 		git commit -a -m child2 &&
 		git branch bar &&
 		test_must_fail git push ../child1 foo bar 2>stderr &&
-		grep "refusing inconsistent update" stderr
+		test_grep "refusing inconsistent update" stderr
 	)
 '
 
@@ -1161,7 +1161,7 @@
 test_expect_success 'push --porcelain bad url' '
 	mk_empty testrepo &&
 	test_must_fail git push >.git/bar --porcelain asdfasdfasd refs/heads/main:refs/remotes/origin/main &&
-	! grep -q Done .git/bar
+	test_grep ! -q Done .git/bar
 '
 
 test_expect_success 'push --porcelain rejected' '
diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh
index 0e00193..27f38ab 100755
--- a/t/t5520-pull.sh
+++ b/t/t5520-pull.sh
@@ -356,8 +356,8 @@
 	echo "dirty" >>dst/file &&
 	test_config -C dst rebase.autostash true &&
 	git -C dst pull --rebase >actual 2>&1 &&
-	grep -q "Fast-forward" actual &&
-	grep -q "Applied autostash." actual
+	test_grep -q "Fast-forward" actual &&
+	test_grep -q "Applied autostash." actual
 '
 
 test_expect_success '--rebase with conflicts shows advice' '
diff --git a/t/t5524-pull-msg.sh b/t/t5524-pull-msg.sh
index b2be360..493156a 100755
--- a/t/t5524-pull-msg.sh
+++ b/t/t5524-pull-msg.sh
@@ -31,7 +31,7 @@
 	git pull --no-rebase --log &&
 	git log -2 &&
 	git cat-file commit HEAD >result &&
-	grep Dollar result
+	test_grep Dollar result
 )
 '
 
@@ -44,8 +44,8 @@
 	git pull --no-rebase --log=1 &&
 	git log -3 &&
 	git cat-file commit HEAD >result &&
-	grep Dollar result &&
-	! grep "second commit" result
+	test_grep Dollar result &&
+	test_grep ! "second commit" result
 )
 '
 
diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh
index 1242ee9..7b3b735 100755
--- a/t/t5526-fetch-submodules.sh
+++ b/t/t5526-fetch-submodules.sh
@@ -199,7 +199,7 @@
 	) &&
 	test_must_be_empty actual.out &&
 	verify_fetch_result actual.err &&
-	grep "2 tasks" trace.out
+	test_grep "2 tasks" trace.out
 '
 
 test_expect_success "fetch alone only fetches superproject" '
@@ -723,25 +723,25 @@
 	(
 		cd downstream &&
 		GIT_TRACE=$(pwd)/trace.out git fetch &&
-		grep "1 tasks" trace.out &&
+		test_grep "1 tasks" trace.out &&
 		>trace.out &&
 
 		GIT_TRACE=$(pwd)/trace.out git fetch --jobs 7 &&
-		grep "7 tasks" trace.out &&
+		test_grep "7 tasks" trace.out &&
 		>trace.out &&
 
 		git config submodule.fetchJobs 8 &&
 		GIT_TRACE=$(pwd)/trace.out git fetch &&
-		grep "8 tasks" trace.out &&
+		test_grep "8 tasks" trace.out &&
 		>trace.out &&
 
 		GIT_TRACE=$(pwd)/trace.out git fetch --jobs 9 &&
-		grep "9 tasks" trace.out &&
+		test_grep "9 tasks" trace.out &&
 		>trace.out &&
 
 		GIT_TRACE=$(pwd)/trace.out git -c submodule.fetchJobs=0 fetch &&
-		grep "preparing to run up to [0-9]* tasks" trace.out &&
-		! grep "up to 0 tasks" trace.out &&
+		test_grep "preparing to run up to [0-9]* tasks" trace.out &&
+		test_grep ! "up to 0 tasks" trace.out &&
 		>trace.out
 	)
 '
@@ -1259,7 +1259,7 @@
 		git config submodule.recurse true &&
 		git fetch --all --no-recurse-submodules 2>../fetch-log
 	) &&
-	! grep "Fetching submodule" fetch-log
+	test_grep ! "Fetching submodule" fetch-log
 '
 
 test_done
diff --git a/t/t5529-push-errors.sh b/t/t5529-push-errors.sh
index 80b06a0..5f8aa1e 100755
--- a/t/t5529-push-errors.sh
+++ b/t/t5529-push-errors.sh
@@ -44,14 +44,45 @@
 # 'builtin/push.c:set_refspecs()' and we want to test that regression.
 test_expect_success 'detect empty remote with existing local ref' '
 	test_must_fail git push "" main 2> stderr &&
-	grep "fatal: bad repository ${SQ}${SQ}" stderr
+	test_grep "fatal: bad repository ${SQ}${SQ}" stderr
 '
 
 # While similar to the previous test, here we want to ensure that
 # even targeted refspecs are handled.
 test_expect_success 'detect empty remote with targeted refspec' '
 	test_must_fail git push "" HEAD:refs/heads/main 2> stderr &&
-	grep "fatal: bad repository ${SQ}${SQ}" stderr
+	test_grep "fatal: bad repository ${SQ}${SQ}" stderr
+'
+
+test_expect_success 'suggest <remote> <branch> for a <remote>/<branch> slip' '
+	test_must_fail git push origin/main 2>stderr &&
+	test_grep "${SQ}origin/main${SQ} is not a valid push target" stderr &&
+	test_grep "hint: Did you mean to use: git push origin main?" stderr &&
+	test_must_fail git -c advice.pushRepoLooksLikeRef=false push origin/main 2>stderr &&
+	test_grep ! "Did you mean" stderr
+'
+
+test_expect_success 'suggest <remote> <branch> when the branch has slashes' '
+	test_must_fail git push origin/feature/x 2>stderr &&
+	test_grep "hint: Did you mean to use: git push origin feature/x?" stderr
+'
+
+test_expect_success 'no suggestion when prefix is not a configured remote' '
+	test_must_fail git push not-a-remote/main 2>stderr &&
+	test_grep ! "Did you mean" stderr
+'
+
+test_expect_success 'no suggestion for a trailing slash with no branch' '
+	test_must_fail git push origin/ 2>stderr &&
+	test_grep ! "Did you mean" stderr
+'
+
+test_expect_success 'no suggestion when the argument is an existing path' '
+	test_when_finished "rm -rf origin" &&
+	git init --bare origin/main &&
+	git push origin/main HEAD:refs/heads/pushed 2>stderr &&
+	test_grep ! "Did you mean" stderr &&
+	git -C origin/main rev-parse --verify refs/heads/pushed
 '
 
 test_expect_success 'detect ambiguous refs early' '
diff --git a/t/t5530-upload-pack-error.sh b/t/t5530-upload-pack-error.sh
index d40292c..25d409f 100755
--- a/t/t5530-upload-pack-error.sh
+++ b/t/t5530-upload-pack-error.sh
@@ -44,16 +44,16 @@
 		$(($hexsz + 10)) $(git rev-parse HEAD) \
 		$(($hexsz + 12)) $(git rev-parse HEAD^) >input &&
 	test_must_fail git upload-pack . <input >/dev/null 2>output.err &&
-	grep "bad tree object" output.err
+	test_grep "bad tree object" output.err
 '
 
 test_expect_success 'upload-pack fails due to bad want (no object)' '
 	printf "%04xwant %s multi_ack_detailed\n00000009done\n0000" \
 		$(($hexsz + 29)) $(test_oid deadbeef) >input &&
 	test_must_fail git upload-pack . <input >output 2>output.err &&
-	grep "not our ref" output.err &&
-	grep "ERR" output &&
-	! grep multi_ack_detailed output.err
+	test_grep "not our ref" output.err &&
+	test_grep "ERR" output &&
+	test_grep ! multi_ack_detailed output.err
 '
 
 test_expect_success 'upload-pack fails due to bad want (not tip)' '
@@ -61,17 +61,17 @@
 	printf "%04xwant %s multi_ack_detailed\n00000009done\n0000" \
 		$(($hexsz + 29)) "$oid" >input &&
 	test_must_fail git upload-pack . <input >output 2>output.err &&
-	grep "not our ref" output.err &&
-	grep "ERR" output &&
-	! grep multi_ack_detailed output.err
+	test_grep "not our ref" output.err &&
+	test_grep "ERR" output &&
+	test_grep ! multi_ack_detailed output.err
 '
 
 test_expect_success 'upload-pack fails due to error in pack-objects enumeration' '
 	printf "%04xwant %s\n00000009done\n0000" \
 		$((hexsz + 10)) $(git rev-parse HEAD) >input &&
 	test_must_fail git upload-pack . <input >/dev/null 2>output.err &&
-	grep "bad tree object" output.err &&
-	grep "pack-objects died" output.err
+	test_grep "bad tree object" output.err &&
+	test_grep "pack-objects died" output.err
 '
 
 test_expect_success 'upload-pack tolerates EOF just after stateless client wants' '
diff --git a/t/t5531-deep-submodule-push.sh b/t/t5531-deep-submodule-push.sh
index 05debd1..7d239dd 100755
--- a/t/t5531-deep-submodule-push.sh
+++ b/t/t5531-deep-submodule-push.sh
@@ -559,7 +559,7 @@
 	test_must_fail git -C upstream rev-parse refs/heads/downstream-branch &&
 	git -C upstream/sub rev-parse refs/heads/downstream-branch &&
 	git -C upstream/sub/deepsub rev-parse refs/heads/downstream-branch &&
-	grep "recursing into submodule with push.recurseSubmodules=only; using on-demand instead" err
+	test_grep "recursing into submodule with push.recurseSubmodules=only; using on-demand instead" err
 '
 
 test_expect_success 'push propagating the remotes name to a submodule' '
diff --git a/t/t5532-fetch-proxy.sh b/t/t5532-fetch-proxy.sh
index 95d0f33..ccbca7c 100755
--- a/t/t5532-fetch-proxy.sh
+++ b/t/t5532-fetch-proxy.sh
@@ -52,7 +52,7 @@
 
 test_expect_success 'funny hostnames are rejected before running proxy' '
 	test_must_fail git fetch git://-remote/repo.git 2>stderr &&
-	! grep "proxying for" stderr
+	test_grep ! "proxying for" stderr
 '
 
 test_done
diff --git a/t/t5533-push-cas.sh b/t/t5533-push-cas.sh
index cba26a8..2e14aec 100755
--- a/t/t5533-push-cas.sh
+++ b/t/t5533-push-cas.sh
@@ -69,7 +69,7 @@
 		cd dst &&
 		test_commit D &&
 		test_must_fail git push --force-with-lease=main:main origin main 2>err &&
-		grep "stale info" err
+		test_grep "stale info" err
 	) &&
 	git ls-remote . refs/heads/main >expect &&
 	git ls-remote src refs/heads/main >actual &&
@@ -82,7 +82,7 @@
 		cd dst &&
 		test_commit D &&
 		git push --force --force-with-lease=main:main origin main 2>err &&
-		grep "forced update" err
+		test_grep "forced update" err
 	) &&
 	git ls-remote dst refs/heads/main >expect &&
 	git ls-remote src refs/heads/main >actual &&
@@ -147,7 +147,7 @@
 		cd dst &&
 		test_commit D &&
 		git push --force-with-lease=main origin main 2>err &&
-		! grep "forced update" err
+		test_grep ! "forced update" err
 	) &&
 	git ls-remote dst refs/heads/main >expect &&
 	git ls-remote src refs/heads/main >actual &&
@@ -161,7 +161,7 @@
 		git reset --hard HEAD^ &&
 		test_commit D &&
 		git push --force-with-lease=main origin main 2>err &&
-		grep "forced update" err
+		test_grep "forced update" err
 	) &&
 	git ls-remote dst refs/heads/main >expect &&
 	git ls-remote src refs/heads/main >actual &&
@@ -194,7 +194,7 @@
 	(
 		cd dst &&
 		git push --force-with-lease=main origin :main 2>err &&
-		grep deleted err
+		test_grep deleted err
 	) &&
 	git ls-remote src refs/heads/main >actual &&
 	test_must_be_empty actual
@@ -350,7 +350,7 @@
 		remote_head="$(git rev-parse refs/remotes/origin/main)" &&
 		git fetch --all &&
 		test_must_fail git push --force-if-includes --force-with-lease="main:$remote_head" 2>err &&
-		grep "stale info" err
+		test_grep "stale info" err
 	) &&
 	git ls-remote dst refs/heads/main >actual.main &&
 	test_cmp expect.main actual.main
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
index 2a78221..21f0262 100755
--- a/t/t5534-push-signed.sh
+++ b/t/t5534-push-signed.sh
@@ -126,8 +126,8 @@
 	noop=$(git rev-parse noop) &&
 	ff=$(git rev-parse ff) &&
 	noff=$(git rev-parse noff) &&
-	grep "$noop $ff refs/heads/ff" dst/push-cert &&
-	grep "$noop $noff refs/heads/noff" dst/push-cert &&
+	test_grep "$noop $ff refs/heads/ff" dst/push-cert &&
+	test_grep "$noop $noff refs/heads/noff" dst/push-cert &&
 	test_cmp expect dst/push-cert-status
 '
 
@@ -172,8 +172,8 @@
 	noop=$(git rev-parse noop) &&
 	ff=$(git rev-parse ff) &&
 	noff=$(git rev-parse noff) &&
-	grep "$noop $ff refs/heads/ff" dst/push-cert &&
-	grep "$noop $noff refs/heads/noff" dst/push-cert &&
+	test_grep "$noop $ff refs/heads/ff" dst/push-cert &&
+	test_grep "$noop $noff refs/heads/noff" dst/push-cert &&
 	test_cmp expect dst/push-cert-status
 '
 
@@ -211,7 +211,7 @@
 	git -C dst config receive.advertisepushoptions 1 &&
 	git receive-pack dst <push.tweak >out &&
 	git -C dst rev-parse ff &&
-	grep "inconsistent push options" out
+	test_grep "inconsistent push options" out
 '
 
 test_expect_success GPG 'fail without key and heed user.signingkey' '
@@ -257,8 +257,8 @@
 	noop=$(git rev-parse noop) &&
 	ff=$(git rev-parse ff) &&
 	noff=$(git rev-parse noff) &&
-	grep "$noop $ff refs/heads/ff" dst/push-cert &&
-	grep "$noop $noff refs/heads/noff" dst/push-cert &&
+	test_grep "$noop $ff refs/heads/ff" dst/push-cert &&
+	test_grep "$noop $noff refs/heads/noff" dst/push-cert &&
 	test_cmp expect dst/push-cert-status
 '
 
@@ -309,8 +309,8 @@
 	noop=$(git rev-parse noop) &&
 	ff=$(git rev-parse ff) &&
 	noff=$(git rev-parse noff) &&
-	grep "$noop $ff refs/heads/ff" dst/push-cert &&
-	grep "$noop $noff refs/heads/noff" dst/push-cert &&
+	test_grep "$noop $ff refs/heads/ff" dst/push-cert &&
+	test_grep "$noop $noff refs/heads/noff" dst/push-cert &&
 	test_cmp expect dst/push-cert-status
 '
 
@@ -362,8 +362,8 @@
 	noop=$(git rev-parse noop) &&
 	ff=$(git rev-parse ff) &&
 	noff=$(git rev-parse noff) &&
-	grep "$noop $ff refs/heads/ff" dst/push-cert &&
-	grep "$noop $noff refs/heads/noff" dst/push-cert &&
+	test_grep "$noop $ff refs/heads/ff" dst/push-cert &&
+	test_grep "$noop $noff refs/heads/noff" dst/push-cert &&
 	test_cmp expect dst/push-cert-status
 '
 
diff --git a/t/t5537-fetch-shallow.sh b/t/t5537-fetch-shallow.sh
index 9982dd2..f323cee 100755
--- a/t/t5537-fetch-shallow.sh
+++ b/t/t5537-fetch-shallow.sh
@@ -244,7 +244,7 @@
 		origin "+refs/heads/*:refs/remotes/origin/*" &&
 	git -C shallow-client repack -adfl &&
 	test_must_fail git -C shallow-client rev-parse --verify $d^0 &&
-	! grep $d shallow-client/.git/shallow &&
+	test_grep ! $d shallow-client/.git/shallow &&
 
 	git -C shallow-server branch branch-orig $d &&
 	git -C shallow-client fetch --prune --depth=2 \
diff --git a/t/t5538-push-shallow.sh b/t/t5538-push-shallow.sh
index dc0e972..afab456 100755
--- a/t/t5538-push-shallow.sh
+++ b/t/t5538-push-shallow.sh
@@ -65,7 +65,7 @@
 	(
 	cd shallow2 &&
 	test_must_fail git push ../.git +main:refs/remotes/shallow2/main 2>err &&
-	grep "shallow2/main.*shallow update not allowed" err
+	test_grep "shallow2/main.*shallow update not allowed" err
 	) &&
 	test_must_fail git rev-parse shallow2/main &&
 	git fsck
diff --git a/t/t5539-fetch-http-shallow.sh b/t/t5539-fetch-http-shallow.sh
index 3ea75d3..44e6f60 100755
--- a/t/t5539-fetch-http-shallow.sh
+++ b/t/t5539-fetch-http-shallow.sh
@@ -76,8 +76,8 @@
 		# might be able to run this test in all protocol versions.
 		GIT_TRACE_PACKET="$TRASH_DIRECTORY/trace" GIT_TEST_PROTOCOL_VERSION=0 \
 			git fetch --depth=2 &&
-		grep "fetch-pack< ACK .* ready" ../trace &&
-		! grep "fetch-pack> done" ../trace
+		test_grep "fetch-pack< ACK .* ready" ../trace &&
+		test_grep ! "fetch-pack> done" ../trace
 	)
 '
 
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index 538b603..8c34833 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -56,8 +56,8 @@
 	git commit -m path2 &&
 	HEAD=$(git rev-parse --verify HEAD) &&
 	GIT_TRACE_CURL=true git push -v -v 2>err &&
-	! grep "Expect: 100-continue" err &&
-	grep "POST git-receive-pack ([0-9]* bytes)" err &&
+	test_grep ! "Expect: 100-continue" err &&
+	test_grep "POST git-receive-pack ([0-9]* bytes)" err &&
 	(cd "$HTTPD_DOCUMENT_ROOT_PATH"/test_repo.git &&
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
@@ -84,7 +84,7 @@
 	git commit -m path_lang &&
 	HEAD=$(git rev-parse --verify HEAD) &&
 	GIT_TRACE_CURL=true LANGUAGE="ko_KR.UTF-8" git push -v -v 2>err &&
-	! grep "Expect: 100-continue" err &&
+	test_grep ! "Expect: 100-continue" err &&
 
 	grep "=> Send header: Accept-Language:" err >err.language &&
 	test_cmp exp err.language
@@ -148,8 +148,8 @@
 	test_must_fail git push -v origin +main main:niam >output 2>&1'
 
 test_expect_success 'push fails for non-fast-forward refs unmatched by remote helper: remote output' '
-	grep "^ + [a-f0-9]*\.\.\.[a-f0-9]* *main -> main (forced update)$" output &&
-	grep "^ ! \[rejected\] *main -> niam (non-fast-forward)$" output
+	test_grep "^ + [a-f0-9]*\.\.\.[a-f0-9]* *main -> main (forced update)$" output &&
+	test_grep "^ ! \[rejected\] *main -> niam (non-fast-forward)$" output
 '
 
 test_expect_success 'push fails for non-fast-forward refs unmatched by remote helper: our output' '
@@ -163,7 +163,7 @@
 	HEAD=$(git rev-parse --verify HEAD) &&
 	test_config http.postbuffer 4 &&
 	git push -v -v origin $BRANCH 2>err &&
-	grep "POST git-receive-pack (chunked)" err &&
+	test_grep "POST git-receive-pack (chunked)" err &&
 	(cd "$HTTPD_DOCUMENT_ROOT_PATH"/test_repo.git &&
 	 test $HEAD = $(git rev-parse --verify HEAD))
 '
@@ -215,15 +215,15 @@
 	test_cmp expected actual &&
 
 	# the failed refs should be indicated to the user
-	grep "^ ! .*rejected.* main -> main" output &&
+	test_grep "^ ! .*rejected.* main -> main" output &&
 
 	# the collateral failure refs should be indicated to the user
-	grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
-	grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output &&
+	test_grep "^ ! .*rejected.* atomic -> atomic .*atomic push failed" output &&
+	test_grep "^ ! .*rejected.* collateral -> collateral .*atomic push failed" output &&
 
 	# never report what we do not push
-	! grep "^ ! .*rejected.* atomic1 " output &&
-	! grep "^ ! .*rejected.* other " output
+	test_grep ! "^ ! .*rejected.* atomic1 " output &&
+	test_grep ! "^ ! .*rejected.* other " output
 '
 
 test_expect_success 'push --atomic fails on server-side errors' '
@@ -247,10 +247,10 @@
 	test_must_fail git -C "$d" show-ref --verify refs/heads/other &&
 
 	# the failed refs should be indicated to the user
-	grep "^ ! .*rejected.* other -> other .*atomic transaction failed" output &&
+	test_grep "^ ! .*rejected.* other -> other .*atomic transaction failed" output &&
 
 	# the collateral failure refs should be indicated to the user
-	grep "^ ! .*rejected.* atomic -> atomic .*atomic transaction failed" output
+	test_grep "^ ! .*rejected.* atomic -> atomic .*atomic transaction failed" output
 '
 
 test_expect_success 'push --all can push to empty repo' '
@@ -442,7 +442,7 @@
 		"$HTTPD_URL_USER_PASS/smart/test_repo.git" \
 		+HEAD:scrub >status &&
 	# should have been scrubbed down to vanilla URL
-	grep "^To $HTTPD_URL/smart/test_repo.git" status
+	test_grep "^To $HTTPD_URL/smart/test_repo.git" status
 '
 
 test_expect_success 'clone/fetch scrubs password from reflogs' '
@@ -456,8 +456,8 @@
 		+main:main &&
 	# should have been scrubbed down to vanilla URL
 	git log -g main >reflog &&
-	grep "$HTTPD_URL" reflog &&
-	! grep "$HTTPD_URL_USER_PASS" reflog
+	test_grep "$HTTPD_URL" reflog &&
+	test_grep ! "$HTTPD_URL_USER_PASS" reflog
 '
 
 test_expect_success 'Non-ASCII branch name can be used with --force-with-lease' '
diff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh
index 89147a0..908d3b8 100755
--- a/t/t5544-pack-objects-hook.sh
+++ b/t/t5544-pack-objects-hook.sh
@@ -27,7 +27,7 @@
 	clear_hook_results &&
 	test_config_global uploadpack.packObjectsHook ./hook &&
 	git clone --no-local . dst.git 2>stderr &&
-	grep "hook running" stderr
+	test_grep "hook running" stderr
 '
 
 test_expect_success 'hook outputs are sane' '
@@ -38,7 +38,7 @@
 	# the full argument list or the exact pack contents, as it would make
 	# the test brittle. So just sanity check that we could replay
 	# the packing procedure.
-	grep "^git" .git/hook.args &&
+	test_grep "^git" .git/hook.args &&
 	$(cat .git/hook.args) <.git/hook.stdin >replay
 '
 
@@ -47,14 +47,14 @@
 	git clone --no-local \
 	  -u "git -c uploadpack.packObjectsHook=./hook upload-pack" \
 	  . dst.git 2>stderr &&
-	grep "hook running" stderr
+	test_grep "hook running" stderr
 '
 
 test_expect_success 'hook does not run from repo config' '
 	clear_hook_results &&
 	test_config uploadpack.packObjectsHook "./hook" &&
 	git clone --no-local . dst.git 2>stderr &&
-	! grep "hook running" stderr &&
+	test_grep ! "hook running" stderr &&
 	test_path_is_missing .git/hook.args &&
 	test_path_is_missing .git/hook.stdin &&
 	test_path_is_missing .git/hook.stdout &&
@@ -62,7 +62,7 @@
 	# check that global config is used instead
 	test_config_global uploadpack.packObjectsHook ./hook &&
 	git clone --no-local . dst2.git 2>stderr &&
-	grep "hook running" stderr
+	test_grep "hook running" stderr
 '
 
 test_expect_success 'hook works with partial clone' '
@@ -72,7 +72,7 @@
 	git clone --bare --no-local --filter=blob:none . dst.git &&
 	git -C dst.git rev-list --objects --missing=allow-any --no-object-names --all >objects &&
 	git -C dst.git cat-file --batch-check="%(objecttype)" <objects >types &&
-	! grep blob types
+	test_grep ! blob types
 '
 
 test_done
diff --git a/t/t5550-http-fetch-dumb.sh b/t/t5550-http-fetch-dumb.sh
index b0080bf..f00eeae 100755
--- a/t/t5550-http-fetch-dumb.sh
+++ b/t/t5550-http-fetch-dumb.sh
@@ -124,7 +124,7 @@
 	set_netrc 127.0.0.1 forbidden-user@host pass@host &&
 	test_must_fail git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-netrc-403 2>err &&
 	expect_askpass none &&
-	grep "The requested URL returned error: 403" err
+	test_grep "The requested URL returned error: 403" err
 '
 
 test_expect_success 'http auth can use user/pass in URL' '
@@ -280,7 +280,7 @@
 		--index-pack-arg=--keep \
 		"$HTTPD_URL"/dumb/repo_pack.git/$p >out &&
 
-	grep -E "^keep.[0-9a-f]{16,}$" out &&
+	test_grep -E "^keep.[0-9a-f]{16,}$" out &&
 	cut -c6- out >packhash &&
 
 	# Ensure that the expected files are generated
diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
index dcff0bc..805bec0 100755
--- a/t/t5551-http-fetch-smart.sh
+++ b/t/t5551-http-fetch-smart.sh
@@ -164,8 +164,8 @@
 
 test_expect_success 'used upload-pack service' '
 	strip_access_log >log &&
-	grep "GET  /smart/repo.git/info/refs?service=git-upload-pack HTTP/[0-9.]* 200" log &&
-	grep "POST /smart/repo.git/git-upload-pack HTTP/[0-9.]* 200" log
+	test_grep "GET  /smart/repo.git/info/refs?service=git-upload-pack HTTP/[0-9.]* 200" log &&
+	test_grep "POST /smart/repo.git/git-upload-pack HTTP/[0-9.]* 200" log
 '
 
 test_expect_success 'follow redirects (301)' '
@@ -264,8 +264,8 @@
 
 	# Ensure that there is no "Basic" followed by a base64 string, but that
 	# the auth details are redacted
-	! grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace &&
-	grep -i "Authorization: Basic <redacted>" trace
+	test_grep ! -i "Authorization: Basic [0-9a-zA-Z+/]" trace &&
+	test_grep -i "Authorization: Basic <redacted>" trace
 '
 
 test_expect_success 'GIT_CURL_VERBOSE redacts auth details' '
@@ -276,8 +276,8 @@
 
 	# Ensure that there is no "Basic" followed by a base64 string, but that
 	# the auth details are redacted
-	! grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace &&
-	grep -i "Authorization: Basic <redacted>" trace
+	test_grep ! -i "Authorization: Basic [0-9a-zA-Z+/]" trace &&
+	test_grep -i "Authorization: Basic <redacted>" trace
 '
 
 test_expect_success 'GIT_TRACE_CURL does not redact auth details if GIT_TRACE_REDACT=0' '
@@ -287,7 +287,7 @@
 		git clone --bare "$HTTPD_URL/auth/smart/repo.git" redact-auth &&
 	expect_askpass both user%40host &&
 
-	grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace
+	test_grep -i "Authorization: Basic [0-9a-zA-Z+/]" trace
 '
 
 test_expect_success 'disable dumb http on server' '
@@ -419,7 +419,7 @@
 		clone --bare "$HTTPD_URL/smart/repo.git" split.git 2>err &&
 	{
 		test_have_prereq HTTP2 ||
-		grep "^=> Send header: Transfer-Encoding: chunked" err
+		test_grep "^=> Send header: Transfer-Encoding: chunked" err
 	}
 '
 
@@ -554,10 +554,10 @@
 	GIT_TRACE_CURL=true \
 		git -c "http.cookieFile=$(pwd)/cookies" clone \
 		$HTTPD_URL/smart/repo.git clone 2>err &&
-	grep -i "Cookie:.*Foo=<redacted>" err &&
-	grep -i "Cookie:.*Bar=<redacted>" err &&
-	! grep -i "Cookie:.*Foo=1" err &&
-	! grep -i "Cookie:.*Bar=2" err
+	test_grep -i "Cookie:.*Foo=<redacted>" err &&
+	test_grep -i "Cookie:.*Bar=<redacted>" err &&
+	test_grep ! -i "Cookie:.*Foo=1" err &&
+	test_grep ! -i "Cookie:.*Bar=2" err
 '
 
 test_expect_success 'empty values of cookies are also redacted' '
@@ -566,7 +566,7 @@
 	GIT_TRACE_CURL=true \
 		git -c "http.cookieFile=$(pwd)/cookies" clone \
 		$HTTPD_URL/smart/repo.git clone 2>err &&
-	grep -i "Cookie:.*Foo=<redacted>" err
+	test_grep -i "Cookie:.*Foo=<redacted>" err
 '
 
 test_expect_success 'GIT_TRACE_REDACT=0 disables cookie redaction' '
@@ -576,20 +576,20 @@
 	GIT_TRACE_REDACT=0 GIT_TRACE_CURL=true \
 		git -c "http.cookieFile=$(pwd)/cookies" clone \
 		$HTTPD_URL/smart/repo.git clone 2>err &&
-	grep -i "Cookie:.*Foo=1" err &&
-	grep -i "Cookie:.*Bar=2" err
+	test_grep -i "Cookie:.*Foo=1" err &&
+	test_grep -i "Cookie:.*Bar=2" err
 '
 
 test_expect_success 'GIT_TRACE_CURL_NO_DATA prevents data from being traced' '
 	rm -rf clone &&
 	GIT_TRACE_CURL=true \
 		git clone $HTTPD_URL/smart/repo.git clone 2>err &&
-	grep "=> Send data" err &&
+	test_grep "=> Send data" err &&
 
 	rm -rf clone &&
 	GIT_TRACE_CURL=true GIT_TRACE_CURL_NO_DATA=1 \
 		git clone $HTTPD_URL/smart/repo.git clone 2>err &&
-	! grep "=> Send data" err
+	test_grep ! "=> Send data" err
 '
 
 test_expect_success 'server-side error detected' '
@@ -644,7 +644,7 @@
 	git clone $HTTPD_URL/smart_v0/repo.git repo-v0 &&
 	# check for v0; there the HEAD symref is communicated in the capability
 	# line; v2 uses a different syntax on each ref advertisement line
-	grep symref=HEAD:refs/heads/ trace
+	test_grep symref=HEAD:refs/heads/ trace
 '
 
 test_expect_success 'create empty http-accessible SHA-256 repository' '
@@ -692,7 +692,7 @@
 
 	git -c transfer.credentialsInUrl=allow \
 		clone $url_userpass attempt1 2>err &&
-	! grep "$message" err &&
+	test_grep ! "$message" err &&
 
 	git -c transfer.credentialsInUrl=warn \
 		clone $url_userpass attempt2 2>err &&
@@ -721,12 +721,12 @@
 	esac &&
 
 	git -c transfer.credentialsInUrl=warn clone $url_user attempt1 2>err &&
-	! grep "uses plaintext credentials" err
+	test_grep ! "uses plaintext credentials" err
 '
 
 test_expect_success 'fetch warns or fails when using username:password' '
 	git -c transfer.credentialsInUrl=allow fetch $url_userpass 2>err &&
-	! grep "$message" err &&
+	test_grep ! "$message" err &&
 
 	git -c transfer.credentialsInUrl=warn fetch $url_userpass 2>err &&
 	grep "warning: $message" err >warnings &&
@@ -746,7 +746,7 @@
 
 test_expect_success 'push warns or fails when using username:password' '
 	git -c transfer.credentialsInUrl=allow push $url_userpass 2>err &&
-	! grep "$message" err &&
+	test_grep ! "$message" err &&
 
 	git -c transfer.credentialsInUrl=warn push $url_userpass 2>err &&
 	grep "warning: $message" err >warnings &&
@@ -763,7 +763,7 @@
 	git clone $HTTPD_URL/smart/repo.git/ clone-with-slash &&
 
 	strip_access_log >log &&
-	! grep "//" log
+	test_grep ! "//" log
 '
 
 test_expect_success 'tag following always works over v0 http' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index eeddb85..96e2241 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -201,7 +201,7 @@
 		export GIT_TEST_PROTOCOL_VERSION &&
 		trace_fetch client "$(pwd)/server" to_fetch
 	) &&
-	grep "  fetch" trace &&
+	test_grep "  fetch" trace &&
 
 	# fetch-pack sends 2 requests each containing 16 "have" lines before
 	# processing the first response. In these 2 requests, 4 commits from
@@ -211,8 +211,8 @@
 
 	# While fetch-pack is processing the first response, it should read that
 	# the server ACKs b1.c19 and b1.c17.
-	grep "fetch< ACK $(git -C client rev-parse b1.c19) common" trace &&
-	grep "fetch< ACK $(git -C client rev-parse b1.c17) common" trace &&
+	test_grep "fetch< ACK $(git -C client rev-parse b1.c19) common" trace &&
+	test_grep "fetch< ACK $(git -C client rev-parse b1.c17) common" trace &&
 
 	# fetch-pack should thus not send any more commits in the b1 branch, but
 	# should still send the others (in this test, just check b2).
diff --git a/t/t5554-noop-fetch-negotiator.sh b/t/t5554-noop-fetch-negotiator.sh
index 17e73b6..d50ffe5 100755
--- a/t/t5554-noop-fetch-negotiator.sh
+++ b/t/t5554-noop-fetch-negotiator.sh
@@ -16,8 +16,8 @@
 	test_config -C client fetch.negotiationalgorithm noop &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch "$(pwd)/server" &&
 
-	! grep "fetch> have" trace &&
-	grep "fetch> done" trace
+	test_grep ! "fetch> have" trace &&
+	test_grep "fetch> done" trace
 '
 
 test_done
diff --git a/t/t5557-http-get.sh b/t/t5557-http-get.sh
index 67fcc23..e8ffe3f 100755
--- a/t/t5557-http-get.sh
+++ b/t/t5557-http-get.sh
@@ -18,7 +18,7 @@
 
 	test_must_fail git remote-http $url <input 2>err &&
 	test_path_is_missing file1 &&
-	grep "failed to download file at URL" err
+	test_grep "failed to download file at URL" err
 '
 
 test_expect_success 'get by URL: 200' '
diff --git a/t/t5558-clone-bundle-uri.sh b/t/t5558-clone-bundle-uri.sh
index 7a0943b..f81f1f8 100755
--- a/t/t5558-clone-bundle-uri.sh
+++ b/t/t5558-clone-bundle-uri.sh
@@ -8,14 +8,14 @@
 test_expect_success 'fail to clone from non-existent file' '
 	test_when_finished rm -rf test &&
 	git clone --bundle-uri="$(pwd)/does-not-exist" . test 2>err &&
-	grep "failed to download bundle from URI" err
+	test_grep "failed to download bundle from URI" err
 '
 
 test_expect_success 'fail to clone from non-bundle file' '
 	test_when_finished rm -rf test &&
 	echo bogus >bogus &&
 	git clone --bundle-uri="$(pwd)/bogus" . test 2>err &&
-	grep "is not a bundle" err
+	test_grep "is not a bundle" err
 '
 
 test_expect_success 'create bundle' '
@@ -197,7 +197,7 @@
 
 	git clone --bundle-uri="file://$(pwd)/bundle-list" \
 		clone-from clone-list-file 2>err &&
-	! grep "Repository lacks these prerequisite commits" err &&
+	test_grep ! "Repository lacks these prerequisite commits" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-list-file cat-file --batch-check <oids &&
@@ -242,9 +242,9 @@
 	GIT_TRACE2_PERF=1 \
 	git clone --bundle-uri="file://$(pwd)/bundle-list" \
 		clone-from clone-all-some 2>err &&
-	! grep "Repository lacks these prerequisite commits" err &&
-	! grep "fatal" err &&
-	grep "warning: failed to download bundle from URI" err &&
+	test_grep ! "Repository lacks these prerequisite commits" err &&
+	test_grep ! "fatal" err &&
+	test_grep "warning: failed to download bundle from URI" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-all-some cat-file --batch-check <oids &&
@@ -275,15 +275,15 @@
 
 	git clone --bundle-uri="file://$(pwd)/bundle-list" \
 		clone-from clone-all-fail 2>err &&
-	! grep "Repository lacks these prerequisite commits" err &&
-	! grep "fatal" err &&
-	grep "warning: failed to download bundle from URI" err &&
+	test_grep ! "Repository lacks these prerequisite commits" err &&
+	test_grep ! "fatal" err &&
+	test_grep "warning: failed to download bundle from URI" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-all-fail cat-file --batch-check <oids &&
 
 	git -C clone-all-fail for-each-ref --format="%(refname)" >refs &&
-	! grep "refs/bundles/heads/" refs
+	test_grep ! "refs/bundles/heads/" refs
 '
 
 test_expect_success 'clone bundle list (file, any mode)' '
@@ -306,7 +306,7 @@
 
 	git clone --bundle-uri="file://$(pwd)/bundle-list" \
 		clone-from clone-any-file 2>err &&
-	! grep "Repository lacks these prerequisite commits" err &&
+	test_grep ! "Repository lacks these prerequisite commits" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-any-file cat-file --batch-check <oids &&
@@ -336,14 +336,14 @@
 
 	git clone --bundle-uri="file://$(pwd)/bundle-list" \
 		clone-from clone-any-fail 2>err &&
-	! grep "fatal" err &&
-	grep "warning: failed to download bundle from URI" err &&
+	test_grep ! "fatal" err &&
+	test_grep "warning: failed to download bundle from URI" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-any-fail cat-file --batch-check <oids &&
 
 	git -C clone-any-fail for-each-ref --format="%(refname)" >refs &&
-	! grep "refs/bundles/heads/" refs
+	test_grep ! "refs/bundles/heads/" refs
 '
 
 test_expect_success 'negotiation: bundle with part of wanted commits' '
@@ -477,14 +477,14 @@
 test_expect_success 'fail to fetch from non-existent HTTP URL' '
 	test_when_finished rm -rf test &&
 	git clone --bundle-uri="$HTTPD_URL/does-not-exist" . test 2>err &&
-	grep "failed to download bundle from URI" err
+	test_grep "failed to download bundle from URI" err
 '
 
 test_expect_success 'fail to fetch from non-bundle HTTP URL' '
 	test_when_finished rm -rf test &&
 	echo bogus >"$HTTPD_DOCUMENT_ROOT_PATH/bogus" &&
 	git clone --bundle-uri="$HTTPD_URL/bogus" . test 2>err &&
-	grep "is not a bundle" err
+	test_grep "is not a bundle" err
 '
 
 test_expect_success 'clone HTTP bundle' '
@@ -536,7 +536,7 @@
 	GIT_TRACE2_EVENT="$(pwd)/trace-clone.txt" \
 		git clone --bundle-uri="$HTTPD_URL/bundle-list" \
 		clone-from clone-list-http  2>err &&
-	! grep "Repository lacks these prerequisite commits" err &&
+	test_grep ! "Repository lacks these prerequisite commits" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-list-http cat-file --batch-check <oids &&
@@ -576,8 +576,8 @@
 
 	git clone --bundle-uri="$HTTPD_URL/bundle-list" \
 		clone-from clone-any-http 2>err &&
-	! grep "fatal" err &&
-	grep "warning: failed to download bundle from URI" err &&
+	test_grep ! "fatal" err &&
+	test_grep "warning: failed to download bundle from URI" err &&
 
 	git -C clone-from for-each-ref --format="%(objectname)" >oids &&
 	git -C clone-any-http cat-file --batch-check <oids &&
@@ -1302,6 +1302,35 @@
 	test_path_is_missing escape
 '
 
+test_expect_success 'bundles advertised with missing URI' '
+	git clone --no-local --mirror clone-from \
+		"$HTTPD_DOCUMENT_ROOT_PATH/no-uri.git" &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/no-uri.git" config uploadpack.advertiseBundleURIs true &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/no-uri.git" config bundle.version 1 &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/no-uri.git" config bundle.mode all &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/no-uri.git" config bundle.bundle-1.creationToken 1 &&
+
+	git -c transfer.bundleURI=true clone \
+		"$HTTPD_URL/smart/no-uri.git" target-no-uri 2>err &&
+	test_grep "bundle ${SQ}bundle-1${SQ} has no uri" err &&
+	test_grep ! "expected packfile" err
+'
+
+test_expect_success 'bundles advertised with empty URI' '
+	git clone --no-local --mirror clone-from \
+		"$HTTPD_DOCUMENT_ROOT_PATH/empty-uri.git" &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/empty-uri.git" config uploadpack.advertiseBundleURIs true &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/empty-uri.git" config bundle.version 1 &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/empty-uri.git" config bundle.mode all &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/empty-uri.git" config bundle.bundle-1.uri "" &&
+	git -C "$HTTPD_DOCUMENT_ROOT_PATH/empty-uri.git" config bundle.bundle-1.creationToken 1 &&
+
+	git -c transfer.bundleURI=true clone \
+		"$HTTPD_URL/smart/empty-uri.git" target-empty-uri 2>err &&
+	test_grep "bundle ${SQ}bundle-1${SQ} has no uri" err &&
+	test_grep ! "expected packfile" err
+'
+
 # Do not add tests here unless they use the HTTP server, as they will
 # not run unless the HTTP dependencies exist.
 
diff --git a/t/t5562-http-backend-content-length.sh b/t/t5562-http-backend-content-length.sh
index b6ee06f..59c6a50 100755
--- a/t/t5562-http-backend-content-length.sh
+++ b/t/t5562-http-backend-content-length.sh
@@ -163,7 +163,7 @@
 		REQUEST_METHOD=POST \
 		CONTENT_LENGTH="$NOT_FIT_IN_SSIZE" \
 		git http-backend </dev/null >/dev/null 2>err &&
-	grep "fatal:.*CONTENT_LENGTH" err
+	test_grep "fatal:.*CONTENT_LENGTH" err
 '
 
 test_expect_success 'empty CONTENT_LENGTH' '
diff --git a/t/t5564-http-proxy.sh b/t/t5564-http-proxy.sh
index 817cd1f..866e2b4 100755
--- a/t/t5564-http-proxy.sh
+++ b/t/t5564-http-proxy.sh
@@ -20,14 +20,14 @@
 test_expect_success 'proxy requires password' '
 	test_config_global http.proxy $HTTPD_DEST &&
 	test_must_fail git clone $HTTPD_URL/smart/repo.git 2>err &&
-	grep "error.*407" err
+	test_grep "error.*407" err
 '
 
 test_expect_success 'clone through proxy with auth' '
 	test_when_finished "rm -rf clone" &&
 	test_config_global http.proxy http://proxuser:proxpass@$HTTPD_DEST &&
 	GIT_TRACE_CURL=$PWD/trace git clone $HTTPD_URL/smart/repo.git clone &&
-	grep -i "Proxy-Authorization: Basic <redacted>" trace
+	test_grep -i "Proxy-Authorization: Basic <redacted>" trace
 '
 
 test_expect_success 'clone can prompt for proxy password' '
@@ -82,7 +82,7 @@
 			GIT_TRACE_CURL=$PWD/trace \
 			GIT_TRACE_CURL_COMPONENTS=socks \
 			git clone "$HTTPD_URL/smart/repo.git" clone 2>err &&
-			grep -i "SOCKS4 request granted" trace
+			test_grep -i "SOCKS4 request granted" trace
 		} ||
 		old_libcurl_error err
 	}
@@ -90,14 +90,14 @@
 
 test_expect_success 'Unix socket requires socks*:' - <<\EOT
 	! git clone -c http.proxy=localhost/path https://example.com/repo.git 2>err && {
-		grep -Fx "fatal: Invalid proxy URL 'localhost/path': only SOCKS proxies support paths" err ||
+		test_grep -Fx "fatal: Invalid proxy URL 'localhost/path': only SOCKS proxies support paths" err ||
 		old_libcurl_error err
 	}
 EOT
 
 test_expect_success 'Unix socket requires localhost' - <<\EOT
 	! git clone -c http.proxy=socks4://127.0.0.1/path https://example.com/repo.git 2>err && {
-		grep -Fx "fatal: Invalid proxy URL 'socks4://127.0.0.1/path': host must be localhost if a path is present" err ||
+		test_grep -Fx "fatal: Invalid proxy URL 'socks4://127.0.0.1/path': host must be localhost if a path is present" err ||
 		old_libcurl_error err
 	}
 EOT
diff --git a/t/t5581-http-curl-verbose.sh b/t/t5581-http-curl-verbose.sh
index cded79c..6788cf8 100755
--- a/t/t5581-http-curl-verbose.sh
+++ b/t/t5581-http-curl-verbose.sh
@@ -23,7 +23,7 @@
 	test_might_fail env GIT_CURL_VERBOSE=1 \
 		git clone "$HTTPD_URL/error_git_upload_pack/smart/repo.git" \
 		2>curl_log &&
-	grep "<= Recv header: HTTP/1.1 500 Intentional Breakage" curl_log
+	test_grep "<= Recv header: HTTP/1.1 500 Intentional Breakage" curl_log
 '
 
 test_done
diff --git a/t/t5583-push-branches.sh b/t/t5583-push-branches.sh
index e7e1b6d..1ccfaee 100755
--- a/t/t5583-push-branches.sh
+++ b/t/t5583-push-branches.sh
@@ -60,21 +60,21 @@
 	test_must_fail git push remote-1 --all main >actual.all 2>&1 &&
 	test_must_fail git push remote-1 --branches main >actual.branches 2>&1 &&
 	test_cmp actual.all actual.branches &&
-	grep "be combined with refspecs" actual.all
+	test_grep "be combined with refspecs" actual.all
 '
 
 test_expect_success '--all or --branches can not be combined with --mirror' '
 	test_must_fail git push remote-1 --all --mirror >actual.all 2>&1 &&
 	test_must_fail git push remote-1 --branches --mirror >actual.branches 2>&1 &&
 	test_cmp actual.all actual.branches &&
-	grep "cannot be used together" actual.all
+	test_grep "cannot be used together" actual.all
 '
 
 test_expect_success '--all or --branches can not be combined with --tags' '
 	test_must_fail git push remote-1 --all --tags >actual.all 2>&1 &&
 	test_must_fail git push remote-1 --branches --tags >actual.branches 2>&1 &&
 	test_cmp actual.all actual.branches &&
-	grep "cannot be used together" actual.all
+	test_grep "cannot be used together" actual.all
 '
 
 
@@ -82,7 +82,7 @@
 	test_must_fail git push remote-1 --all --delete >actual.all 2>&1 &&
 	test_must_fail git push remote-1 --branches --delete >actual.branches 2>&1 &&
 	test_cmp actual.all actual.branches &&
-	grep "cannot be used together" actual.all
+	test_grep "cannot be used together" actual.all
 '
 
 test_expect_success '--all or --branches combines with --follow-tags have same behavior' '
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index 3dd229c..b616758 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -159,8 +159,8 @@
 	git clone --mirror src mirror2 &&
 	(cd mirror2 &&
 	 git show-ref 2> clone.err > clone.out) &&
-	! grep Duplicate mirror2/clone.err &&
-	grep some-tag mirror2/clone.out
+	test_grep ! Duplicate mirror2/clone.err &&
+	test_grep some-tag mirror2/clone.out
 
 '
 
@@ -224,12 +224,12 @@
 		cd src-0 && git init
 	) &&
 	git clone "file://$(pwd)/src-0" target-6 2>err-6 &&
-	! grep "fatal:" err-6 &&
+	test_grep ! "fatal:" err-6 &&
 	(
 		cd src-0 && test_commit A
 	) &&
 	git clone "file://$(pwd)/src-0" target-7 2>err-7 &&
-	! grep "fatal:" err-7 &&
+	test_grep ! "fatal:" err-7 &&
 	# There is no reason to insist they are bit-for-bit
 	# identical, but this test should suffice for now.
 	test_cmp target-6/.git/config target-7/.git/config
@@ -298,22 +298,22 @@
 	rm -rf dst &&
 	echo foo=bar >>realgitdir/config &&
 	test_must_fail git clone --separate-git-dir realgitdir src dst &&
-	grep foo=bar realgitdir/config
+	test_grep foo=bar realgitdir/config
 '
 
 test_expect_success 'clone --reference from original' '
 	git clone --shared --bare src src-1 &&
 	git clone --bare src src-2 &&
 	git clone --reference=src-2 --bare src-1 target-8 &&
-	grep /src-2/ target-8/objects/info/alternates
+	test_grep /src-2/ target-8/objects/info/alternates
 '
 
 test_expect_success 'clone with more than one --reference' '
 	git clone --bare src src-3 &&
 	git clone --bare src src-4 &&
 	git clone --reference=src-3 --reference=src-4 src target-9 &&
-	grep /src-3/ target-9/.git/objects/info/alternates &&
-	grep /src-4/ target-9/.git/objects/info/alternates
+	test_grep /src-3/ target-9/.git/objects/info/alternates &&
+	test_grep /src-4/ target-9/.git/objects/info/alternates
 '
 
 test_expect_success 'clone from original with relative alternate' '
@@ -321,7 +321,7 @@
 	git clone --bare src nest/src-5 &&
 	echo ../../../src/.git/objects >nest/src-5/objects/info/alternates &&
 	git clone --bare nest/src-5 target-10 &&
-	grep /src/\\.git/objects target-10/objects/info/alternates
+	test_grep /src/\\.git/objects target-10/objects/info/alternates
 '
 
 test_expect_success 'clone checking out a tag' '
@@ -663,8 +663,8 @@
 '
 
 test_expect_success PERL_TEST_HELPERS,CASE_INSENSITIVE_FS 'colliding file detection' '
-	grep X icasefs/warning &&
-	grep x icasefs/warning &&
+	test_grep X icasefs/warning &&
+	test_grep x icasefs/warning &&
 	test_grep "the following paths have collided" icasefs/warning
 '
 
@@ -857,7 +857,7 @@
 	cat >pattern <<-EOF &&
 	"event":"child_start".*"argv":\["git-remote-https","$HTTPD_URL/everything.bundle"\]
 	EOF
-	grep -f pattern trace.txt
+	test_grep -f pattern trace.txt
 '
 
 test_expect_success 'auto-discover multiple bundles from HTTP clone' '
@@ -888,11 +888,11 @@
 	cat >pattern <<-EOF &&
 	"event":"child_start".*"argv":\["git-remote-https","$HTTPD_URL/everything.bundle"\]
 	EOF
-	grep -f pattern trace.txt &&
+	test_grep -f pattern trace.txt &&
 	cat >pattern <<-EOF &&
 	"event":"child_start".*"argv":\["git-remote-https","$HTTPD_URL/new.bundle"\]
 	EOF
-	grep -f pattern trace.txt
+	test_grep -f pattern trace.txt
 '
 
 test_expect_success 'auto-discover multiple bundles from HTTP clone: creationToken heuristic' '
diff --git a/t/t5604-clone-reference.sh b/t/t5604-clone-reference.sh
index c232ab8..39a0c31 100755
--- a/t/t5604-clone-reference.sh
+++ b/t/t5604-clone-reference.sh
@@ -65,7 +65,7 @@
 
 test_expect_success 'fetched no objects' '
 	test -s "$U.D" &&
-	! grep " want" "$U.D"
+	test_grep ! " want" "$U.D"
 '
 
 test_expect_success 'existence of info/alternates' '
@@ -157,9 +157,9 @@
 	) &&
 	main_object=$(git -C A rev-parse --verify refs/heads/main) &&
 	test -s "$U.K" &&
-	! grep " want $main_object" "$U.K" &&
+	test_grep ! " want $main_object" "$U.K" &&
 	tag_object=$(git -C A rev-parse --verify refs/tags/foo) &&
-	! grep " want $tag_object" "$U.K"
+	test_grep ! " want $tag_object" "$U.K"
 '
 
 test_expect_success 'clone using repo with gitfile as a reference' '
@@ -357,7 +357,7 @@
 	test_must_fail git clone --local malicious clone 2>err &&
 
 	test_path_is_missing clone &&
-	grep "is a symlink, refusing to clone with --local" err
+	test_grep "is a symlink, refusing to clone with --local" err
 '
 
 test_expect_success 'dissociate from repo with commit graph' '
diff --git a/t/t5605-clone-local.sh b/t/t5605-clone-local.sh
index 2397f8f..156362f 100755
--- a/t/t5605-clone-local.sh
+++ b/t/t5605-clone-local.sh
@@ -172,7 +172,7 @@
 	echo a >corrupt/.git/refs/heads/topic &&
 
 	test_must_fail git clone corrupt working 2>err &&
-	grep "has neither a valid OID nor a target" err
+	test_grep "has neither a valid OID nor a target" err
 '
 
 test_done
diff --git a/t/t5606-clone-options.sh b/t/t5606-clone-options.sh
index 8a15237..1b48da4 100755
--- a/t/t5606-clone-options.sh
+++ b/t/t5606-clone-options.sh
@@ -63,7 +63,7 @@
 	for option in --depth=1 --shallow-since=01-01-2000 --shallow-exclude=HEAD
 	do
 		test_must_fail git clone --bundle-uri=bundle $option from to 2>err &&
-		grep "bundle-uri.* cannot be used together" err || return 1
+		test_grep "bundle-uri.* cannot be used together" err || return 1
 	done
 '
 
@@ -147,7 +147,7 @@
 test_expect_success 'redirected clone does not show progress' '
 
 	git clone "file://$(pwd)/parent" clone-redirected >out 2>err &&
-	! grep % err &&
+	test_grep ! % err &&
 	test_grep ! "Checking connectivity" err
 
 '
@@ -156,7 +156,7 @@
 
 	git clone --progress "file://$(pwd)/parent" clone-redirected-progress \
 		>out 2>err &&
-	grep % err
+	test_grep % err
 
 '
 
diff --git a/t/t5608-clone-2gb.sh b/t/t5608-clone-2gb.sh
index 4f8a95d..5d56deb 100755
--- a/t/t5608-clone-2gb.sh
+++ b/t/t5608-clone-2gb.sh
@@ -10,45 +10,47 @@
 fi
 
 test_expect_success 'setup' '
-
-	git config pack.compression 0 &&
-	git config pack.depth 0 &&
-	blobsize=$((100*1024*1024)) &&
-	blobcount=$((2*1024*1024*1024/$blobsize+1)) &&
-	i=1 &&
-	(while test $i -le $blobcount
-	 do
-		printf "Generating blob $i/$blobcount\r" >&2 &&
-		printf "blob\nmark :$i\ndata $blobsize\n" &&
-		#test-tool genrandom $i $blobsize &&
-		printf "%-${blobsize}s" $i &&
-		echo "M 100644 :$i $i" >> commit &&
-		i=$(($i+1)) ||
-		echo $? > exit-status
-	 done &&
-	 echo "commit refs/heads/main" &&
-	 echo "author A U Thor <author@email.com> 123456789 +0000" &&
-	 echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
-	 echo "data 5" &&
-	 echo ">2gb" &&
-	 cat commit) |
-	git fast-import --big-file-threshold=2 &&
-	test ! -f exit-status
-
+	git init 2gb-repo &&
+	(
+		cd 2gb-repo &&
+		git config pack.compression 0 &&
+		git config pack.depth 0 &&
+		blobsize=$((100*1024*1024)) &&
+		blobcount=$((2*1024*1024*1024/$blobsize+1)) &&
+		i=1 &&
+		(while test $i -le $blobcount
+		 do
+			printf "Generating blob $i/$blobcount\r" >&2 &&
+			printf "blob\nmark :$i\ndata $blobsize\n" &&
+			#test-tool genrandom $i $blobsize &&
+			printf "%-${blobsize}s" $i &&
+			echo "M 100644 :$i $i" >> commit &&
+			i=$(($i+1)) ||
+			echo $? > exit-status
+		 done &&
+		 echo "commit refs/heads/main" &&
+		 echo "author A U Thor <author@email.com> 123456789 +0000" &&
+		 echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
+		 echo "data 5" &&
+		 echo ">2gb" &&
+		 cat commit) |
+		git fast-import --big-file-threshold=2 &&
+		test ! -f exit-status
+	)
 '
 
 test_expect_success 'clone - bare' '
-
-	git clone --bare --no-hardlinks . clone-bare
-
+	test_when_finished rm -rf clone-bare &&
+	git clone --bare --no-hardlinks 2gb-repo clone-bare
 '
 
 test_expect_success 'clone - with worktree, file:// protocol' '
-
-	git clone "file://$(pwd)" clone-wt
-
+	test_when_finished rm -rf clone-wt &&
+	git clone "file://$(pwd)/2gb-repo" clone-wt
 '
 
+rm -rf 2gb-repo 2>/dev/null
+
 test_expect_success SIZE_T_IS_64BIT,EXPENSIVE 'set up repo with >4GB object' '
 	large_blob_size=$((4*1024*1024*1024+1)) &&
 	git init --bare 4gb-repo &&
@@ -61,6 +63,7 @@
 '
 
 test_expect_success SIZE_T_IS_64BIT,EXPENSIVE 'clone >4GB object via unpack-objects' '
+	test_when_finished rm -rf 4gb-clone-unpack &&
 	# The synthesized pack has five objects, so a large unpack limit keeps
 	# fetch-pack on the unpack-objects path.
 	git -c fetch.unpackLimit=100 clone --bare \
@@ -77,6 +80,7 @@
 '
 
 test_expect_success SIZE_T_IS_64BIT,EXPENSIVE 'clone with >4GB object via index-pack' '
+	test_when_finished rm -rf 4gb-clone-index &&
 	# Force fetch-pack to hand the pack to index-pack instead.
 	git -c fetch.unpackLimit=1 clone --bare \
 		"file://$(pwd)/4gb-repo" 4gb-clone-index &&
diff --git a/t/t5612-clone-refspec.sh b/t/t5612-clone-refspec.sh
index 3126cfd..dd1fe0a 100755
--- a/t/t5612-clone-refspec.sh
+++ b/t/t5612-clone-refspec.sh
@@ -97,7 +97,7 @@
 test_expect_success 'clone with --no-tags' '
 	(
 		cd dir_all_no_tags &&
-		grep tagOpt .git/config &&
+		test_grep tagOpt .git/config &&
 		git fetch &&
 		git for-each-ref refs/tags >../actual
 	) &&
diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh
index 9050520..7cc6f58 100755
--- a/t/t5616-partial-clone.sh
+++ b/t/t5616-partial-clone.sh
@@ -60,8 +60,8 @@
 	ls pc1/.git/objects/pack/pack-*.promisor >promisorlist &&
 	test_line_count = 1 promisorlist &&
 	git -C srv.bare rev-parse --verify HEAD >headhash &&
-	grep "$(cat headhash) HEAD" $(cat promisorlist) &&
-	grep "$(cat headhash) refs/heads/main" $(cat promisorlist)
+	test_grep "$(cat headhash) HEAD" $(cat promisorlist) &&
+	test_grep "$(cat headhash) refs/heads/main" $(cat promisorlist)
 '
 
 # checkout main to force dynamic object fetch of blobs at HEAD.
@@ -254,8 +254,8 @@
 	GIT_TRACE2_EVENT="$PWD/trace1.event" \
 	git -C pc1 fetch --refetch origin &&
 	test_subcommand git maintenance run --auto --no-quiet --no-detach <trace1.event &&
-	grep \"param\":\"gc.autopacklimit\",\"value\":\"1\" trace1.event &&
-	grep \"param\":\"maintenance.incremental-repack.auto\",\"value\":\"-1\" trace1.event &&
+	test_grep \"param\":\"gc.autopacklimit\",\"value\":\"1\" trace1.event &&
+	test_grep \"param\":\"maintenance.incremental-repack.auto\",\"value\":\"-1\" trace1.event &&
 
 	GIT_TRACE2_EVENT="$PWD/trace2.event" \
 	git -c protocol.version=0 \
@@ -263,8 +263,8 @@
 		-c maintenance.incremental-repack.auto=1234 \
 		-C pc1 fetch --refetch origin &&
 	test_subcommand git maintenance run --auto --no-quiet --no-detach <trace2.event &&
-	grep \"param\":\"gc.autopacklimit\",\"value\":\"0\" trace2.event &&
-	grep \"param\":\"maintenance.incremental-repack.auto\",\"value\":\"-1\" trace2.event &&
+	test_grep \"param\":\"gc.autopacklimit\",\"value\":\"0\" trace2.event &&
+	test_grep \"param\":\"maintenance.incremental-repack.auto\",\"value\":\"-1\" trace2.event &&
 
 	GIT_TRACE2_EVENT="$PWD/trace3.event" \
 	git -c protocol.version=0 \
@@ -272,8 +272,8 @@
 		-c maintenance.incremental-repack.auto=0 \
 		-C pc1 fetch --refetch origin &&
 	test_subcommand git maintenance run --auto --no-quiet --no-detach <trace3.event &&
-	grep \"param\":\"gc.autopacklimit\",\"value\":\"1\" trace3.event &&
-	grep \"param\":\"maintenance.incremental-repack.auto\",\"value\":\"0\" trace3.event
+	test_grep \"param\":\"gc.autopacklimit\",\"value\":\"1\" trace3.event &&
+	test_grep \"param\":\"maintenance.incremental-repack.auto\",\"value\":\"0\" trace3.event
 '
 
 test_expect_success 'partial clone with transfer.fsckobjects=1 works with submodules' '
@@ -308,7 +308,7 @@
 
 	GIT_TRACE="$(pwd)/trace" git -c transfer.fsckobjects=1 \
 		clone --filter="blob:none" "file://$(pwd)/src" dst &&
-	grep "git index-pack.*--fsck-objects" trace
+	test_grep "git index-pack.*--fsck-objects" trace
 '
 
 test_expect_success 'use fsck before and after manually fetching a missing subtree' '
@@ -336,7 +336,7 @@
 
 	# Auto-fetch a tree with cat-file.
 	git -C dst cat-file -p $SUBTREE >tree_contents &&
-	grep file.txt tree_contents &&
+	test_grep file.txt tree_contents &&
 
 	# fsck still works after an auto-fetch of a tree.
 	git -C dst fsck &&
@@ -357,14 +357,14 @@
 	GIT_TRACE=$(pwd)/trace git clone --bare \
 		--filter=blob:none --filter=tree:1 \
 		"file://$(pwd)/srv.bare" pc2 &&
-	grep "trace:.* git pack-objects .*--filter=combine:blob:none+tree:1" \
+	test_grep "trace:.* git pack-objects .*--filter=combine:blob:none+tree:1" \
 		trace &&
 	git -C pc2 rev-list --objects --missing=allow-any HEAD >objects &&
 
 	# We should have gotten some root trees.
-	grep " $" objects &&
+	test_grep " $" objects &&
 	# Should not have gotten any non-root trees or blobs.
-	! grep " ." objects &&
+	test_grep ! " ." objects &&
 
 	xargs -n 1 git -C pc2 cat-file -t <objects >types &&
 	sort -u types >unique_types.actual &&
@@ -433,7 +433,7 @@
 	git -C src tag myblob "$BLOB" &&
 
 	git clone --filter="blob:none" "file://$(pwd)/src" dst 2>err &&
-	! grep "does not point to a valid object" err &&
+	test_grep ! "does not point to a valid object" err &&
 	git -C dst fsck
 '
 
@@ -448,10 +448,10 @@
 
 	git clone --bare --filter=blob:none "file://$(pwd)/src" dst.git &&
 	git -C dst.git rev-list --objects --quiet --missing=print HEAD >missing_before &&
-	grep "?$(cat blob)" missing_before &&
+	test_grep "?$(cat blob)" missing_before &&
 	git -C dst.git fetch origin $(cat blob) &&
 	git -C dst.git rev-list --objects --quiet --missing=print HEAD >missing_after &&
-	! grep "?$(cat blob)" missing_after
+	test_grep ! "?$(cat blob)" missing_after
 '
 
 test_expect_success 'setup src repo for sparse filter' '
@@ -473,8 +473,8 @@
 	(
 		cd dst.git &&
 		git rev-list --objects --missing=print HEAD >out &&
-		grep "^$(git rev-parse HEAD:one.t)" out &&
-		grep "^?$(git rev-parse HEAD:two.t)" out
+		test_grep "^$(git rev-parse HEAD:one.t)" out &&
+		test_grep "^?$(git rev-parse HEAD:two.t)" out
 	)
 '
 
@@ -545,7 +545,7 @@
 	# Verify the assumption that the client needed to fetch the delta base
 	# to resolve the delta.
 	git -C server rev-parse HEAD~1^{tree} >hash &&
-	grep "want $(cat hash)" trace
+	test_grep "want $(cat hash)" trace
 '
 
 test_expect_success 'fetch lazy-fetches only to resolve deltas, protocol v2' '
@@ -562,12 +562,12 @@
 		fetch "file://$(pwd)/server" main &&
 
 	# Verify that protocol version 2 was used.
-	grep "fetch< version 2" trace &&
+	test_grep "fetch< version 2" trace &&
 
 	# Verify the assumption that the client needed to fetch the delta base
 	# to resolve the delta.
 	git -C server rev-parse HEAD~1^{tree} >hash &&
-	grep "want $(cat hash)" trace
+	test_grep "want $(cat hash)" trace
 '
 
 test_expect_success 'fetch does not lazy-fetch missing targets of its refs' '
@@ -587,7 +587,7 @@
 		--no-tags --recurse-submodules=no \
 		origin refs/tags/bar &&
 	FOO_HASH=$(git -C server rev-parse foo) &&
-	! grep "want $FOO_HASH" trace
+	test_grep ! "want $FOO_HASH" trace
 '
 
 # The following two tests must be in this order. It is important that
@@ -645,7 +645,7 @@
 	test_config -C client protocol.version 0 &&
 	test_commit -C client bar &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch "file://$(pwd)/server" &&
-	! grep "version 2" trace
+	test_grep ! "version 2" trace
 '
 
 test_expect_success 'fetch from a partial clone, protocol v2' '
@@ -664,7 +664,7 @@
 	test_config -C client protocol.version 2 &&
 	test_commit -C client bar &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch "file://$(pwd)/server" &&
-	grep "version 2" trace
+	test_grep "version 2" trace
 '
 
 test_expect_success 'repack does not loosen promisor objects' '
@@ -672,7 +672,7 @@
 	git clone --bare --filter=blob:none "file://$(pwd)/srv.bare" client &&
 	test_when_finished "rm -rf client trace" &&
 	GIT_TRACE2_PERF="$(pwd)/trace" git -C client repack -A -d &&
-	grep "loosen_unused_packed_objects/loosened:0" trace
+	test_grep "loosen_unused_packed_objects/loosened:0" trace
 '
 
 test_expect_success 'lazy-fetch in submodule succeeds' '
@@ -848,7 +848,7 @@
 	# Exercise to make sure it works.
 	git -c protocol.version=2 clone \
 		--filter=blob:none $HTTPD_URL/one_time_script/server repo 2> err &&
-	! grep "missing object referenced by" err &&
+	test_grep ! "missing object referenced by" err &&
 
 	# Ensure that the one-time-script script was used.
 	! test -e "$HTTPD_ROOT_PATH/one-time-script"
@@ -905,13 +905,13 @@
 	# by any 3 nybbles, then the OID of the delta base.
 	printf "f.,..%s" $(intersperse "," <deltabase_missing) >want &&
 	hex_unpack <thin.pack | intersperse "," >have &&
-	grep $(cat want) have &&
+	test_grep $(cat want) have &&
 
 	# Ensure that the pack contains one delta against HEAD^:have.txt,
 	# similar to the above.
 	printf "f.,..%s" $(intersperse "," <deltabase_have) >want &&
 	hex_unpack <thin.pack | intersperse "," >have &&
-	grep $(cat want) have &&
+	test_grep $(cat want) have &&
 
 	replace_packfile thin.pack &&
 
@@ -925,8 +925,8 @@
 
 	# Ensure that the missing delta base was directly fetched, but not the
 	# one that the client has.
-	grep "want $(cat deltabase_missing)" trace &&
-	! grep "want $(cat deltabase_have)" trace &&
+	test_grep "want $(cat deltabase_missing)" trace &&
+	test_grep ! "want $(cat deltabase_have)" trace &&
 
 	# Ensure that the one-time-script script was used.
 	! test -e "$HTTPD_ROOT_PATH/one-time-script"
diff --git a/t/t5619-clone-local-ambiguous-transport.sh b/t/t5619-clone-local-ambiguous-transport.sh
index cce62bf..e4218e5 100755
--- a/t/t5619-clone-local-ambiguous-transport.sh
+++ b/t/t5619-clone-local-ambiguous-transport.sh
@@ -64,7 +64,7 @@
 	#
 	# This works for now, and if we ever fix the URL detection, it
 	# is OK to change this to detect the transport error.
-	grep "protocol .* is not supported" err
+	test_grep "protocol .* is not supported" err
 '
 
 test_done
diff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh
index d2ea68e..7462280 100755
--- a/t/t5620-backfill.sh
+++ b/t/t5620-backfill.sh
@@ -144,7 +144,7 @@
 test_expect_success 'backfill --sparse without sparse-checkout fails' '
 	git init not-sparse &&
 	test_must_fail git -C not-sparse backfill --sparse 2>err &&
-	grep "problem loading sparse-checkout" err
+	test_grep "problem loading sparse-checkout" err
 '
 
 test_expect_success 'backfill --sparse' '
@@ -422,7 +422,7 @@
 		-C backfill-log log -p HEAD~2..HEAD >log-output &&
 
 	# No promisor fetches should have been needed.
-	! grep "fetch_count" log-trace
+	test_grep ! "fetch_count" log-trace
 '
 
 test_expect_success 'backfill range without include edges causes on-demand fetches in git-log' '
@@ -439,7 +439,7 @@
 	GIT_TRACE2_EVENT="$(pwd)/log-no-bdy-trace" git \
 		-C backfill-log-no-bdy log -p HEAD~2..HEAD >log-output &&
 
-	grep "fetch_count" log-no-bdy-trace
+	test_grep "fetch_count" log-no-bdy-trace
 '
 
 test_expect_success 'backfill range enables fetch-free replay' '
@@ -470,7 +470,7 @@
 	GIT_TRACE2_EVENT="$(pwd)/replay-trace" git -C replay-dest.git \
 		replay --onto main topic~1..topic >replay-out &&
 
-	! grep "fetch_count" replay-trace
+	test_grep ! "fetch_count" replay-trace
 '
 
 test_expect_success 'backfill enables fetch-free merge' '
@@ -501,7 +501,7 @@
 	GIT_TRACE2_EVENT="$(pwd)/merge-trace" git -C merge-dest \
 		merge origin/side -m "test merge" &&
 
-	! grep "fetch_count" merge-trace
+	test_grep ! "fetch_count" merge-trace
 '
 
 . "$TEST_DIRECTORY"/lib-httpd.sh
@@ -530,7 +530,7 @@
 	awk "{print \$1;}" <rev-list-out >oids &&
 	GIT_TRACE2_EVENT="$(pwd)/walk-trace" git -C backfill-http \
 		cat-file --batch-check <oids >batch-out &&
-	! grep missing batch-out
+	test_grep ! missing batch-out
 '
 
 # DO NOT add non-httpd-specific tests here, because the last part of this
diff --git a/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh
index db3b8cf..5478942 100755
--- a/t/t5621-clone-revision.sh
+++ b/t/t5621-clone-revision.sh
@@ -90,6 +90,14 @@
 	test_must_fail git -C dst config remote.origin.fetch
 '
 
+test_expect_success 'clone with --revision and protocol v0' '
+	test_when_finished "rm -rf dst" &&
+	git -c protocol.version=0 clone --no-local --revision=refs/heads/main . dst &&
+	git rev-parse refs/heads/main >expect &&
+	git -C dst rev-parse HEAD >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'clone with --revision being a short raw commit hash' '
 	test_when_finished "rm -rf dst" &&
 	oid=$(git rev-parse --short refs/heads/feature) &&
diff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh
index a73b4d4..ffeb01a 100755
--- a/t/t5700-protocol-v1.sh
+++ b/t/t5700-protocol-v1.sh
@@ -33,9 +33,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v1
-	grep "clone> .*\\\0\\\0version=1\\\0$" log &&
+	test_grep "clone> .*\\\0\\\0version=1\\\0$" log &&
 	# Server responded using protocol v1
-	grep "clone< version 1" log
+	test_grep "clone< version 1" log
 '
 
 test_expect_success 'fetch with git:// using protocol v1' '
@@ -49,9 +49,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v1
-	grep "fetch> .*\\\0\\\0version=1\\\0$" log &&
+	test_grep "fetch> .*\\\0\\\0version=1\\\0$" log &&
 	# Server responded using protocol v1
-	grep "fetch< version 1" log
+	test_grep "fetch< version 1" log
 '
 
 test_expect_success 'pull with git:// using protocol v1' '
@@ -63,9 +63,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v1
-	grep "fetch> .*\\\0\\\0version=1\\\0$" log &&
+	test_grep "fetch> .*\\\0\\\0version=1\\\0$" log &&
 	# Server responded using protocol v1
-	grep "fetch< version 1" log
+	test_grep "fetch< version 1" log
 '
 
 test_expect_success 'push with git:// using protocol v1' '
@@ -81,9 +81,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v1
-	grep "push> .*\\\0\\\0version=1\\\0$" log &&
+	test_grep "push> .*\\\0\\\0version=1\\\0$" log &&
 	# Server responded using protocol v1
-	grep "push< version 1" log
+	test_grep "push< version 1" log
 '
 
 stop_git_daemon
@@ -104,7 +104,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "clone< version 1" log
+	test_grep "clone< version 1" log
 '
 
 test_expect_success 'fetch with file:// using protocol v1' '
@@ -118,7 +118,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "fetch< version 1" log
+	test_grep "fetch< version 1" log
 '
 
 test_expect_success 'pull with file:// using protocol v1' '
@@ -130,7 +130,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "fetch< version 1" log
+	test_grep "fetch< version 1" log
 '
 
 test_expect_success 'push with file:// using protocol v1' '
@@ -146,7 +146,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "push< version 1" log
+	test_grep "push< version 1" log
 '
 
 test_expect_success 'cloning branchless tagless but not refless remote' '
@@ -196,7 +196,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "clone< version 1" log
+	test_grep "clone< version 1" log
 '
 
 test_expect_success 'fetch with ssh:// using protocol v1' '
@@ -211,7 +211,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "fetch< version 1" log
+	test_grep "fetch< version 1" log
 '
 
 test_expect_success 'pull with ssh:// using protocol v1' '
@@ -224,7 +224,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "fetch< version 1" log
+	test_grep "fetch< version 1" log
 '
 
 test_expect_success 'push with ssh:// using protocol v1' '
@@ -241,7 +241,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "push< version 1" log
+	test_grep "push< version 1" log
 '
 
 test_expect_success 'clone propagates object-format from empty repo' '
@@ -277,9 +277,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v1
-	grep "Git-Protocol: version=1" log &&
+	test_grep "Git-Protocol: version=1" log &&
 	# Server responded using protocol v1
-	grep "git< version 1" log
+	test_grep "git< version 1" log
 '
 
 test_expect_success 'clone with http:// using protocol v1 with empty SHA-256 repo' '
@@ -291,9 +291,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v1
-	grep "Git-Protocol: version=1" log &&
+	test_grep "Git-Protocol: version=1" log &&
 	# Server responded using protocol v1
-	grep "git< version 1" log
+	test_grep "git< version 1" log
 '
 
 test_expect_success 'fetch with http:// using protocol v1' '
@@ -307,7 +307,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "git< version 1" log
+	test_grep "git< version 1" log
 '
 
 test_expect_success 'pull with http:// using protocol v1' '
@@ -319,7 +319,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "git< version 1" log
+	test_grep "git< version 1" log
 '
 
 test_expect_success 'push with http:// using protocol v1' '
@@ -335,7 +335,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v1
-	grep "git< version 1" log
+	test_grep "git< version 1" log
 '
 
 # DO NOT add non-httpd-specific tests here, because the last part of this
diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh
index d4c28ba..9a575aa 100755
--- a/t/t5701-git-serve.sh
+++ b/t/t5701-git-serve.sh
@@ -7,6 +7,8 @@
 
 . ./test-lib.sh
 
+unknown_oid=$(printf "test" | git hash-object --stdin)
+
 test_expect_success 'setup to generate files with expected content' '
 	printf "agent=git/%s" "$(git version | cut -d" " -f3)" >agent_capability &&
 
@@ -97,7 +99,7 @@
 	0000
 	EOF
 	test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
-	grep invalid.command.*agent err
+	test_grep invalid.command.*agent err
 '
 
 test_expect_success 'request command as capability' '
@@ -108,7 +110,7 @@
 	0000
 	EOF
 	test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
-	grep unknown.capability err
+	test_grep unknown.capability err
 '
 
 test_expect_success 'requested command is command=value' '
@@ -118,7 +120,7 @@
 	0000
 	EOF
 	test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
-	grep invalid.command.*ls-refs=whatever err
+	test_grep invalid.command.*ls-refs=whatever err
 '
 
 test_expect_success 'wrong object-format' '
@@ -175,7 +177,7 @@
 	EOF
 
 	test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
-	grep unexpected.line.*no-such-arg err
+	test_grep unexpected.line.*no-such-arg err
 '
 
 test_expect_success 'basic ref-prefixes' '
@@ -334,7 +336,7 @@
 		cd server &&
 		test_must_fail test-tool serve-v2 --stateless-rpc
 	) <in >/dev/null 2>err &&
-	grep "unexpected line: .this-is-not-a-command." err
+	test_grep "unexpected line: .this-is-not-a-command." err
 '
 
 # Test the basics of object-info
@@ -364,6 +366,67 @@
 	test_cmp expect actual
 '
 
+test_expect_success 'bare OID request' '
+	test_config transfer.advertiseObjectInfo true &&
+
+	test-tool pkt-line pack >in <<-EOF &&
+	command=object-info
+	object-format=$(test_oid algo)
+	0001
+	oid $(git rev-parse two:two.t)
+	0000
+	EOF
+
+	cat >expect <<-EOF &&
+	$(git rev-parse two:two.t)
+	0000
+	EOF
+
+	test-tool serve-v2 --stateless-rpc <in >out &&
+	test-tool pkt-line unpack <out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'object-info with bare unrecognized OID' '
+	test_config transfer.advertiseObjectInfo true &&
+
+	test-tool pkt-line pack >in <<-EOF &&
+	command=object-info
+	object-format=$(test_oid algo)
+	0001
+	oid $unknown_oid
+	0000
+	EOF
+
+	printf "%s \n" "$unknown_oid" >expect &&
+	printf "0000\n" >>expect &&
+
+	test-tool serve-v2 --stateless-rpc <in >out &&
+	test-tool pkt-line unpack <out >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'object-info with size for unrecognized OID' '
+	test_config transfer.advertiseObjectInfo true &&
+
+	test-tool pkt-line pack >in <<-EOF &&
+	command=object-info
+	object-format=$(test_oid algo)
+	0001
+	size
+	oid $unknown_oid
+	0000
+	EOF
+
+	printf "size\n" >expect &&
+	printf "%s \n" "$unknown_oid" >>expect &&
+	printf "0000\n" >>expect &&
+
+	test-tool serve-v2 --stateless-rpc <in >out &&
+	test-tool pkt-line unpack <out >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'test capability advertisement with uploadpack.advertiseBundleURIs' '
 	test_config uploadpack.advertiseBundleURIs true &&
 
@@ -410,7 +473,7 @@
 		--advertise-capabilities >out &&
 	test-tool pkt-line unpack <out >actual &&
 
-	! grep object.info actual
+	test_grep ! object.info actual
 '
 
 test_expect_success 'object-info commands rejected when disabled' '
@@ -421,7 +484,7 @@
 	EOF
 
 	test_must_fail test-tool serve-v2 --stateless-rpc <in 2>err &&
-	grep invalid.command err
+	test_grep invalid.command err
 '
 
 test_done
diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh
index 9f6cf41..74a2b77 100755
--- a/t/t5702-protocol-v2.sh
+++ b/t/t5702-protocol-v2.sh
@@ -27,9 +27,9 @@
 		ls-remote --symref "$GIT_DAEMON_URL/parent" >actual &&
 
 	# Client requested to use protocol v2
-	grep "ls-remote> .*\\\0\\\0version=2\\\0$" log &&
+	test_grep "ls-remote> .*\\\0\\\0version=2\\\0$" log &&
 	# Server responded using protocol v2
-	grep "ls-remote< version 2" log &&
+	test_grep "ls-remote< version 2" log &&
 
 	git ls-remote --symref "$GIT_DAEMON_URL/parent" >expect &&
 	test_cmp expect actual
@@ -59,9 +59,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v2
-	grep "clone> .*\\\0\\\0version=2\\\0$" log &&
+	test_grep "clone> .*\\\0\\\0version=2\\\0$" log &&
 	# Server responded using protocol v2
-	grep "clone< version 2" log
+	test_grep "clone< version 2" log
 '
 
 test_expect_success 'fetch with git:// using protocol v2' '
@@ -77,9 +77,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v2
-	grep "fetch> .*\\\0\\\0version=2\\\0$" log &&
+	test_grep "fetch> .*\\\0\\\0version=2\\\0$" log &&
 	# Server responded using protocol v2
-	grep "fetch< version 2" log
+	test_grep "fetch< version 2" log
 '
 
 test_expect_success 'fetch by hash without tag following with protocol v2 does not list refs' '
@@ -91,8 +91,8 @@
 	GIT_TRACE_PACKET="$(pwd)/log" git -C daemon_child -c protocol.version=2 \
 		fetch --no-tags origin $(cat two_a_hash) &&
 
-	grep "fetch< version 2" log &&
-	! grep "fetch> command=ls-refs" log
+	test_grep "fetch< version 2" log &&
+	test_grep ! "fetch> command=ls-refs" log
 '
 
 test_expect_success 'pull with git:// using protocol v2' '
@@ -106,9 +106,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v2
-	grep "fetch> .*\\\0\\\0version=2\\\0$" log &&
+	test_grep "fetch> .*\\\0\\\0version=2\\\0$" log &&
 	# Server responded using protocol v2
-	grep "fetch< version 2" log
+	test_grep "fetch< version 2" log
 '
 
 test_expect_success 'push with git:// and a config of v2 does not request v2' '
@@ -130,9 +130,9 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v2
-	! grep "push> .*\\\0\\\0version=2\\\0$" log &&
+	test_grep ! "push> .*\\\0\\\0version=2\\\0$" log &&
 	# Server responded using protocol v2
-	! grep "push< version 2" log
+	test_grep ! "push< version 2" log
 '
 
 stop_git_daemon
@@ -151,7 +151,7 @@
 		ls-remote --symref "file://$(pwd)/file_parent" >actual &&
 
 	# Server responded using protocol v2
-	grep "ls-remote< version 2" log &&
+	test_grep "ls-remote< version 2" log &&
 
 	git ls-remote --symref "file://$(pwd)/file_parent" >expect &&
 	test_cmp expect actual
@@ -181,8 +181,8 @@
 	EOF
 
 	test_cmp expect actual &&
-	grep "server-option=hello" log &&
-	grep "server-option=world" log
+	test_grep "server-option=hello" log &&
+	test_grep "server-option=world" log
 '
 
 test_expect_success 'server-options from configuration are used by ls-remote' '
@@ -241,12 +241,12 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v2
-	grep "clone< version 2" log &&
+	test_grep "clone< version 2" log &&
 
 	# Client sent ref-prefixes to filter the ref-advertisement
-	grep "ref-prefix HEAD" log &&
-	grep "ref-prefix refs/heads/" log &&
-	grep "ref-prefix refs/tags/" log
+	test_grep "ref-prefix HEAD" log &&
+	test_grep "ref-prefix refs/heads/" log &&
+	test_grep "ref-prefix refs/tags/" log
 '
 
 test_expect_success 'clone of empty repo propagates name of default branch' '
@@ -311,7 +311,7 @@
 	echo "refs/heads/mydefaultbranch" >expect &&
 	git -C file_unborn_child symbolic-ref HEAD >actual &&
 	test_cmp expect actual &&
-	grep "warning: remote HEAD refers to nonexistent ref" stderr
+	test_grep "warning: remote HEAD refers to nonexistent ref" stderr
 '
 
 test_expect_success 'clone propagates object-format from empty repo' '
@@ -343,7 +343,7 @@
 	echo "refs/heads/mydefaultbranch" >expect &&
 	git -C file_unborn_child.git symbolic-ref HEAD >actual &&
 	test_cmp expect actual &&
-	! grep "warning:" stderr
+	test_grep ! "warning:" stderr
 '
 
 test_expect_success 'defaulted HEAD uses remote branch if available' '
@@ -366,7 +366,7 @@
 	git -C file_unborn_child symbolic-ref HEAD >actual &&
 	test_cmp expect actual &&
 	test_path_is_file file_unborn_child/stuff.t &&
-	! grep "warning:" stderr
+	test_grep ! "warning:" stderr
 '
 
 test_expect_success 'fetch with file:// using protocol v2' '
@@ -382,7 +382,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v2
-	grep "fetch< version 2" log
+	test_grep "fetch< version 2" log
 '
 
 test_expect_success 'ref advertisement is filtered during fetch using protocol v2' '
@@ -398,8 +398,8 @@
 	git -C file_parent log -1 --format=%s >expect &&
 	test_cmp expect actual &&
 
-	grep "refs/heads/main" log &&
-	! grep "refs/heads/unwanted-branch" log
+	test_grep "refs/heads/main" log &&
+	test_grep ! "refs/heads/unwanted-branch" log
 '
 
 test_expect_success 'server-options are sent when fetching' '
@@ -414,8 +414,8 @@
 	git -C file_parent log -1 --format=%s >expect &&
 	test_cmp expect actual &&
 
-	grep "server-option=hello" log &&
-	grep "server-option=world" log
+	test_grep "server-option=hello" log &&
+	test_grep "server-option=world" log
 '
 
 test_expect_success 'server-options are sent when fetch multiple remotes' '
@@ -485,8 +485,8 @@
 		clone --server-option=hello --server-option=world \
 		"file://$(pwd)/file_parent" myclone &&
 
-	grep "server-option=hello" log &&
-	grep "server-option=world" log
+	test_grep "server-option=hello" log &&
+	test_grep "server-option=world" log
 '
 
 test_expect_success 'server-options from configuration are used by git-clone' '
@@ -578,12 +578,12 @@
 test_expect_success 'partial clone' '
 	GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 \
 		clone --filter=blob:none "file://$(pwd)/server" client &&
-	grep "version 2" trace &&
+	test_grep "version 2" trace &&
 
 	# Ensure that the old version of the file is missing
 	git -C client rev-list --quiet --objects --missing=print main \
 		>observed.oids &&
-	grep "$(git -C server rev-parse message1:a.txt)" observed.oids &&
+	test_grep "$(git -C server rev-parse message1:a.txt)" observed.oids &&
 
 	# Ensure that client passes fsck
 	git -C client fsck
@@ -593,11 +593,11 @@
 	rm "$(pwd)/trace" &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
 		cat-file -p $(git -C server rev-parse message1:a.txt) &&
-	grep "version 2" trace
+	test_grep "version 2" trace
 '
 
 test_expect_success 'when dynamically fetching missing object, do not list refs' '
-	! grep "git> command=ls-refs" trace
+	test_grep ! "git> command=ls-refs" trace
 '
 
 test_expect_success 'partial fetch' '
@@ -607,12 +607,12 @@
 
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
 		fetch --filter=blob:none "$SERVER" main:refs/heads/other &&
-	grep "version 2" trace &&
+	test_grep "version 2" trace &&
 
 	# Ensure that the old version of the file is missing
 	git -C client rev-list --quiet --objects --missing=print other \
 		>observed.oids &&
-	grep "$(git -C server rev-parse message1:a.txt)" observed.oids &&
+	test_grep "$(git -C server rev-parse message1:a.txt)" observed.oids &&
 
 	# Ensure that client passes fsck
 	git -C client fsck
@@ -625,14 +625,14 @@
 	git -C server config uploadpack.allowfilter 1 &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 \
 		ls-remote "$SERVER" &&
-	grep "fetch=.*filter" trace &&
+	test_grep "fetch=.*filter" trace &&
 
 	rm "$(pwd)/trace" &&
 	git -C server config uploadpack.allowfilter 0 &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 \
 		ls-remote "$SERVER" &&
 	grep "fetch=" trace >fetch_capabilities &&
-	! grep filter fetch_capabilities
+	test_grep ! filter fetch_capabilities
 '
 
 test_expect_success 'partial clone warns if filter is not advertised' '
@@ -658,7 +658,7 @@
 
 	test_must_fail test-tool -C server serve-v2 --stateless-rpc \
 		<in >/dev/null 2>err &&
-	grep "unexpected line: .filter blob:none." err &&
+	test_grep "unexpected line: .filter blob:none." err &&
 
 	# Exercise to ensure that if advertised, filter works
 	git -C server config uploadpack.allowfilter 1 &&
@@ -675,8 +675,8 @@
 	git -C file_parent log -1 --format=%s three >expect &&
 	test_cmp expect actual &&
 
-	grep "ref-prefix refs/heads/" log &&
-	grep "ref-prefix refs/tags/" log
+	test_grep "ref-prefix refs/heads/" log &&
+	test_grep "ref-prefix refs/tags/" log
 '
 
 test_expect_success 'set up parent for prefix tests' '
@@ -750,9 +750,9 @@
 		"$(git -C server rev-parse fetch-by-sha1)" &&
 
 	# Ensure that the appropriate prefixes are sent (using a sample)
-	grep "fetch> ref-prefix dwim" trace &&
-	grep "fetch> ref-prefix refs/heads/dwim" trace &&
-	grep "fetch> ref-prefix refs/tags/prefix" trace &&
+	test_grep "fetch> ref-prefix dwim" trace &&
+	test_grep "fetch> ref-prefix refs/heads/dwim" trace &&
+	test_grep "fetch> ref-prefix refs/tags/prefix" trace &&
 
 	# Ensure that the correct objects are returned
 	git -C client cat-file -e $(git -C server rev-parse dwim) &&
@@ -779,9 +779,9 @@
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
 		fetch "$(pwd)/server" to_fetch:to_fetch &&
 
-	grep "fetch> ref-prefix to_fetch" trace &&
-	grep "fetch> ref-prefix refs/tags/" trace &&
-	grep "fetch> include-tag" trace &&
+	test_grep "fetch> ref-prefix to_fetch" trace &&
+	test_grep "fetch> ref-prefix refs/tags/" trace &&
+	test_grep "fetch> include-tag" trace &&
 
 	git -C client cat-file -e $(git -C client rev-parse annotated_tag)
 '
@@ -805,7 +805,7 @@
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
 		fetch origin newbranch &&
 	# Ensure that protocol v2 is used
-	grep "fetch< version 2" trace
+	test_grep "fetch< version 2" trace
 '
 
 test_expect_success 'ensure that multiple fetches in same process from a shallow repo works' '
@@ -823,7 +823,7 @@
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
 		fetch --shallow-exclude one origin &&
 	# Ensure that protocol v2 is used
-	grep "fetch< version 2" trace
+	test_grep "fetch< version 2" trace
 '
 
 test_expect_success 'deepen-relative' '
@@ -844,7 +844,7 @@
 	GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
 		fetch --deepen=1 origin &&
 	# Ensure that protocol v2 is used
-	grep "fetch< version 2" trace &&
+	test_grep "fetch< version 2" trace &&
 
 	git -C client log --pretty=tformat:%s origin/main >actual &&
 	cat >expected <<-\EOF &&
@@ -909,7 +909,7 @@
 		--negotiation-tip=$(git -C client rev-parse HEAD) \
 		origin >out &&
 	COMMON=$(git -C "$SERVER" rev-parse two) &&
-	grep "$COMMON" out
+	test_grep "$COMMON" out
 '
 
 test_expect_success 'file:// --negotiate-only with protocol v0' '
@@ -932,7 +932,7 @@
 		--receive-pack="env >../env.trace; git-receive-pack" \
 		origin HEAD:refs/heads/custom-push-test &&
 	test_path_is_file env.trace &&
-	! grep ^GIT_PROTOCOL env.trace
+	test_grep ! ^GIT_PROTOCOL env.trace
 '
 
 test_expect_success 'fetch with custom path does request v2' '
@@ -940,7 +940,7 @@
 	git -C client fetch \
 		--upload-pack="env >../env.trace; git-upload-pack" \
 		origin HEAD &&
-	grep ^GIT_PROTOCOL=version=2 env.trace
+	test_grep ^GIT_PROTOCOL=version=2 env.trace
 '
 
 test_expect_success 'archive with custom path does not request v2' '
@@ -950,7 +950,7 @@
 		--remote=origin \
 		HEAD >/dev/null &&
 	test_path_is_file env.trace &&
-	! grep ^GIT_PROTOCOL env.trace
+	test_grep ! ^GIT_PROTOCOL env.trace
 '
 
 test_expect_success 'reject client packfile-uris if not advertised' '
@@ -994,11 +994,11 @@
 	test_cmp expect actual &&
 
 	# Client requested to use protocol v2
-	grep "Git-Protocol: version=2" log &&
+	test_grep "Git-Protocol: version=2" log &&
 	# Server responded using protocol v2
-	grep "git< version 2" log &&
+	test_grep "git< version 2" log &&
 	# Verify that the chunked encoding sending codepath is NOT exercised
-	! grep "Send header: Transfer-Encoding: chunked" log
+	test_grep ! "Send header: Transfer-Encoding: chunked" log
 '
 
 test_expect_success 'clone repository with http:// using protocol v2 with incomplete pktline length' '
@@ -1011,9 +1011,9 @@
 		clone "$HTTPD_URL/smart/incomplete_length" incomplete_length_child 2>err &&
 
 	# Client requested to use protocol v2
-	grep "Git-Protocol: version=2" log &&
+	test_grep "Git-Protocol: version=2" log &&
 	# Server responded using protocol v2
-	grep "git< version 2" log &&
+	test_grep "git< version 2" log &&
 	# Client reported appropriate failure
 	test_grep "bytes of length header were received" err
 '
@@ -1028,9 +1028,9 @@
 		clone "$HTTPD_URL/smart/incomplete_body" incomplete_body_child 2>err &&
 
 	# Client requested to use protocol v2
-	grep "Git-Protocol: version=2" log &&
+	test_grep "Git-Protocol: version=2" log &&
 	# Server responded using protocol v2
-	grep "git< version 2" log &&
+	test_grep "git< version 2" log &&
 	# Client reported appropriate failure
 	test_grep "bytes of body are still expected" err
 '
@@ -1043,9 +1043,9 @@
 		clone --shallow-since=20151012 "$HTTPD_URL/smart/http_parent" http_child_invalid &&
 
 	# Client requested to use protocol v2
-	grep "Git-Protocol: version=2" log &&
+	test_grep "Git-Protocol: version=2" log &&
 	# Server responded using protocol v2
-	grep "git< version 2" log
+	test_grep "git< version 2" log
 '
 
 test_expect_success 'clone big repository with http:// using protocol v2' '
@@ -1070,11 +1070,11 @@
 		clone "$HTTPD_URL/smart/big" big_child &&
 
 	# Client requested to use protocol v2
-	grep "Git-Protocol: version=2" log &&
+	test_grep "Git-Protocol: version=2" log &&
 	# Server responded using protocol v2
-	grep "git< version 2" log &&
+	test_grep "git< version 2" log &&
 	# Verify that the chunked encoding sending codepath is exercised
-	grep "Send header: Transfer-Encoding: chunked" log
+	test_grep "Send header: Transfer-Encoding: chunked" log
 '
 
 test_expect_success 'fetch with http:// using protocol v2' '
@@ -1090,7 +1090,7 @@
 	test_cmp expect actual &&
 
 	# Server responded using protocol v2
-	grep "git< version 2" log
+	test_grep "git< version 2" log
 '
 
 test_expect_success 'fetch with http:// by hash without tag following with protocol v2 does not list refs' '
@@ -1102,8 +1102,8 @@
 	GIT_TRACE_PACKET="$(pwd)/log" git -C http_child -c protocol.version=2 \
 		fetch --no-tags origin $(cat two_a_hash) &&
 
-	grep "fetch< version 2" log &&
-	! grep "fetch> command=ls-refs" log
+	test_grep "fetch< version 2" log &&
+	test_grep ! "fetch> command=ls-refs" log
 '
 
 test_expect_success 'fetch from namespaced repo respects namespaces' '
@@ -1120,7 +1120,7 @@
 		refs/heads/main:refs/heads/theirs &&
 
 	# Server responded using protocol v2
-	grep "fetch< version 2" log &&
+	test_grep "fetch< version 2" log &&
 
 	git -C "$HTTPD_DOCUMENT_ROOT_PATH/nsrepo" rev-parse one >expect &&
 	git -C http_child rev-parse theirs >actual &&
@@ -1157,9 +1157,9 @@
 	test_cmp expect actual &&
 
 	# Client did not request to use protocol v2
-	! grep "Git-Protocol: version=2" log &&
+	test_grep ! "Git-Protocol: version=2" log &&
 	# Server did not respond using protocol v2
-	! grep "git< version 2" log
+	test_grep ! "git< version 2" log
 '
 
 test_expect_success 'when server sends "ready", expect DELIM' '
@@ -1207,8 +1207,8 @@
 	test_must_fail env GIT_TRACE_PACKET="$(pwd)/log" git -C http_child \
 		-c protocol.version=2 \
 		fetch "$HTTPD_URL/one_time_script/http_parent" 2> err &&
-	grep "fetch< .*acknowledgments" log &&
-	! grep "fetch< .*ready" log &&
+	test_grep "fetch< .*acknowledgments" log &&
+	test_grep ! "fetch< .*ready" log &&
 	test_grep "expected no other sections to be sent after no .ready." err
 '
 
@@ -1247,7 +1247,7 @@
 	do
 		git verify-pack --object-format=$(test_oid algo) --verbose $idx >out &&
 		{
-			grep -E "^[0-9a-f]{16,} " out || :
+			grep -E "^[0-9a-f]{16,} " out || : # lint-ok: data filter
 		} >out.objectlist &&
 		if test_line_count = 1 out.objectlist
 		then
@@ -1446,7 +1446,7 @@
 		-c fetch.uriprotocols=http,https \
 		clone "$HTTPD_URL/smart/http_parent" http_child &&
 
-	grep -F "clone< \\1$(cat packh) $HTTPD_URL/<redacted>" log
+	test_grep -F "clone< \\1$(cat packh) $HTTPD_URL/<redacted>" log
 '
 
 test_expect_success 'packfile-uri path not redacted in trace when GIT_TRACE_REDACT=0' '
@@ -1472,7 +1472,7 @@
 		-c fetch.uriprotocols=http,https \
 		clone "$HTTPD_URL/smart/http_parent" http_child &&
 
-	grep -F "clone< \\1$(cat packh) $HTTPD_URL/dumb/mypack-$(cat packh).pack" log
+	test_grep -F "clone< \\1$(cat packh) $HTTPD_URL/dumb/mypack-$(cat packh).pack" log
 '
 
 test_expect_success 'http:// --negotiate-only' '
@@ -1487,7 +1487,7 @@
 		--negotiation-tip=$(git -C client rev-parse HEAD) \
 		origin >out &&
 	COMMON=$(git -C "$SERVER" rev-parse two) &&
-	grep "$COMMON" out
+	test_grep "$COMMON" out
 '
 
 test_expect_success 'http:// --negotiate-only without wait-for-done support' '
diff --git a/t/t5703-upload-pack-ref-in-want.sh b/t/t5703-upload-pack-ref-in-want.sh
index 249137b..330d049 100755
--- a/t/t5703-upload-pack-ref-in-want.sh
+++ b/t/t5703-upload-pack-ref-in-want.sh
@@ -101,7 +101,7 @@
 
 	test-tool pkt-line pack <pkt >in &&
 	test_must_fail test-tool serve-v2 --stateless-rpc 2>out <in &&
-	grep "unknown ref" out
+	test_grep "unknown ref" out
 '
 
 test_expect_success 'basic want-ref' '
@@ -235,11 +235,11 @@
 		git -C local fetch origin \
 		"$oid":refs/heads/actual &&
 
-	grep \"key\":\"total_rounds\",\"value\":\"2\" trace2 &&
+	test_grep \"key\":\"total_rounds\",\"value\":\"2\" trace2 &&
 	git -C "$REPO" rev-parse "d" >expected &&
 	git -C local rev-parse refs/heads/actual >actual &&
 	test_cmp expected actual &&
-	grep "want $oid" log
+	test_grep "want $oid" log
 '
 
 test_expect_success 'fetching multiple refs' '
@@ -252,8 +252,8 @@
 	git -C "$REPO" rev-parse "main" "baz" >expected &&
 	git -C local rev-parse refs/remotes/origin/main refs/remotes/origin/baz >actual &&
 	test_cmp expected actual &&
-	grep "want-ref refs/heads/main" log &&
-	grep "want-ref refs/heads/baz" log
+	test_grep "want-ref refs/heads/main" log &&
+	test_grep "want-ref refs/heads/baz" log
 '
 
 test_expect_success 'fetching ref and exact OID' '
@@ -268,8 +268,8 @@
 	git -C "$REPO" rev-parse "main" "b" >expected &&
 	git -C local rev-parse refs/remotes/origin/main refs/heads/actual >actual &&
 	test_cmp expected actual &&
-	grep "want $oid" log &&
-	grep "want-ref refs/heads/main" log
+	test_grep "want $oid" log &&
+	test_grep "want-ref refs/heads/main" log
 '
 
 test_expect_success 'fetching with wildcard that does not match any refs' '
@@ -291,8 +291,8 @@
 	git -C "$REPO" rev-parse "o/foo" "o/bar" >expected &&
 	git -C local rev-parse "o/foo" "o/bar" >actual &&
 	test_cmp expected actual &&
-	grep "want-ref refs/heads/o/foo" log &&
-	grep "want-ref refs/heads/o/bar" log
+	test_grep "want-ref refs/heads/o/foo" log &&
+	test_grep "want-ref refs/heads/o/bar" log
 '
 
 REPO="$(pwd)/repo-ns"
@@ -345,7 +345,7 @@
 
 	test_must_fail env GIT_NAMESPACE=ns \
 		test-tool -C "$REPO" serve-v2 --stateless-rpc >out <in &&
-	grep "unknown ref" out
+	test_grep "unknown ref" out
 '
 
 # Cross-check refs/heads/ns-no indeed exists
@@ -381,7 +381,7 @@
 
 	test_must_fail env GIT_NAMESPACE=ns \
 		test-tool -C "$REPO" serve-v2 --stateless-rpc >out <in &&
-	grep "unknown ref" out
+	test_grep "unknown ref" out
 '
 
 # Cross-check refs/heads/hidden indeed exists
diff --git a/t/t5705-session-id-in-capabilities.sh b/t/t5705-session-id-in-capabilities.sh
index ed38c76..ef0db57 100755
--- a/t/t5705-session-id-in-capabilities.sh
+++ b/t/t5705-session-id-in-capabilities.sh
@@ -55,8 +55,8 @@
 		git -c protocol.version=$PROTO -C local fetch \
 			--upload-pack "GIT_TRACE2_EVENT=\"$(pwd)/tr2-server-events\" git-upload-pack" \
 			origin &&
-		grep \"key\":\"server-sid\" tr2-client-events &&
-		grep \"key\":\"client-sid\" tr2-server-events
+		test_grep \"key\":\"server-sid\" tr2-client-events &&
+		test_grep \"key\":\"client-sid\" tr2-server-events
 	'
 
 	test_expect_success "session IDs advertised (push v${PROTO})" '
@@ -68,8 +68,8 @@
 		git -c protocol.version=$PROTO -C local push \
 			--receive-pack "GIT_TRACE2_EVENT=\"$(pwd)/tr2-server-events\" git-receive-pack" \
 			origin HEAD:new-branch &&
-		grep \"key\":\"server-sid\" tr2-client-events &&
-		grep \"key\":\"client-sid\" tr2-server-events
+		test_grep \"key\":\"server-sid\" tr2-client-events &&
+		test_grep \"key\":\"client-sid\" tr2-server-events
 	'
 
 	test_expect_success "client & server log negotiated version (v${PROTO})" '
@@ -79,8 +79,8 @@
 		git -c protocol.version=$PROTO -C local fetch \
 			--upload-pack "GIT_TRACE2_EVENT=\"$(pwd)/tr2-server-events\" git-upload-pack" \
 			origin &&
-		grep \"key\":\"negotiated-version\",\"value\":\"$PROTO\" tr2-client-events &&
-		grep \"key\":\"negotiated-version\",\"value\":\"$PROTO\" tr2-server-events
+		test_grep \"key\":\"negotiated-version\",\"value\":\"$PROTO\" tr2-client-events &&
+		test_grep \"key\":\"negotiated-version\",\"value\":\"$PROTO\" tr2-server-events
 	'
 done
 
diff --git a/t/t5750-bundle-uri-parse.sh b/t/t5750-bundle-uri-parse.sh
index 294f9d9..70ae94c 100755
--- a/t/t5750-bundle-uri-parse.sh
+++ b/t/t5750-bundle-uri-parse.sh
@@ -78,7 +78,7 @@
 	# now until the interface for relative_url() allows for reporting
 	# an error instead of die()ing.
 	test_must_fail test-tool bundle-uri parse-key-values in >actual 2>err &&
-	grep "fatal: cannot strip one component off url" err
+	test_grep "fatal: cannot strip one component off url" err
 '
 
 test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty key or value' '
@@ -283,7 +283,7 @@
 	EOF
 
 	test-tool bundle-uri parse-config expect >actual 2>err &&
-	grep "could not parse bundle list key creationToken with value '\''bogus'\''" err
+	test_grep "could not parse bundle list key creationToken with value '\''bogus'\''" err
 '
 
 test_expect_success 'parse config format: bundle with missing uri' '
@@ -296,7 +296,7 @@
 	EOF
 
 	test_must_fail test-tool bundle-uri parse-config input 2>err &&
-	grep "bundle '\''missing-uri'\'' has no uri" err
+	test_grep "bundle '\''missing-uri'\'' has no uri" err
 '
 
 test_expect_success 'parse config format: bundle with url instead of uri' '
@@ -309,7 +309,7 @@
 	EOF
 
 	test_must_fail test-tool bundle-uri parse-config input 2>err &&
-	grep "bundle '\''typo'\'' has no uri" err
+	test_grep "bundle '\''typo'\'' has no uri" err
 '
 
 test_done
diff --git a/t/t5801-remote-helpers.sh b/t/t5801-remote-helpers.sh
index d218771..5fafc85 100755
--- a/t/t5801-remote-helpers.sh
+++ b/t/t5801-remote-helpers.sh
@@ -306,7 +306,7 @@
 	echo new >>file &&
 	git commit -a -m new &&
 	git push origin new_branch 2> msg &&
-	! grep "\[new branch\]" msg
+	test_grep ! "\[new branch\]" msg
 	)
 '
 
@@ -352,7 +352,7 @@
 	test_must_fail \
 		env PATH="$PWD:$PATH" \
 		git clone broken://example.com/foo.git 2>stderr &&
-	grep aborted stderr
+	test_grep aborted stderr
 '
 
 test_done
diff --git a/t/t5810-proto-disable-local.sh b/t/t5810-proto-disable-local.sh
index 96a2c46..cb3b13f 100755
--- a/t/t5810-proto-disable-local.sh
+++ b/t/t5810-proto-disable-local.sh
@@ -28,7 +28,7 @@
 test_expect_success 'repo names starting with dash are rejected' '
 	rm -f trace.out &&
 	test_must_fail env GIT_TRACE="$PWD/trace.out" git fetch -- -repo.git &&
-	! grep upload-pack trace.out
+	test_grep ! upload-pack trace.out
 '
 
 test_expect_success 'full paths still work' '
diff --git a/t/t5813-proto-disable-ssh.sh b/t/t5813-proto-disable-ssh.sh
index 045e2fe..1800d27 100755
--- a/t/t5813-proto-disable-ssh.sh
+++ b/t/t5813-proto-disable-ssh.sh
@@ -24,7 +24,7 @@
 # simply confirm from its output that it did not run at all.
 test_expect_success 'hostnames starting with dash are rejected' '
 	test_must_fail git clone ssh://-remote/repo.git dash-host 2>stderr &&
-	! grep ^ssh: stderr
+	test_grep ! ^ssh: stderr
 '
 
 test_expect_success 'setup repo with dash' '
@@ -34,7 +34,7 @@
 
 test_expect_success 'repo names starting with dash are rejected' '
 	test_must_fail git clone remote:-repo.git dash-path 2>stderr &&
-	! grep ^ssh: stderr
+	test_grep ! ^ssh: stderr
 '
 
 test_expect_success 'full paths still work' '
diff --git a/t/t6000-rev-list-misc.sh b/t/t6000-rev-list-misc.sh
index a95ba57..ce968f6 100755
--- a/t/t6000-rev-list-misc.sh
+++ b/t/t6000-rev-list-misc.sh
@@ -17,8 +17,8 @@
 
 test_expect_success 'rev-list --objects heeds pathspecs' '
 	git rev-list --objects HEAD -- wanted_file >output &&
-	grep wanted_file output &&
-	! grep unwanted_file output
+	test_grep wanted_file output &&
+	test_grep ! unwanted_file output
 '
 
 test_expect_success 'rev-list --objects with pathspecs and deeper paths' '
@@ -29,11 +29,11 @@
 	git commit -m two &&
 
 	git rev-list --objects HEAD -- foo >output &&
-	grep foo/file output &&
+	test_grep foo/file output &&
 
 	git rev-list --objects HEAD -- foo/file >output &&
-	grep foo/file output &&
-	! grep unwanted_file output
+	test_grep foo/file output &&
+	test_grep ! unwanted_file output
 '
 
 test_expect_success 'rev-list --objects with pathspecs and copied files' '
@@ -49,28 +49,28 @@
 
 	ONE=$(git rev-parse HEAD:one) &&
 	git rev-list --objects HEAD two >output &&
-	grep "$ONE two/three" output &&
-	! grep one output
+	test_grep "$ONE two/three" output &&
+	test_grep ! one output
 '
 
 test_expect_success 'rev-list --objects --no-object-names has no space/names' '
 	git rev-list --objects --no-object-names HEAD >output &&
-	! grep wanted_file output &&
-	! grep unwanted_file output &&
-	! grep " " output
+	test_grep ! wanted_file output &&
+	test_grep ! unwanted_file output &&
+	test_grep ! " " output
 '
 
 test_expect_success 'rev-list --objects --no-object-names works with cat-file' '
 	git rev-list --objects --no-object-names --all >list-output &&
 	git cat-file --batch-check <list-output >cat-output &&
-	! grep missing cat-output
+	test_grep ! missing cat-output
 '
 
 test_expect_success '--no-object-names and --object-names are last-one-wins' '
 	git rev-list --objects --no-object-names --object-names --all >output &&
-	grep wanted_file output &&
+	test_grep wanted_file output &&
 	git rev-list --objects --object-names --no-object-names --all >output &&
-	! grep wanted_file output
+	test_grep ! wanted_file output
 '
 
 test_expect_success 'rev-list A..B and rev-list ^A B are the same' '
diff --git a/t/t6005-rev-list-count.sh b/t/t6005-rev-list-count.sh
index 6cde997..0bef583 100755
--- a/t/t6005-rev-list-count.sh
+++ b/t/t6005-rev-list-count.sh
@@ -18,7 +18,7 @@
 
 test_expect_success '--max-count' '
 	test_must_fail git rev-list --max-count=1q HEAD 2>error &&
-	grep "not an integer" error &&
+	test_grep "not an integer" error &&
 
 	test_stdout_line_count = 0 git rev-list HEAD --max-count=0 &&
 	test_stdout_line_count = 3 git rev-list HEAD --max-count=3 &&
@@ -29,10 +29,10 @@
 
 test_expect_success '--max-count all forms' '
 	test_must_fail git rev-list -1q HEAD 2>error &&
-	grep "not an integer" error &&
+	test_grep "not an integer" error &&
 	test_must_fail git rev-list --1 HEAD &&
 	test_must_fail git rev-list -n 1q HEAD 2>error &&
-	grep "not an integer" error &&
+	test_grep "not an integer" error &&
 
 	test_stdout_line_count = 1 git rev-list HEAD --max-count=1 &&
 	test_stdout_line_count = 1 git rev-list HEAD -1 &&
@@ -43,7 +43,7 @@
 
 test_expect_success '--skip' '
 	test_must_fail git rev-list --skip 1q HEAD 2>error &&
-	grep "not an integer" error &&
+	test_grep "not an integer" error &&
 
 	test_stdout_line_count = 5 git rev-list HEAD --skip=0 &&
 	test_stdout_line_count = 2 git rev-list HEAD --skip=3 &&
diff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh
index 5819844..d2648e1 100755
--- a/t/t6006-rev-list-format.sh
+++ b/t/t6006-rev-list-format.sh
@@ -537,7 +537,7 @@
 test_expect_success 'del LF before empty (2)' '
 	git show -s --pretty=format:"%s%n%-b%nThanks%n" HEAD >actual &&
 	test_line_count = 6 actual &&
-	grep "^$" actual
+	test_grep "^$" actual
 '
 
 test_expect_success 'add LF before non-empty (1)' '
@@ -548,7 +548,7 @@
 test_expect_success 'add LF before non-empty (2)' '
 	git show -s --pretty=format:"%s%+b%nThanks%n" HEAD >actual &&
 	test_line_count = 6 actual &&
-	grep "^$" actual
+	test_grep "^$" actual
 '
 
 test_expect_success 'add SP before non-empty (1)' '
diff --git a/t/t6009-rev-list-parent.sh b/t/t6009-rev-list-parent.sh
index 9c9a845..fd6ee2e 100755
--- a/t/t6009-rev-list-parent.sh
+++ b/t/t6009-rev-list-parent.sh
@@ -64,10 +64,10 @@
 
 test_expect_success 'parse --max-parents & --min-parents' '
 	test_must_fail git rev-list --max-parents=1q HEAD 2>error &&
-	grep "not an integer" error &&
+	test_grep "not an integer" error &&
 
 	test_must_fail git rev-list --min-parents=1q HEAD 2>error &&
-	grep "not an integer" error &&
+	test_grep "not an integer" error &&
 
 	git rev-list --max-parents=1 --min-parents=1 HEAD &&
 	git rev-list --max-parents=-1 --min-parents=-1 HEAD
diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh
index 4cecb62..2284bbb 100755
--- a/t/t6012-rev-list-simplify.sh
+++ b/t/t6012-rev-list-simplify.sh
@@ -285,4 +285,22 @@
 	test_cmp expect actual
 '
 
+test_expect_success 'exclude-first-parent-only with parent already seen' '
+	git checkout --orphan test-seen &&
+	git rm -rf . &&
+	test_commit r1 &&
+	git checkout -b branch-f &&
+	test_commit f &&
+	git checkout test-seen &&
+	git merge --no-ff --no-edit -m r2 branch-f &&
+	git tag r2 &&
+
+	git rev-list --exclude-first-parent-only f ^r2 >actual &&
+	git rev-parse f >expect &&
+	test_cmp expect actual &&
+
+	git rev-list --exclude-first-parent-only f r1 ^r2 >actual2 &&
+	test_cmp expect actual2
+'
+
 test_done
diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh
index 54b0a6f..e0d9c3c 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -13,11 +13,6 @@
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/lib-log-graph.sh
 
-check_graph () {
-	cat >expect &&
-	lib_test_cmp_graph --format=%s "$@"
-}
-
 test_expect_success 'set up rev-list --graph test' '
 	# 3 commits on branch A
 	test_commit A1 foo.txt &&
@@ -54,7 +49,7 @@
 '
 
 test_expect_success '--graph --all' '
-	check_graph --all <<-\EOF
+	lib_test_check_graph --all <<-\EOF
 	* A7
 	*   A6
 	|\
@@ -82,7 +77,7 @@
 # that undecorated merges are interesting, even with --simplify-by-decoration
 test_expect_success '--graph --simplify-by-decoration' '
 	git tag -d A4 &&
-	check_graph --all --simplify-by-decoration <<-\EOF
+	lib_test_check_graph --all --simplify-by-decoration <<-\EOF
 	* A7
 	*   A6
 	|\
@@ -114,7 +109,7 @@
 
 # Graph with branch B simplified away
 test_expect_success '--graph --simplify-by-decoration prune branch B' '
-	check_graph --simplify-by-decoration --all <<-\EOF
+	lib_test_check_graph --simplify-by-decoration --all <<-\EOF
 	* A7
 	*   A6
 	|\
@@ -133,7 +128,7 @@
 '
 
 test_expect_success '--graph --full-history -- bar.txt' '
-	check_graph --full-history --all -- bar.txt <<-\EOF
+	lib_test_check_graph --full-history --all -- bar.txt <<-\EOF
 	* A7
 	*   A6
 	|\
@@ -148,7 +143,7 @@
 '
 
 test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '
-	check_graph --full-history --simplify-merges --all -- bar.txt <<-\EOF
+	lib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\EOF
 	* A7
 	*   A6
 	|\
@@ -161,7 +156,7 @@
 '
 
 test_expect_success '--graph -- bar.txt' '
-	check_graph --all -- bar.txt <<-\EOF
+	lib_test_check_graph --all -- bar.txt <<-\EOF
 	* A7
 	* A5
 	* A3
@@ -172,7 +167,7 @@
 '
 
 test_expect_success '--graph --sparse -- bar.txt' '
-	check_graph --sparse --all -- bar.txt <<-\EOF
+	lib_test_check_graph --sparse --all -- bar.txt <<-\EOF
 	* A7
 	* A6
 	* A5
@@ -189,7 +184,7 @@
 '
 
 test_expect_success '--graph ^C4' '
-	check_graph --all ^C4 <<-\EOF
+	lib_test_check_graph --all ^C4 <<-\EOF
 	* A7
 	* A6
 	* A5
@@ -202,7 +197,7 @@
 '
 
 test_expect_success '--graph ^C3' '
-	check_graph --all ^C3 <<-\EOF
+	lib_test_check_graph --all ^C3 <<-\EOF
 	* A7
 	*   A6
 	|\
@@ -220,7 +215,7 @@
 # that important, but this test depends on it.  If the ordering ever changes
 # in the code, we'll need to update this test.
 test_expect_success '--graph --boundary ^C3' '
-	check_graph --boundary --all ^C3 <<-\EOF
+	lib_test_check_graph --boundary --all ^C3 <<-\EOF
 	* A7
 	*   A6
 	|\
diff --git a/t/t6017-rev-list-stdin.sh b/t/t6017-rev-list-stdin.sh
index 4821b90..32284f1 100755
--- a/t/t6017-rev-list-stdin.sh
+++ b/t/t6017-rev-list-stdin.sh
@@ -148,4 +148,22 @@
 	test_cmp expect actual
 '
 
+test_expect_success '--no-walk filters by path (single commit, match)' '
+	git rev-parse side-1 >expect &&
+	git rev-list --no-walk side-1 -- file-1 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '--no-walk filters by path (single commit, no match)' '
+	git rev-list --no-walk side-2 -- file-1 >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success '--no-walk with pathspec exclusion' '
+	git rev-parse side-3 side-2 >expect &&
+	git rev-parse side-1 side-2 side-3 >input &&
+	git rev-list --stdin --no-walk -- ":!file-1" <input >actual &&
+	test_cmp expect actual
+'
+
 test_done
diff --git a/t/t6020-bundle-misc.sh b/t/t6020-bundle-misc.sh
index 500c81b..939d421 100755
--- a/t/t6020-bundle-misc.sh
+++ b/t/t6020-bundle-misc.sh
@@ -612,7 +612,7 @@
 		--all \
 		--filter=blob:none &&
 	test_must_fail git clone --bare partial.bdl partial 2>err &&
-	grep "cannot clone from filtered bundle" err
+	test_grep "cannot clone from filtered bundle" err
 '
 
 test_expect_success 'verify catches unreachable, broken prerequisites' '
@@ -644,13 +644,13 @@
 		# Verify should fail
 		test_must_fail git bundle verify \
 			../clone-from/tip.bundle 2>err &&
-		grep "some prerequisite commits .* are not connected" err &&
+		test_grep "some prerequisite commits .* are not connected" err &&
 		test_line_count = 1 err &&
 
 		# Unbundling should fail
 		test_must_fail git bundle unbundle \
 			../clone-from/tip.bundle 2>err &&
-		grep "some prerequisite commits .* are not connected" err &&
+		test_grep "some prerequisite commits .* are not connected" err &&
 		test_line_count = 1 err
 	)
 '
@@ -658,7 +658,7 @@
 test_expect_success 'bundle progress includes write phase' '
 	GIT_PROGRESS_DELAY=0 \
 		git bundle create --progress out.bundle --all 2>err &&
-	grep 'Writing' err
+	test_grep 'Writing' err
 '
 
 test_expect_success TTY 'create --quiet disables all bundle progress' '
@@ -670,7 +670,7 @@
 test_expect_success 'bundle progress with --no-quiet' '
 	GIT_PROGRESS_DELAY=0 \
 		git bundle create --no-quiet out.bundle --all 2>err &&
-	grep "%" err
+	test_grep "%" err
 '
 
 test_expect_success 'create bundle with duplicate refnames' '
@@ -730,7 +730,7 @@
 	git bundle create some.bundle HEAD &&
 
 	git bundle verify - <some.bundle 2>err &&
-	grep "<stdin> is okay" err &&
+	test_grep "<stdin> is okay" err &&
 
 	git bundle list-heads some.bundle >expect &&
 	git bundle list-heads - <some.bundle >actual &&
diff --git a/t/t6022-rev-list-missing.sh b/t/t6022-rev-list-missing.sh
index 08e92dd..1e472a4 100755
--- a/t/t6022-rev-list-missing.sh
+++ b/t/t6022-rev-list-missing.sh
@@ -68,7 +68,7 @@
 			allow-any)
 				;;
 			print)
-				grep ?$oid actual.raw &&
+				test_grep ?$oid actual.raw &&
 				echo ?$oid >>expect.raw
 				;;
 			esac &&
@@ -132,7 +132,7 @@
 				allow-any)
 					;;
 				print)
-					grep ?$oid actual.raw &&
+					test_grep ?$oid actual.raw &&
 					echo ?$oid >>expect.raw
 					;;
 				esac &&
diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh
index 0811162..338df4f 100755
--- a/t/t6030-bisect-porcelain.sh
+++ b/t/t6030-bisect-porcelain.sh
@@ -118,8 +118,8 @@
 test_expect_success 'bisect start without -- takes unknown arg as pathspec' '
 	git bisect reset &&
 	git bisect start foo bar &&
-	grep foo ".git/BISECT_NAMES" &&
-	grep bar ".git/BISECT_NAMES"
+	test_grep foo ".git/BISECT_NAMES" &&
+	test_grep bar ".git/BISECT_NAMES"
 '
 
 test_expect_success 'bisect reset: back in a branch checked out also elsewhere' '
@@ -258,7 +258,7 @@
 	git bisect start $HASH4 $HASH1 &&
 	git bisect skip &&
 	git bisect bad > my_bisect_log.txt &&
-	grep "$HASH2 is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep "$HASH2 is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 # $HASH1 is good, $HASH4 is bad, we skip $HASH3 and $HASH2
@@ -269,11 +269,11 @@
 	git bisect start $HASH4 $HASH1 &&
 	git bisect skip &&
 	test_expect_code 2 git bisect skip >my_bisect_log.txt &&
-	grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
-	! grep $HASH1 my_bisect_log.txt &&
-	grep $HASH2 my_bisect_log.txt &&
-	grep $HASH3 my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt
+	test_grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
+	test_grep ! $HASH1 my_bisect_log.txt &&
+	test_grep $HASH2 my_bisect_log.txt &&
+	test_grep $HASH3 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt
 '
 
 # $HASH1 is good, $HASH4 is bad, we skip $HASH3
@@ -285,11 +285,11 @@
 	git bisect start $HASH4 $HASH1 &&
 	git bisect skip &&
 	test_expect_code 2 git bisect good >my_bisect_log.txt &&
-	grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
-	! grep $HASH1 my_bisect_log.txt &&
-	! grep $HASH2 my_bisect_log.txt &&
-	grep $HASH3 my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt
+	test_grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
+	test_grep ! $HASH1 my_bisect_log.txt &&
+	test_grep ! $HASH2 my_bisect_log.txt &&
+	test_grep $HASH3 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt
 '
 
 # $HASH1 is good, $HASH4 is both skipped and bad, we skip $HASH3
@@ -304,11 +304,11 @@
 	git bisect good $HASH1 &&
 	git bisect skip &&
 	test_expect_code 2 git bisect good >my_bisect_log.txt &&
-	grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
-	! grep $HASH1 my_bisect_log.txt &&
-	! grep $HASH2 my_bisect_log.txt &&
-	grep $HASH3 my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt
+	test_grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
+	test_grep ! $HASH1 my_bisect_log.txt &&
+	test_grep ! $HASH2 my_bisect_log.txt &&
+	test_grep $HASH3 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt
 '
 
 test_bisect_run_args () {
@@ -439,7 +439,7 @@
 	git bisect good $HASH1 &&
 	git bisect bad $HASH4 &&
 	git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt &&
+	test_grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt &&
 	git bisect reset
 '
 
@@ -449,7 +449,7 @@
 	git bisect good $HASH1 &&
 	git bisect bad $HASH4 &&
 	git bisect run printf "%s %s\n" reset --bisect-skip >my_bisect_log.txt &&
-	grep -e "reset --bisect-skip" my_bisect_log.txt &&
+	test_grep -e "reset --bisect-skip" my_bisect_log.txt &&
 	git bisect reset
 '
 
@@ -461,7 +461,7 @@
 	EOF
 	git bisect start $HASH4 $HASH1 &&
 	git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "$HASH4 is the first '\''bad'\'' commit" my_bisect_log.txt &&
+	test_grep "$HASH4 is the first '\''bad'\'' commit" my_bisect_log.txt &&
 	git bisect reset
 '
 
@@ -474,7 +474,7 @@
 	git bisect good $HASH1 &&
 	git bisect bad $HASH4 &&
 	git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 test_expect_success POSIXPERM 'bisect run fails with non-executable test script' '
@@ -485,7 +485,7 @@
 	git bisect good $HASH1 &&
 	git bisect bad $HASH4 &&
 	test_must_fail git bisect run ./not-executable.sh >my_bisect_log.txt &&
-	! grep "is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep ! "is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 test_expect_success 'bisect run accepts exit code 127 as bad' '
@@ -497,7 +497,7 @@
 	git bisect good $HASH1 &&
 	git bisect bad $HASH4 &&
 	git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep "$HASH3 is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 test_expect_success 'bisect run fails with missing test script' '
@@ -507,7 +507,7 @@
 	git bisect good $HASH1 &&
 	git bisect bad $HASH4 &&
 	test_must_fail git bisect run ./does-not-exist.sh >my_bisect_log.txt &&
-	! grep "is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep ! "is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 # $HASH1 is good, $HASH5 is bad, we skip $HASH3
@@ -520,14 +520,14 @@
 	git bisect start $HASH5 $HASH1 &&
 	git bisect skip &&
 	git bisect good > my_bisect_log.txt &&
-	grep "$HASH5 is the first '\''bad'\'' commit" my_bisect_log.txt &&
+	test_grep "$HASH5 is the first '\''bad'\'' commit" my_bisect_log.txt &&
 	git bisect log > log_to_replay.txt &&
 	git bisect reset
 '
 
 test_expect_success 'bisect skip and bisect replay' '
 	git bisect replay log_to_replay.txt > my_bisect_log.txt &&
-	grep "$HASH5 is the first '\''bad'\'' commit" my_bisect_log.txt &&
+	test_grep "$HASH5 is the first '\''bad'\'' commit" my_bisect_log.txt &&
 	git bisect reset
 '
 
@@ -541,11 +541,11 @@
 	EOF
 	git bisect start $HASH6 $HASH1 &&
 	test_expect_code 2 git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
-	! grep $HASH3 my_bisect_log.txt &&
-	! grep $HASH6 my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt &&
-	grep $HASH5 my_bisect_log.txt
+	test_grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt &&
+	test_grep ! $HASH3 my_bisect_log.txt &&
+	test_grep ! $HASH6 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt &&
+	test_grep $HASH5 my_bisect_log.txt
 '
 
 HASH7=
@@ -560,7 +560,7 @@
 	EOF
 	git bisect start $HASH7 $HASH1 &&
 	git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "$HASH6 is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep "$HASH6 is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 test_expect_success 'bisect skip only one range' '
@@ -569,7 +569,7 @@
 	git bisect skip $HASH1..$HASH5 &&
 	test "$HASH6" = "$(git rev-parse --verify HEAD)" &&
 	test_must_fail git bisect bad > my_bisect_log.txt &&
-	grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt
+	test_grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt
 '
 
 test_expect_success 'bisect skip many ranges' '
@@ -578,7 +578,7 @@
 	git bisect skip $HASH2 $HASH2.. ..$HASH5 &&
 	test "$HASH6" = "$(git rev-parse --verify HEAD)" &&
 	test_must_fail git bisect bad > my_bisect_log.txt &&
-	grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt
+	test_grep "first '\''bad'\'' commit could be any of" my_bisect_log.txt
 '
 
 test_expect_success 'bisect starting with a detached HEAD' '
@@ -610,7 +610,7 @@
 	rev_hash6=$(git rev-parse --verify HEAD) &&
 	test "$rev_hash6" = "$HASH6" &&
 	git bisect good > my_bisect_log.txt &&
-	grep "$HASH7 is the first '\''bad'\'' commit" my_bisect_log.txt &&
+	test_grep "$HASH7 is the first '\''bad'\'' commit" my_bisect_log.txt &&
 	git bisect reset &&
 	rev_hash6=$(git rev-parse --verify bisect) &&
 	test "$rev_hash6" = "$HASH6" &&
@@ -637,26 +637,26 @@
 test_expect_success 'good merge base when good and bad are siblings' '
 	git bisect start "$HASH7" "$SIDE_HASH7" > my_bisect_log.txt &&
 	test_grep "merge base must be tested" my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt &&
 	git bisect good > my_bisect_log.txt &&
-	! grep "merge base must be tested" my_bisect_log.txt &&
-	grep $HASH6 my_bisect_log.txt &&
+	test_grep ! "merge base must be tested" my_bisect_log.txt &&
+	test_grep $HASH6 my_bisect_log.txt &&
 	git bisect reset
 '
 test_expect_success 'skipped merge base when good and bad are siblings' '
 	git bisect start "$SIDE_HASH7" "$HASH7" > my_bisect_log.txt &&
 	test_grep "merge base must be tested" my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt &&
 	git bisect skip > my_bisect_log.txt 2>&1 &&
-	grep "warning" my_bisect_log.txt &&
-	grep $SIDE_HASH6 my_bisect_log.txt &&
+	test_grep "warning" my_bisect_log.txt &&
+	test_grep $SIDE_HASH6 my_bisect_log.txt &&
 	git bisect reset
 '
 
 test_expect_success 'bad merge base when good and bad are siblings' '
 	git bisect start "$HASH7" HEAD > my_bisect_log.txt &&
 	test_grep "merge base must be tested" my_bisect_log.txt &&
-	grep $HASH4 my_bisect_log.txt &&
+	test_grep $HASH4 my_bisect_log.txt &&
 	test_must_fail git bisect bad > my_bisect_log.txt 2>&1 &&
 	test_grep "merge base $HASH4 is bad" my_bisect_log.txt &&
 	test_grep "fixed between $HASH4 and \[$SIDE_HASH7\]" my_bisect_log.txt &&
@@ -686,8 +686,8 @@
 	B_HASH=$(git rev-parse --verify HEAD) &&
 	git merge-base --all "$A_HASH" "$B_HASH" > merge_bases.txt &&
 	test_line_count = 2 merge_bases.txt &&
-	grep "$HASH5" merge_bases.txt &&
-	grep "$SIDE_HASH5" merge_bases.txt
+	test_grep "$HASH5" merge_bases.txt &&
+	test_grep "$SIDE_HASH5" merge_bases.txt
 '
 
 # We want to automatically find the merge that
@@ -703,7 +703,7 @@
 	git bisect good $HASH4 &&
 	git bisect bad $B_HASH &&
 	git bisect run ./test_script.sh >my_bisect_log.txt &&
-	grep "$B_HASH is the first '\''bad'\'' commit" my_bisect_log.txt &&
+	test_grep "$B_HASH is the first '\''bad'\'' commit" my_bisect_log.txt &&
 	git bisect reset &&
 	test_path_is_missing .git/BISECT_FIRST_PARENT
 '
@@ -715,11 +715,11 @@
 	test_grep "merge base must be tested" my_bisect_log2.txt &&
 	{
 		{
-			grep "$SIDE_HASH5" my_bisect_log.txt &&
-			grep "$HASH5" my_bisect_log2.txt
+			test_grep "$SIDE_HASH5" my_bisect_log.txt &&
+			test_grep "$HASH5" my_bisect_log2.txt
 		} || {
-			grep "$SIDE_HASH5" my_bisect_log2.txt &&
-			grep "$HASH5" my_bisect_log.txt
+			test_grep "$SIDE_HASH5" my_bisect_log2.txt &&
+			test_grep "$HASH5" my_bisect_log.txt
 		}
 	} &&
 	git bisect reset
@@ -728,7 +728,7 @@
 test_expect_success 'optimized merge base checks' '
 	git bisect start "$HASH7" "$SIDE_HASH7" > my_bisect_log.txt &&
 	test_grep "merge base must be tested" my_bisect_log.txt &&
-	grep "$HASH4" my_bisect_log.txt &&
+	test_grep "$HASH4" my_bisect_log.txt &&
 	git bisect good > my_bisect_log2.txt &&
 	test -f ".git/BISECT_ANCESTORS_OK" &&
 	test "$HASH6" = $(git rev-parse --verify HEAD) &&
@@ -777,7 +777,7 @@
 	para1=$(git rev-parse --verify HEAD) &&
 	test "$para1" = "$PARA_HASH1" &&
 	git bisect bad > my_bisect_log.txt &&
-	grep "$PARA_HASH1 is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep "$PARA_HASH1 is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 test_expect_success 'restricting bisection on one dir and a file' '
@@ -795,7 +795,7 @@
 	para1=$(git rev-parse --verify HEAD) &&
 	test "$para1" = "$PARA_HASH1" &&
 	git bisect good > my_bisect_log.txt &&
-	grep "$PARA_HASH4 is the first '\''bad'\'' commit" my_bisect_log.txt
+	test_grep "$PARA_HASH4 is the first '\''bad'\'' commit" my_bisect_log.txt
 '
 
 test_expect_success 'skipping away from skipped commit' '
@@ -826,7 +826,7 @@
 			"test \$(git rev-list BISECT_HEAD ^$HASH2 --max-count=1 | wc -l) = 0" \
 			>../nocheckout.log
 	) &&
-	grep "$HASH3 is the first '\''bad'\'' commit" nocheckout.log
+	test_grep "$HASH3 is the first '\''bad'\'' commit" nocheckout.log
 '
 
 
@@ -841,7 +841,7 @@
 			"test \$(git rev-list BISECT_HEAD ^$HASH2 --max-count=1 | wc -l) = 0" \
 			>../defaulted.log
 	) &&
-	grep "$HASH3 is the first '\''bad'\'' commit" defaulted.log
+	test_grep "$HASH3 is the first '\''bad'\'' commit" defaulted.log
 '
 
 #
@@ -1031,21 +1031,21 @@
 	git bisect new $HASH4 &&
 	git bisect new &&
 	git bisect new >bisect_result &&
-	grep "$HASH2 is the first '\''new'\'' commit" bisect_result &&
+	test_grep "$HASH2 is the first '\''new'\'' commit" bisect_result &&
 	git bisect log >log_to_replay.txt &&
 	git bisect reset
 '
 
 test_expect_success 'bisect replay with old and new' '
 	git bisect replay log_to_replay.txt >bisect_result &&
-	grep "$HASH2 is the first '\''new'\'' commit" bisect_result &&
+	test_grep "$HASH2 is the first '\''new'\'' commit" bisect_result &&
 	git bisect reset
 '
 
 test_expect_success 'bisect replay with CRLF log' '
 	append_cr <log_to_replay.txt >log_to_replay_crlf.txt &&
 	git bisect replay log_to_replay_crlf.txt >bisect_result_crlf &&
-	grep "$HASH2 is the first '\''new'\'' commit" bisect_result_crlf &&
+	test_grep "$HASH2 is the first '\''new'\'' commit" bisect_result_crlf &&
 	git bisect reset
 '
 
@@ -1092,16 +1092,16 @@
 test_expect_success 'bogus command does not start bisect' '
 	git bisect reset &&
 	test_must_fail git bisect --bisect-terms 1 2 2>out &&
-	! grep "You need to start" out &&
+	test_grep ! "You need to start" out &&
 	test_must_fail git bisect --bisect-terms 2>out &&
-	! grep "You need to start" out &&
-	grep "git bisect.*visualize" out &&
+	test_grep ! "You need to start" out &&
+	test_grep "git bisect.*visualize" out &&
 	git bisect reset
 '
 
 test_expect_success 'bisect replay with term1 and term2' '
 	git bisect replay log_to_replay.txt >bisect_result &&
-	grep "$HASH2 is the first '\''term1'\'' commit" bisect_result &&
+	test_grep "$HASH2 is the first '\''term1'\'' commit" bisect_result &&
 	git bisect reset
 '
 
@@ -1120,7 +1120,7 @@
 	git bisect start --term-new term1 --term-old term2 $HASH4 $HASH1 &&
 	git bisect term1 &&
 	git bisect term1 >bisect_result &&
-	grep "$HASH2 is the first '\''term1'\'' commit" bisect_result &&
+	test_grep "$HASH2 is the first '\''term1'\'' commit" bisect_result &&
 	git bisect log >log_to_replay.txt &&
 	git bisect reset
 '
@@ -1212,7 +1212,7 @@
 	git bisect good tag-one &&
 	git bisect bad tag-two >output &&
 	bad=$(git rev-parse --verify tag-two^{commit}) &&
-	grep "$bad is the first '\''bad'\'' commit" output
+	test_grep "$bad is the first '\''bad'\'' commit" output
 '
 
 test_expect_success 'bisect run fails with exit code equals or greater than 128' '
@@ -1236,29 +1236,29 @@
 test_expect_success 'bisect state output with multiple good commits' '
 	git bisect reset &&
 	git bisect start >output &&
-	grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
+	test_grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
 	git bisect log >output &&
-	grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
+	test_grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
 	git bisect good "$HASH1" >output &&
-	grep "waiting for '\''bad'\'' commit, 1 '\''good'\'' commit known" output &&
+	test_grep "waiting for '\''bad'\'' commit, 1 '\''good'\'' commit known" output &&
 	git bisect log >output &&
-	grep "waiting for '\''bad'\'' commit, 1 '\''good'\'' commit known" output &&
+	test_grep "waiting for '\''bad'\'' commit, 1 '\''good'\'' commit known" output &&
 	git bisect good "$HASH2" >output &&
-	grep "waiting for '\''bad'\'' commit, 2 '\''good'\'' commits known" output &&
+	test_grep "waiting for '\''bad'\'' commit, 2 '\''good'\'' commits known" output &&
 	git bisect log >output &&
-	grep "waiting for '\''bad'\'' commit, 2 '\''good'\'' commits known" output
+	test_grep "waiting for '\''bad'\'' commit, 2 '\''good'\'' commits known" output
 '
 
 test_expect_success 'bisect state output with bad commit' '
 	git bisect reset &&
 	git bisect start >output &&
-	grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
+	test_grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
 	git bisect log >output &&
-	grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
+	test_grep "waiting for both '\''good'\'' and '\''bad'\'' commits" output &&
 	git bisect bad "$HASH4" >output &&
-	grep -F "waiting for '\''good'\'' commit(s), '\''bad'\'' commit known" output &&
+	test_grep -F "waiting for '\''good'\'' commit(s), '\''bad'\'' commit known" output &&
 	git bisect log >output &&
-	grep -F "waiting for '\''good'\'' commit(s), '\''bad'\'' commit known" output
+	test_grep -F "waiting for '\''good'\'' commit(s), '\''bad'\'' commit known" output
 '
 
 test_expect_success 'verify correct error message' '
@@ -1268,7 +1268,7 @@
 	rm .git/BISECT*
 	EOF
 	test_must_fail git bisect run ./test_script.sh 2>error &&
-	grep "git bisect good.*exited with error code" error
+	test_grep "git bisect good.*exited with error code" error
 '
 
 test_done
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 91cbb87..e95d420 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -275,7 +275,7 @@
 	git branch --set-upstream-to main from-topic_2 &&
 	git config branch.from-main.merge > actual &&
 	git rev-parse from-topic_2 >actual2 &&
-	grep -q "^refs/heads/main$" actual &&
+	test_grep -q "^refs/heads/main$" actual &&
 	cmp expect2 actual2
 '
 
diff --git a/t/t6112-rev-list-filters-objects.sh b/t/t6112-rev-list-filters-objects.sh
index e0a825b..a1c244c 100755
--- a/t/t6112-rev-list-filters-objects.sh
+++ b/t/t6112-rev-list-filters-objects.sh
@@ -45,8 +45,8 @@
 		 awk -f print_2.awk) &&
 
 	git -C r1 rev-list --objects --filter=blob:none HEAD $file_3 >observed &&
-	grep "$file_3" observed &&
-	! grep "$file_4" observed
+	test_grep "$file_3" observed &&
+	test_grep ! "$file_4" observed
 '
 
 test_expect_success 'verify emitted+omitted == all' '
@@ -324,7 +324,7 @@
 
 	git -C r3.b rev-list --missing=allow-any --objects HEAD \
 		>objs 2>rev_list_err &&
-	! grep $TREE objs &&
+	test_grep ! $TREE objs &&
 	test_must_be_empty rev_list_err
 '
 
@@ -353,7 +353,7 @@
 	test_line_count = 2 actual &&
 
 	# Make sure no other trees were considered besides the root.
-	! grep "Skipping contents of tree [^.]" filter_trace &&
+	test_grep ! "Skipping contents of tree [^.]" filter_trace &&
 
 	# Try this again with "combine:". If both sub-filters are skipping
 	# trees, the composite filter should also skip trees. This is not
@@ -539,7 +539,7 @@
 		HEAD >actual &&
 
 	test_cmp expect actual &&
-	grep "Add to combine filter-spec: sparse:oid=main:p%3bat%25ter%2bn" \
+	test_grep "Add to combine filter-spec: sparse:oid=main:p%3bat%25ter%2bn" \
 		trace1 &&
 
 	# Repeat the above test, but this time, the characters to encode are in
@@ -551,7 +551,7 @@
 		HEAD >actual &&
 
 	test_cmp expect actual &&
-	grep "Add to combine filter-spec: sparse:oid=main:%5e%7epattern" \
+	test_grep "Add to combine filter-spec: sparse:oid=main:%5e%7epattern" \
 		trace2
 '
 
@@ -623,9 +623,9 @@
 	omitted_2=$(echo a     | git hash-object --stdin) &&
 	omitted_3=$(echo abcde | git hash-object --stdin) &&
 
-	grep "~$omitted_1" actual &&
-	grep "~$omitted_2" actual &&
-	grep "~$omitted_3" actual &&
+	test_grep "~$omitted_1" actual &&
+	test_grep "~$omitted_2" actual &&
+	test_grep "~$omitted_3" actual &&
 	test_line_count = 3 actual
 '
 
@@ -654,7 +654,7 @@
 	blob_hash=$(git -C r4 rev-parse HEAD:subdir/bar) &&
 
 	git -C r4 rev-list --objects --filter=tree:1 HEAD $blob_hash >actual &&
-	grep ^$blob_hash actual
+	test_grep ^$blob_hash actual
 '
 
 # Delete some loose objects and use rev-list, but WITHOUT any filtering.
@@ -693,8 +693,8 @@
 	git -C r2 config --local uploadpack.allowfilter 1 &&
 	GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 clone \
 		--filter=blob:limit=1k "file://$(pwd)/r2" limit &&
-	! grep "blob:limit=1k" trace &&
-	grep "blob:limit=1024" trace
+	test_grep ! "blob:limit=1k" trace &&
+	test_grep "blob:limit=1024" trace
 '
 
 test_expect_success EXPENSIVE 'large sparse filter file ignored' '
diff --git a/t/t6115-rev-list-du.sh b/t/t6115-rev-list-du.sh
index 04c577d..b9650b8 100755
--- a/t/t6115-rev-list-du.sh
+++ b/t/t6115-rev-list-du.sh
@@ -61,13 +61,13 @@
 test_expect_success 'rev-list --disk-usage=human' '
 	git rev-list --objects HEAD --disk-usage=human >actual &&
 	disk_usage_slow --objects HEAD >actual_size &&
-	grep "$(cat actual_size) bytes" actual
+	test_grep "$(cat actual_size) bytes" actual
 '
 
 test_expect_success 'rev-list --disk-usage=human with bitmaps' '
 	git rev-list --objects HEAD --use-bitmap-index --disk-usage=human >actual &&
 	disk_usage_slow --objects HEAD >actual_size &&
-	grep "$(cat actual_size) bytes" actual
+	test_grep "$(cat actual_size) bytes" actual
 '
 
 test_expect_success 'rev-list use --disk-usage unproperly' '
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 4d72033..7a7c466 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -128,7 +128,7 @@
 	warning: tag 'Q' is externally known as 'A'
 	EOF
 	test_cmp expected err &&
-	grep -E '^A-8-g[0-9a-f]+$' out
+	test_grep -E '^A-8-g[0-9a-f]+$' out
 "
 
 test_expect_success 'misnamed annotated tag forces long output' '
@@ -160,7 +160,7 @@
 test_expect_success 'describe works from outside repo using --git-dir' '
 	git clone --bare "$TRASH_DIRECTORY" "$TRASH_DIRECTORY/bare" &&
 	git --git-dir "$TRASH_DIRECTORY/bare" describe >out &&
-	grep -E "^A-8-g[0-9a-f]+$" out
+	test_grep -E "^A-8-g[0-9a-f]+$" out
 '
 
 check_describe "A-8-gHASH" --dirty
@@ -170,7 +170,7 @@
 		cd "$TEST_DIRECTORY" &&
 		git --git-dir "$TRASH_DIRECTORY/.git" --work-tree "$TRASH_DIRECTORY" describe --dirty >"$TRASH_DIRECTORY/out"
 	) &&
-	grep -E "^A-8-g[0-9a-f]+$" out
+	test_grep -E "^A-8-g[0-9a-f]+$" out
 '
 
 test_expect_success 'set-up dirty work tree' '
@@ -183,7 +183,7 @@
 		cd "$TEST_DIRECTORY" &&
 		git --git-dir "$TRASH_DIRECTORY/.git" --work-tree "$TRASH_DIRECTORY" describe --dirty >"$TRASH_DIRECTORY/out"
 	) &&
-	grep -E "^A-8-g[0-9a-f]+-dirty$" out &&
+	test_grep -E "^A-8-g[0-9a-f]+-dirty$" out &&
 	test_cmp expected out
 '
 
@@ -193,7 +193,7 @@
 		cd "$TEST_DIRECTORY" &&
 		git --git-dir "$TRASH_DIRECTORY/.git" --work-tree "$TRASH_DIRECTORY" describe --dirty=.mod >"$TRASH_DIRECTORY/out"
 	) &&
-	grep -E "^A-8-g[0-9a-f]+.mod$" out &&
+	test_grep -E "^A-8-g[0-9a-f]+.mod$" out &&
 	test_cmp expected out
 '
 
@@ -399,7 +399,7 @@
 
 test_expect_success 'describe ignoring a broken submodule' '
 	git describe --broken >out &&
-	grep broken out
+	test_grep broken out
 '
 
 test_expect_success 'describe with --work-tree ignoring a broken submodule' '
@@ -408,7 +408,7 @@
 		git --git-dir "$TRASH_DIRECTORY/.git" --work-tree "$TRASH_DIRECTORY" describe --broken >"$TRASH_DIRECTORY/out"
 	) &&
 	test_when_finished "mv .git/modules/sub_moved .git/modules/sub1" &&
-	grep broken out
+	test_grep broken out
 '
 
 test_expect_success 'describe a blob at a directly tagged commit' '
diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh
index 011e5df..79fe1f2 100755
--- a/t/t6200-fmt-merge-msg.sh
+++ b/t/t6200-fmt-merge-msg.sh
@@ -125,20 +125,20 @@
 	git checkout main &&
 	git fetch . signed-good-tag &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}signed-good-tag${apos}" actual &&
-	grep "^signed-tag-msg" actual &&
-	grep "^# gpg: Signature made" actual &&
-	grep "^# gpg: Good signature from" actual
+	test_grep "^Merge tag ${apos}signed-good-tag${apos}" actual &&
+	test_grep "^signed-tag-msg" actual &&
+	test_grep "^# gpg: Signature made" actual &&
+	test_grep "^# gpg: Good signature from" actual
 '
 
 test_expect_success GPG 'message for merging local tag signed by unknown key' '
 	git checkout main &&
 	git fetch . signed-good-tag &&
 	GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}signed-good-tag${apos}" actual &&
-	grep "^signed-tag-msg" actual &&
-	grep "^# gpg: Signature made" actual &&
-	grep -E "^# gpg: Can${apos}t check signature: (public key not found|No public key)" actual
+	test_grep "^Merge tag ${apos}signed-good-tag${apos}" actual &&
+	test_grep "^signed-tag-msg" actual &&
+	test_grep "^# gpg: Signature made" actual &&
+	test_grep -E "^# gpg: Can${apos}t check signature: (public key not found|No public key)" actual
 '
 
 test_expect_success GPGSSH 'message for merging local tag signed by good ssh key' '
@@ -146,10 +146,10 @@
 	git checkout main &&
 	git fetch . signed-good-ssh-tag &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}signed-good-ssh-tag${apos}" actual &&
-	grep "^signed-ssh-tag-msg" actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual
+	test_grep "^Merge tag ${apos}signed-good-ssh-tag${apos}" actual &&
+	test_grep "^signed-ssh-tag-msg" actual &&
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual
 '
 
 test_expect_success GPGSSH 'message for merging local tag signed by unknown ssh key' '
@@ -157,11 +157,11 @@
 	git checkout main &&
 	git fetch . signed-untrusted-ssh-tag &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}signed-untrusted-ssh-tag${apos}" actual &&
-	grep "^signed-ssh-tag-msg-untrusted" actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
-	grep "${GPGSSH_KEY_NOT_TRUSTED}" actual
+	test_grep "^Merge tag ${apos}signed-untrusted-ssh-tag${apos}" actual &&
+	test_grep "^signed-ssh-tag-msg-untrusted" actual &&
+	test_grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
+	test_grep "${GPGSSH_KEY_NOT_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by expired ssh key' '
@@ -169,9 +169,9 @@
 	git checkout main &&
 	git fetch . expired-signed &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}expired-signed${apos}" actual &&
-	grep "^expired-signed" actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep "^Merge tag ${apos}expired-signed${apos}" actual &&
+	test_grep "^expired-signed" actual &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by not yet valid ssh key' '
@@ -179,9 +179,9 @@
 	git checkout main &&
 	git fetch . notyetvalid-signed &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}notyetvalid-signed${apos}" actual &&
-	grep "^notyetvalid-signed" actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep "^Merge tag ${apos}notyetvalid-signed${apos}" actual &&
+	test_grep "^notyetvalid-signed" actual &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by valid timeboxed ssh key' '
@@ -189,10 +189,10 @@
 	git checkout main &&
 	git fetch . timeboxedvalid-signed &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}timeboxedvalid-signed${apos}" actual &&
-	grep "^timeboxedvalid-signed" actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual
+	test_grep "^Merge tag ${apos}timeboxedvalid-signed${apos}" actual &&
+	test_grep "^timeboxedvalid-signed" actual &&
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by invalid timeboxed ssh key' '
@@ -200,9 +200,9 @@
 	git checkout main &&
 	git fetch . timeboxedinvalid-signed &&
 	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
-	grep "^Merge tag ${apos}timeboxedinvalid-signed${apos}" actual &&
-	grep "^timeboxedinvalid-signed" actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep "^Merge tag ${apos}timeboxedinvalid-signed${apos}" actual &&
+	test_grep "^timeboxedinvalid-signed" actual &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success 'message for merging external branch' '
@@ -651,21 +651,21 @@
 	git show -s --format="%s" >full.0 &&
 	head -n1 full.0 >actual &&
 	# expect that HEAD is shown as-is
-	grep -e "Merge branch .side. into HEAD$" actual &&
+	test_grep -e "Merge branch .side. into HEAD$" actual &&
 
 	git reset --hard main &&
 	git merge --no-ff --into-name=main side &&
 	git show -s --format="%s" >full.1 &&
 	head -n1 full.1 >actual &&
 	# expect that we pretend to be merging to main, that is suppressed
-	grep -e "Merge branch .side.$" actual &&
+	test_grep -e "Merge branch .side.$" actual &&
 
 	git checkout -b throwaway main &&
 	git merge --no-ff --into-name=main side &&
 	git show -s --format="%s" >full.2 &&
 	head -n1 full.2 >actual &&
 	# expect that we pretend to be merging to main, that is suppressed
-	grep -e "Merge branch .side.$" actual
+	test_grep -e "Merge branch .side.$" actual
 '
 
 test_expect_success 'merge.suppressDest configuration' '
@@ -677,28 +677,28 @@
 
 	git -c merge.suppressDest="" fmt-merge-msg <.git/FETCH_HEAD >full.1 &&
 	head -n1 full.1 >actual &&
-	grep -e "Merge branch .side. into main" actual &&
+	test_grep -e "Merge branch .side. into main" actual &&
 
 	git -c merge.suppressDest="mast" fmt-merge-msg <.git/FETCH_HEAD >full.2 &&
 	head -n1 full.2 >actual &&
-	grep -e "Merge branch .side. into main$" actual &&
+	test_grep -e "Merge branch .side. into main$" actual &&
 
 	git -c merge.suppressDest="ma?*[rn]" fmt-merge-msg <.git/FETCH_HEAD >full.3 &&
 	head -n1 full.3 >actual &&
-	grep -e "Merge branch .side." actual &&
-	! grep -e " into main$" actual &&
+	test_grep -e "Merge branch .side." actual &&
+	test_grep ! -e " into main$" actual &&
 
 	git checkout --detach HEAD &&
 	git -c merge.suppressDest="main" fmt-merge-msg <.git/FETCH_HEAD >full.4 &&
 	head -n1 full.4 >actual &&
-	grep -e "Merge branch .side. into HEAD$" actual &&
+	test_grep -e "Merge branch .side. into HEAD$" actual &&
 
 	git -c merge.suppressDest="main" fmt-merge-msg \
 		--into-name=main <.git/FETCH_HEAD >full.5 &&
 	head -n1 full.5 >actual &&
-	grep -e "Merge branch .side." actual &&
-	! grep -e " into main$" actual &&
-	! grep -e " into HEAD$" actual
+	test_grep -e "Merge branch .side." actual &&
+	test_grep ! -e " into main$" actual &&
+	test_grep ! -e " into HEAD$" actual
 '
 
 test_done
diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh
index 1d98091..6d27b42 100755
--- a/t/t6300-for-each-ref.sh
+++ b/t/t6300-for-each-ref.sh
@@ -8,9 +8,9 @@
 . ./test-lib.sh
 
 test_expect_success "for-each-ref does not crash with -h" '
-	test_expect_code 129 git for-each-ref -h >usage &&
+	git for-each-ref -h >usage &&
 	test_grep "[Uu]sage: git for-each-ref " usage &&
-	test_expect_code 129 nongit git for-each-ref -h >usage &&
+	nongit git for-each-ref -h >usage &&
 	test_grep "[Uu]sage: git for-each-ref " usage
 '
 
diff --git a/t/t6301-for-each-ref-errors.sh b/t/t6301-for-each-ref-errors.sh
index e06feb0..72b27c8 100755
--- a/t/t6301-for-each-ref-errors.sh
+++ b/t/t6301-for-each-ref-errors.sh
@@ -52,6 +52,28 @@
 	test_must_be_empty brief-err
 '
 
+test_expect_success 'missing ancestors are reported by contains filters' '
+	test_when_finished "git update-ref -d refs/heads/missing-parent" &&
+	{
+		echo "tree $(git rev-parse HEAD^{tree})" &&
+		echo "parent $MISSING" &&
+		git cat-file commit HEAD |
+			sed -n -e "/^author /p" -e "/^committer /p" &&
+		echo &&
+		echo "missing parent"
+	} >commit &&
+	broken=$(git hash-object -t commit -w commit) &&
+	git update-ref refs/heads/missing-parent "$broken" &&
+	for option in --contains --no-contains
+	do
+		test_must_fail git for-each-ref "$option=HEAD" \
+			refs/heads/missing-parent >out 2>err &&
+		test_must_be_empty out &&
+		test_grep "parse commit $MISSING" err ||
+		return 1
+	done
+'
+
 test_expect_success 'ahead-behind requires an argument' '
 	test_must_fail git for-each-ref \
 		--format="%(ahead-behind)" 2>err &&
diff --git a/t/t6402-merge-rename.sh b/t/t6402-merge-rename.sh
index ff00b74..fb1595a 100755
--- a/t/t6402-merge-rename.sh
+++ b/t/t6402-merge-rename.sh
@@ -332,7 +332,7 @@
 	git checkout -q dir-in-way^0 &&
 	test_must_fail git merge --strategy=recursive renamed-file-has-no-conflicts >output 2>errors &&
 
-	! grep "error: refusing to lose untracked file at" errors &&
+	test_grep ! "error: refusing to lose untracked file at" errors &&
 	test_grep "CONFLICT (modify/delete): dir/file-in-the-way" output &&
 	test_grep "Auto-merging dir" output &&
 	test_grep "moving it to dir~renamed-file-has-no-conflicts instead" output &&
@@ -883,7 +883,7 @@
 test_expect_success 'no spurious "refusing to lose untracked" message' '
 	git checkout main^0 &&
 	test_must_fail git merge rename^0 2>errors.txt &&
-	! grep "refusing to lose untracked file" errors.txt
+	test_grep ! "refusing to lose untracked file" errors.txt
 '
 
 test_expect_success 'do not follow renames for empty files' '
diff --git a/t/t6403-merge-file.sh b/t/t6403-merge-file.sh
index 801284c..dcc5c2a 100755
--- a/t/t6403-merge-file.sh
+++ b/t/t6403-merge-file.sh
@@ -356,7 +356,7 @@
 test_expect_success 'binary files cannot be merged' '
 	test_must_fail git merge-file -p \
 		orig.txt "$TEST_DIRECTORY"/test-binary-1.png new1.txt 2> merge.err &&
-	grep "Cannot merge binary files" merge.err
+	test_grep "Cannot merge binary files" merge.err
 '
 
 test_expect_success 'binary files cannot be merged with --object-id' '
@@ -364,7 +364,7 @@
 	git add orig.txt new1.txt test-binary-1.png &&
 	test_must_fail git merge-file --object-id \
 		:orig.txt :test-binary-1.png :new1.txt 2> merge.err &&
-	grep "Cannot merge binary files" merge.err
+	test_grep "Cannot merge binary files" merge.err
 '
 
 test_expect_success 'MERGE_ZEALOUS simplifies non-conflicts' '
@@ -539,7 +539,7 @@
 test_expect_success '--object-id fails without repository' '
 	empty="$(test_oid empty_blob)" &&
 	nongit test_must_fail git merge-file --object-id $empty $empty $empty 2>err &&
-	grep "not a git repository" err
+	test_grep "not a git repository" err
 '
 
 test_expect_success 'run in a linked worktree with --object-id' '
diff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh
index 346f360..ce420fe 100755
--- a/t/t6404-recursive-merge.sh
+++ b/t/t6404-recursive-merge.sh
@@ -109,7 +109,7 @@
 	git add binary-file &&
 	git commit -m binary2 &&
 	test_must_fail git merge F >merge_output &&
-	grep "Cannot merge binary files: binary-file (HEAD vs. F)" merge_output
+	test_grep "Cannot merge binary files: binary-file (HEAD vs. F)" merge_output
 '
 
 test_expect_success 'mark rename/delete as unmerged' '
diff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh
index 8f6fbef..7f49c59 100755
--- a/t/t6406-merge-attr.sh
+++ b/t/t6406-merge-attr.sh
@@ -97,11 +97,11 @@
 test_expect_success 'check merge result in working tree' '
 
 	git cat-file -p HEAD:binary >binary-orig &&
-	grep "<<<<<<<" text &&
+	test_grep "<<<<<<<" text &&
 	cmp binary-orig binary &&
-	! grep "<<<<<<<" union &&
-	grep Main union &&
-	grep Side union
+	test_grep ! "<<<<<<<" union &&
+	test_grep Main union &&
+	test_grep Side union
 
 '
 
@@ -112,9 +112,9 @@
 		s/ .*$//
 		p
 	}" >actual text &&
-	grep ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" actual &&
-	grep "================================" actual &&
-	grep "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" actual
+	test_grep ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" actual &&
+	test_grep "================================" actual &&
+	test_grep "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" actual
 '
 
 test_expect_success 'invalid conflict-marker-size 3a' '
@@ -193,7 +193,7 @@
 	>./please-abort &&
 	echo "* merge=custom" >.gitattributes &&
 	test_expect_code 2 git merge main 2>err &&
-	grep "^error: failed to execute internal merge" err &&
+	test_grep "^error: failed to execute internal merge" err &&
 	git ls-files -u >output &&
 	git diff --name-only HEAD >>output &&
 	test_must_be_empty output
@@ -260,7 +260,7 @@
 	git commit -am two &&
 
 	test_must_fail git merge bin-main >output &&
-	grep -i "warning.*cannot merge.*HEAD vs. bin-main" output
+	test_grep -i "warning.*cannot merge.*HEAD vs. bin-main" output
 '
 
 test_expect_success !WINDOWS 'custom merge driver that is killed with a signal on recursive merge' '
@@ -297,7 +297,7 @@
 	>./please-abort &&
 	echo "* merge=custom" >.gitattributes &&
 	test_expect_code 2 git merge recursive-a 2>err &&
-	grep "error: failed to execute internal merge" err &&
+	test_grep "error: failed to execute internal merge" err &&
 	git ls-files -u >output &&
 	git diff --name-only HEAD >>output &&
 	test_must_be_empty output
diff --git a/t/t6417-merge-ours-theirs.sh b/t/t6417-merge-ours-theirs.sh
index 62d1406..9486251 100755
--- a/t/t6417-merge-ours-theirs.sh
+++ b/t/t6417-merge-ours-theirs.sh
@@ -26,31 +26,31 @@
 test_expect_success 'plain recursive - should conflict' '
 	git reset --hard main &&
 	test_must_fail git merge -s recursive side &&
-	grep nine file &&
-	grep nueve file &&
-	! grep 9 file &&
-	grep one file &&
-	! grep 1 file
+	test_grep nine file &&
+	test_grep nueve file &&
+	test_grep ! 9 file &&
+	test_grep one file &&
+	test_grep ! 1 file
 '
 
 test_expect_success 'recursive favouring theirs' '
 	git reset --hard main &&
 	git merge -s recursive -Xtheirs side &&
-	! grep nine file &&
-	grep nueve file &&
-	! grep 9 file &&
-	grep one file &&
-	! grep 1 file
+	test_grep ! nine file &&
+	test_grep nueve file &&
+	test_grep ! 9 file &&
+	test_grep one file &&
+	test_grep ! 1 file
 '
 
 test_expect_success 'recursive favouring ours' '
 	git reset --hard main &&
 	git merge -s recursive -X ours side &&
-	grep nine file &&
-	! grep nueve file &&
-	! grep 9 file &&
-	grep one file &&
-	! grep 1 file
+	test_grep nine file &&
+	test_grep ! nueve file &&
+	test_grep ! 9 file &&
+	test_grep one file &&
+	test_grep ! 1 file
 '
 
 test_expect_success 'binary file with -Xours/-Xtheirs' '
diff --git a/t/t6418-merge-text-auto.sh b/t/t6418-merge-text-auto.sh
index 41288a6..c86a1be 100755
--- a/t/t6418-merge-text-auto.sh
+++ b/t/t6418-merge-text-auto.sh
@@ -226,7 +226,7 @@
 		git checkout rename^0 &&
 		test_must_fail git -c merge.renormalize=true merge nuke >out &&
 
-		grep "rename/delete" out
+		test_grep "rename/delete" out
 	)
 '
 
diff --git a/t/t6422-merge-rename-corner-cases.sh b/t/t6422-merge-rename-corner-cases.sh
index 81b645b..82b76da 100755
--- a/t/t6422-merge-rename-corner-cases.sh
+++ b/t/t6422-merge-rename-corner-cases.sh
@@ -916,8 +916,8 @@
 		# bar should have two-way merged contents of the different
 		# versions of bar; check that content from both sides is
 		# present.
-		grep original bar &&
-		grep different bar
+		test_grep original bar &&
+		test_grep different bar
 	)
 '
 
@@ -991,8 +991,8 @@
 		# baz should have two-way merged contents of the original
 		# contents of foo and bar; check that content from both sides
 		# is present.
-		grep foo baz &&
-		grep bar baz
+		test_grep foo baz &&
+		test_grep bar baz
 	)
 '
 
diff --git a/t/t6423-merge-rename-directories.sh b/t/t6423-merge-rename-directories.sh
index 53535a8..24d4c54 100755
--- a/t/t6423-merge-rename-directories.sh
+++ b/t/t6423-merge-rename-directories.sh
@@ -1113,7 +1113,7 @@
 		test_cmp expect actual &&
 		test_path_is_missing x/d &&
 		test_path_is_file y/d &&
-		grep -q "<<<<" y/d  # conflict markers should be present
+		test_grep -q "<<<<" y/d  # conflict markers should be present
 	)
 '
 
@@ -2841,10 +2841,10 @@
 
 		test_must_fail git -c merge.directoryRenames=true merge -s recursive B^0 >out &&
 		grep "CONFLICT (implicit dir rename): Cannot map more than one path to combined/yo" out >error_line &&
-		grep -q dir1/yo error_line &&
-		grep -q dir2/yo error_line &&
-		grep -q dir3/yo error_line &&
-		grep -q dirN/yo error_line &&
+		test_grep -q dir1/yo error_line &&
+		test_grep -q dir2/yo error_line &&
+		test_grep -q dir3/yo error_line &&
+		test_grep -q dirN/yo error_line &&
 
 		git ls-files -s >out &&
 		test_line_count = 16 out &&
@@ -3578,7 +3578,7 @@
 		test_path_is_missing .git/MERGE_HEAD &&
 		test_grep "error: Your local changes to the following files would be overwritten by merge" err &&
 
-		grep -q stuff z/c &&
+		test_grep -q stuff z/c &&
 		test_seq 1 10 >expected &&
 		echo stuff >>expected &&
 		test_cmp expected z/c
@@ -3636,7 +3636,7 @@
 		test_path_is_missing .git/MERGE_HEAD &&
 		test_grep "error: Your local changes to the following files would be overwritten by merge" err &&
 
-		grep -q stuff y/c &&
+		test_grep -q stuff y/c &&
 		test_seq 1 10 >expected &&
 		echo stuff >>expected &&
 		test_cmp expected y/c &&
@@ -3705,7 +3705,7 @@
 		test_path_is_missing .git/MERGE_HEAD &&
 		test_grep "error: Your local changes to the following files would be overwritten by merge" err &&
 
-		grep -q stuff z/c &&
+		test_grep -q stuff z/c &&
 		test_seq 1 10 >expected &&
 		echo stuff >>expected &&
 		test_cmp expected z/c
@@ -4779,7 +4779,7 @@
 
 		test_must_fail git -c merge.directoryRenames=conflict merge -s recursive B^0 >out &&
 
-		grep "CONFLICT (implicit dir rename).*source/bar in the way" out &&
+		test_grep "CONFLICT (implicit dir rename).*source/bar in the way" out &&
 		test_path_is_missing source/bar &&
 		test_path_is_file source/subdir/bar &&
 		test_path_is_file source/baz &&
@@ -4920,7 +4920,7 @@
 		# error about there being a file in the way.
 
 		test_must_fail git -c merge.directoryRenames=conflict merge -s recursive B^0 >out &&
-		grep "CONFLICT (implicit dir rename).*bar in the way" out &&
+		test_grep "CONFLICT (implicit dir rename).*bar in the way" out &&
 
 		test_path_is_missing bar &&
 		test_path_is_file subdir/bar &&
@@ -4999,7 +4999,7 @@
 		# error about there being a file in the way.
 
 		test_must_fail git -c merge.directoryRenames=conflict merge -s recursive B^0 >out &&
-		grep "CONFLICT (implicit dir rename).*dirA/bar in the way" out &&
+		test_grep "CONFLICT (implicit dir rename).*dirA/bar in the way" out &&
 
 		test_path_is_missing dirA/bar &&
 		test_path_is_file dirB/bar &&
@@ -5402,7 +5402,7 @@
 		test_stdout_line_count = 1 git ls-files -s B/stuff &&
 		test_stdout_line_count = 1 git ls-files -s D/file2 &&
 
-		grep "CONFLICT (implicit dir rename).*Existing file/dir at A/file1 in the way" out &&
+		test_grep "CONFLICT (implicit dir rename).*Existing file/dir at A/file1 in the way" out &&
 		test_stdout_line_count = 1 git ls-files -s C/file1
 	)
 '
@@ -5489,7 +5489,7 @@
 		test_stdout_line_count = 1 git ls-files -s B/stuff &&
 		test_stdout_line_count = 1 git ls-files -s D/file2 &&
 
-		grep "CONFLICT (implicit dir rename).*Existing file/dir at A/file1 in the way" out &&
+		test_grep "CONFLICT (implicit dir rename).*Existing file/dir at A/file1 in the way" out &&
 		test_stdout_line_count = 1 git ls-files -s C/file1
 	)
 '
@@ -5570,8 +5570,8 @@
 
 		test_must_fail git -c merge.directoryRenames=conflict merge -s recursive B^0 >out &&
 
-		grep "CONFLICT (rename/delete).*A/file1.*D/file2" out &&
-		grep "CONFLICT (implicit dir rename).*Existing file/dir at A/file1 in the way" out &&
+		test_grep "CONFLICT (rename/delete).*A/file1.*D/file2" out &&
+		test_grep "CONFLICT (implicit dir rename).*Existing file/dir at A/file1 in the way" out &&
 
 		test_stdout_line_count = 6 git ls-files -s &&
 		test_stdout_line_count = 1 git ls-files -s A/other &&
@@ -5586,7 +5586,7 @@
 		# the dir rename of C/file1 -> A/file1 would mean modifying
 		# the code so that renames do not adjust both their source
 		# and target paths in all cases.
-		! grep "CONFLICT (file location)" out &&
+		test_grep ! "CONFLICT (file location)" out &&
 		test_stdout_line_count = 1 git ls-files -s C/file1
 	)
 '
@@ -5652,8 +5652,8 @@
 		test_grep CONFLICT..file.location.*z/d.added.in.B^0.*y/d out &&
 
 		git ls-files >paths &&
-		! grep z/ paths &&
-		grep "y/[de]" paths &&
+		test_grep ! z/ paths &&
+		test_grep "y/[de]" paths &&
 
 		test_path_is_missing z/d &&
 		test_path_is_file    y/d &&
@@ -5676,8 +5676,8 @@
 		test_grep Path.updated:.*z/d.added.in.B^0.*y/d out &&
 
 		git ls-files >paths &&
-		! grep z/ paths &&
-		grep "y/[de]" paths &&
+		test_grep ! z/ paths &&
+		test_grep "y/[de]" paths &&
 
 		test_path_is_missing z/d &&
 		test_path_is_file    y/d &&
@@ -5742,8 +5742,8 @@
 		test_grep CONFLICT..file.location.*x/d.renamed.to.z/d.*moved.to.y/d out &&
 
 		git ls-files >paths &&
-		! grep z/ paths &&
-		grep "y/d" paths &&
+		test_grep ! z/ paths &&
+		test_grep "y/d" paths &&
 
 		test_path_is_missing z/d &&
 		test_path_is_file    y/d
@@ -5764,8 +5764,8 @@
 		test_grep Path.updated:.*x/d.renamed.to.z/d.in.B^0.*moving.it.to.y/d out &&
 
 		git ls-files >paths &&
-		! grep z/ paths &&
-		grep "y/d" paths &&
+		test_grep ! z/ paths &&
+		test_grep "y/d" paths &&
 
 		test_path_is_missing z/d &&
 		test_path_is_file    y/d
@@ -5827,8 +5827,8 @@
 		test_grep CONFLICT..file.location.*x/d.renamed.to.z/d.*moved.to.y/d out &&
 
 		git ls-files >paths &&
-		! grep z/ paths &&
-		grep "y/d" paths &&
+		test_grep ! z/ paths &&
+		test_grep "y/d" paths &&
 
 		test_path_is_missing z/d &&
 		test_path_is_file    y/d
@@ -5848,8 +5848,8 @@
 		test_grep Path.updated:.*x/d.renamed.to.z/d.in.B^0.*moving.it.to.y/d out &&
 
 		git ls-files >paths &&
-		! grep z/ paths &&
-		grep "y/d" paths &&
+		test_grep ! z/ paths &&
+		test_grep "y/d" paths &&
 
 		test_path_is_missing z/d &&
 		test_path_is_file    y/d
@@ -5917,9 +5917,9 @@
 		test_grep CONFLICT..file.location.*a/y.renamed.to.c/y.*moved.to.d/y out &&
 
 		git ls-files >paths &&
-		! grep b/ paths &&
-		! grep c/ paths &&
-		grep "d/y" paths &&
+		test_grep ! b/ paths &&
+		test_grep ! c/ paths &&
+		test_grep "d/y" paths &&
 
 		test_path_is_missing b/y &&
 		test_path_is_missing c/y &&
@@ -5941,9 +5941,9 @@
 		test_grep Path.updated.*a/y.renamed.to.c/y.*moving.it.to.d/y out &&
 
 		git ls-files >paths &&
-		! grep b/ paths &&
-		! grep c/ paths &&
-		grep "d/y" paths &&
+		test_grep ! b/ paths &&
+		test_grep ! c/ paths &&
+		test_grep "d/y" paths &&
 
 		test_path_is_missing b/y &&
 		test_path_is_missing c/y &&
@@ -6058,8 +6058,8 @@
 		test_must_be_empty err &&
 
 		git ls-files >paths &&
-		! grep a/x paths &&
-		grep b/x paths
+		test_grep ! a/x paths &&
+		test_grep b/x paths
 	)
 '
 
diff --git a/t/t6424-merge-unrelated-index-changes.sh b/t/t6424-merge-unrelated-index-changes.sh
index ddc7524..12e58bc 100755
--- a/t/t6424-merge-unrelated-index-changes.sh
+++ b/t/t6424-merge-unrelated-index-changes.sh
@@ -298,9 +298,9 @@
 
 	test_must_fail git merge -s ort -s octopus C^0 >output 2>&1 &&
 
-	grep "Trying merge strategy ort..." output &&
-	grep "Trying merge strategy octopus..." output &&
-	grep "No merge strategy handled the merge." output &&
+	test_grep "Trying merge strategy ort..." output &&
+	test_grep "Trying merge strategy octopus..." output &&
+	test_grep "No merge strategy handled the merge." output &&
 
 	# Changes to "a" should remain staged
 	git rev-parse :a >actual &&
diff --git a/t/t6427-diff3-conflict-markers.sh b/t/t6427-diff3-conflict-markers.sh
index 57569c4..818550b 100755
--- a/t/t6427-diff3-conflict-markers.sh
+++ b/t/t6427-diff3-conflict-markers.sh
@@ -39,7 +39,7 @@
 
 		test_must_fail git -c merge.conflictstyle=diff3 merge --allow-unrelated-histories -s recursive R^0 &&
 
-		grep "|||||| empty tree" content
+		test_grep "|||||| empty tree" content
 	)
 '
 
@@ -97,7 +97,7 @@
 
 		test_must_fail git -c merge.conflictstyle=diff3 merge -s recursive R^0 &&
 
-		grep "|||||| $MAIN:content" renamed
+		test_grep "|||||| $MAIN:content" renamed
 	)
 '
 
@@ -185,7 +185,7 @@
 
 		test_must_fail git -c merge.conflictstyle=diff3 merge -s recursive R^0 &&
 
-		grep "|||||| merged common ancestors:content" renamed
+		test_grep "|||||| merged common ancestors:content" renamed
 	)
 '
 
@@ -198,7 +198,7 @@
 		git checkout -b side HEAD^ &&
 		test_commit side file &&
 		test_must_fail git -c merge.conflictstyle=diff3 rebase --merge main &&
-		grep "||||||| parent of" file
+		test_grep "||||||| parent of" file
 	)
 '
 
@@ -207,7 +207,7 @@
 		cd rebase &&
 		git rebase --abort &&
 		test_must_fail git -c merge.conflictstyle=diff3 rebase --apply main &&
-		grep "||||||| constructed fake ancestor" file
+		test_grep "||||||| constructed fake ancestor" file
 	)
 '
 
diff --git a/t/t6432-merge-recursive-space-options.sh b/t/t6432-merge-recursive-space-options.sh
index db4b77e..110b48c 100755
--- a/t/t6432-merge-recursive-space-options.sh
+++ b/t/t6432-merge-recursive-space-options.sh
@@ -109,7 +109,7 @@
 	git read-tree --reset -u HEAD &&
 	test_must_fail git merge-recursive HEAD^ -- HEAD remote &&
 	test_must_fail git update-index --refresh &&
-	grep "<<<<<<" text.txt
+	test_grep "<<<<<<" text.txt
 '
 
 test_expect_success '--ignore-space-change makes merge succeed' '
@@ -123,7 +123,7 @@
 	git read-tree --reset -u HEAD &&
 	test_must_fail git cherry-pick remote &&
 	test_must_fail git update-index --refresh &&
-	grep "<<<<<<" text.txt
+	test_grep "<<<<<<" text.txt
 '
 
 test_expect_success '-Xignore-space-change makes cherry-pick succeed' '
diff --git a/t/t6436-merge-overwrite.sh b/t/t6436-merge-overwrite.sh
index 70b5d2d..5e39a15 100755
--- a/t/t6436-merge-overwrite.sh
+++ b/t/t6436-merge-overwrite.sh
@@ -189,10 +189,10 @@
 
 test_expect_success 'will not clobber WT/index when merging into unborn' '
 	git merge main &&
-	grep foo tracked-file &&
+	test_grep foo tracked-file &&
 	git show :tracked-file >expect &&
-	grep foo expect &&
-	grep bar untracked-file
+	test_grep foo expect &&
+	test_grep bar untracked-file
 '
 
 test_done
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index a564758..107e13a 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh
@@ -111,7 +111,7 @@
 	 git checkout -b test-nonforward-a b &&
 	 test_must_fail git merge c 2>actual &&
 	 sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
-	 grep "$sub_expect" actual
+	 test_grep "$sub_expect" actual
 	 )
 '
 
@@ -148,7 +148,7 @@
 	  git rev-parse --short sub-d > ../expect) &&
 	  test_must_fail git merge c >actual 2>sub-actual &&
 	  sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
-	  grep "$sub_expect" sub-actual &&
+	  test_grep "$sub_expect" sub-actual &&
 	 grep $(cat expect) actual > /dev/null &&
 	 git reset --hard)
 '
@@ -164,7 +164,7 @@
 	 ) &&
 	test_must_fail git merge c >actual 2>sub-actual &&
 	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-c)" &&
-	grep "$sub_expect" sub-actual &&
+	test_grep "$sub_expect" sub-actual &&
 	grep $(cat expect1) actual > /dev/null &&
 	grep $(cat expect2) actual > /dev/null &&
 	git reset --hard)
@@ -207,7 +207,7 @@
 	git checkout -b test-backward e &&
 	test_must_fail git merge f 2>actual &&
 	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short sub-d)" &&
-	grep "$sub_expect" actual
+	test_grep "$sub_expect" actual
 	)
 '
 
@@ -513,7 +513,7 @@
 	git commit -m "b" &&
 	test_must_fail git merge a 2>actual &&
 	sub_expect="go to submodule (sub), and either merge commit $(git -C sub rev-parse --short HEAD^1)" &&
-	grep "$sub_expect" actual
+	test_grep "$sub_expect" actual
 	)
 '
 
diff --git a/t/t6500-gc.sh b/t/t6500-gc.sh
index ea9aaad..a7142ab 100755
--- a/t/t6500-gc.sh
+++ b/t/t6500-gc.sh
@@ -35,7 +35,7 @@
 		cd broken &&
 		git init &&
 		echo "[gc] pruneexpire = CORRUPT" >>.git/config &&
-		test_expect_code 129 git gc -h >usage 2>&1
+		git gc -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage" broken/usage
 '
@@ -176,15 +176,15 @@
 
 	# Check that git-pack-refs is run as a sanity check (done via
 	# gc_before_repack()) but that git-expire is not.
-	grep -E "^trace: (built-in|exec|run_command): git pack-refs --" trace.out &&
-	! grep -E "^trace: (built-in|exec|run_command): git reflog expire --" trace.out
+	test_grep -E "^trace: (built-in|exec|run_command): git pack-refs --" trace.out &&
+	test_grep ! -E "^trace: (built-in|exec|run_command): git reflog expire --" trace.out
 '
 
 test_expect_success 'one of gc.reflogExpire{Unreachable,}=never does not skip "expire" via "gc"' '
 	>trace.out &&
 	test_config gc.reflogExpire never &&
 	GIT_TRACE=$(pwd)/trace.out git gc &&
-	grep -E "^trace: (built-in|exec|run_command): git reflog expire --" trace.out
+	test_grep -E "^trace: (built-in|exec|run_command): git reflog expire --" trace.out
 '
 
 test_expect_success 'gc.repackFilter launches repack with a filter' '
@@ -196,7 +196,7 @@
 	GIT_TRACE=$(pwd)/trace.out git -C bare.git -c gc.repackFilter=blob:none \
 		-c repack.writeBitmaps=false -c gc.cruftPacks=false gc &&
 	test_stdout_line_count = 2 ls bare.git/objects/pack/*.pack &&
-	grep -E "^trace: (built-in|exec|run_command): git repack .* --filter=blob:none ?.*" trace.out
+	test_grep -E "^trace: (built-in|exec|run_command): git repack .* --filter=blob:none ?.*" trace.out
 '
 
 test_expect_success 'gc.repackFilterTo store filtered out objects' '
diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh
index b5b314e..019fed9 100755
--- a/t/t6600-test-reach.sh
+++ b/t/t6600-test-reach.sh
@@ -49,6 +49,42 @@
 			git tag -a -m "$x-$i" tag-$x-$i commit-$x-$i || return 1
 		done
 	done &&
+	# Build a topology with clock skew to test the !FIND_ALL early
+	# exit in paint_down_to_common().  M2 is the correct merge base
+	# of P1 and P2, but its ancestor M1 has a higher committer date
+	# due to clock skew.  With date-only ordering (v1 commit graph
+	# without corrected commit dates), M1 pops from the queue first,
+	# gets both paint sides, and the early exit fires before M2 is
+	# ever visited.
+	#
+	#        P1     P2          @7000
+	#        |     /  \
+	#        A    B    D        @6000
+	#       / \   |    |
+	#      |  M2--+    |        @2000 (correct merge base)
+	#       \ |        |
+	#        M1--------+        @5000 (clock skew: date > M2)
+	#        |
+	#       root                @1000
+	#
+	git checkout --orphan skew-orphan &&
+	skew_tree=$(git mktree </dev/null) &&
+	skew_commit () {
+		GIT_COMMITTER_DATE="@$1 +0000" GIT_AUTHOR_DATE="@$1 +0000" \
+			git commit-tree -m "$2" "$skew_tree" $3 $4 $5 $6
+	} &&
+	skew_root=$(skew_commit 1000 root) &&
+	skew_M1=$(skew_commit 5000 M1 -p "$skew_root") &&
+	skew_M2=$(skew_commit 2000 M2 -p "$skew_M1") &&
+	skew_A=$(skew_commit 6000 A -p "$skew_M1" -p "$skew_M2") &&
+	skew_B=$(skew_commit 6000 B -p "$skew_M2") &&
+	skew_D=$(skew_commit 6000 D -p "$skew_M1") &&
+	skew_P1=$(skew_commit 7000 P1 -p "$skew_A") &&
+	skew_P2=$(skew_commit 7000 P2 -p "$skew_B" -p "$skew_D") &&
+	git branch -f skew-P1 "$skew_P1" &&
+	git branch -f skew-P2 "$skew_P2" &&
+	git tag skew-M2 "$skew_M2" &&
+
 	git commit-graph write --reachable &&
 	mv .git/objects/info/commit-graph commit-graph-full &&
 	chmod u+w commit-graph-full &&
@@ -958,13 +994,18 @@
 	git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all &&
 	git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single &&
 	test_line_count = 1 single &&
-	grep -F -f single all &&
+	test_grep -F -f single all &&
 
 	cp commit-graph-half .git/objects/info/commit-graph &&
 	git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all &&
 	git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single &&
 	test_line_count = 1 single &&
-	grep -F -f single all
+	test_grep -F -f single all
+'
+
+test_expect_success 'merge-base without --all, clock skew, v1 commit-graph' '
+	git rev-parse skew-M2 >expect &&
+	merge_base_all_modes skew-P1 skew-P2
 '
 
 test_done
diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh
index 920479e..7cf4aa5 100755
--- a/t/t7001-mv.sh
+++ b/t/t7001-mv.sh
@@ -46,7 +46,7 @@
 
 test_expect_success 'checking the commit' '
 	git diff-tree -r -M --name-status  HEAD^ HEAD >actual &&
-	grep "^R100..*path0/COPYING..*path1/COPYING" actual
+	test_grep "^R100..*path0/COPYING..*path1/COPYING" actual
 '
 
 test_expect_success 'moving the file back into subdirectory' '
@@ -60,7 +60,7 @@
 
 test_expect_success 'checking the commit' '
 	git diff-tree -r -M --name-status  HEAD^ HEAD >actual &&
-	grep "^R100..*path1/COPYING..*path0/COPYING" actual
+	test_grep "^R100..*path1/COPYING..*path0/COPYING" actual
 '
 
 test_expect_success 'mv --dry-run does not move file' '
@@ -147,8 +147,8 @@
 
 test_expect_success 'checking the commit' '
 	git diff-tree -r -M --name-status  HEAD^ HEAD >actual &&
-	grep "^R100..*path0/COPYING..*path2/COPYING" actual &&
-	grep "^R100..*path0/README..*path2/README" actual
+	test_grep "^R100..*path0/COPYING..*path2/COPYING" actual &&
+	test_grep "^R100..*path0/README..*path2/README" actual
 '
 
 test_expect_success 'succeed when source is a prefix of destination' '
@@ -165,8 +165,8 @@
 
 test_expect_success 'checking the commit' '
 	git diff-tree -r -M --name-status  HEAD^ HEAD >actual &&
-	grep "^R100..*path2/COPYING..*path1/path2/COPYING" actual &&
-	grep "^R100..*path2/README..*path1/path2/README" actual
+	test_grep "^R100..*path2/COPYING..*path1/path2/COPYING" actual &&
+	test_grep "^R100..*path2/README..*path1/path2/README" actual
 '
 
 test_expect_success 'do not move directory over existing directory' '
@@ -200,7 +200,7 @@
 
 	T=$(git write-tree) &&
 	git ls-tree -r $T >out &&
-	grep partA/outline.txt out
+	test_grep partA/outline.txt out
 '
 
 rm -fr papers partA path?
@@ -504,7 +504,7 @@
 	mkdir dummy dest &&
 	git mv -k dummy sub dest &&
 	git status --porcelain >actual &&
-	grep "^R  sub -> dest/sub" actual &&
+	test_grep "^R  sub -> dest/sub" actual &&
 	git reset --hard &&
 	git checkout .
 '
diff --git a/t/t7002-mv-sparse-checkout.sh b/t/t7002-mv-sparse-checkout.sh
index 4d3f221..9c0e82b 100755
--- a/t/t7002-mv-sparse-checkout.sh
+++ b/t/t7002-mv-sparse-checkout.sh
@@ -319,8 +319,8 @@
 	test_path_is_missing sub/d &&
 	test_path_is_missing folder1/d &&
 	git ls-files -t >actual &&
-	! grep "^H sub/d\$" actual &&
-	grep "S folder1/d" actual
+	test_grep ! "^H sub/d\$" actual &&
+	test_grep "S folder1/d" actual
 '
 
 test_expect_success 'move clean path from in-cone to out-of-cone overwrite' '
@@ -346,8 +346,8 @@
 	test_path_is_missing sub/file1 &&
 	test_path_is_missing folder1/file1 &&
 	git ls-files -t >actual &&
-	! grep "H sub/file1" actual &&
-	grep "S folder1/file1" actual &&
+	test_grep ! "H sub/file1" actual &&
+	test_grep "S folder1/file1" actual &&
 
 	# compare file content before move and after move
 	echo "sub/file1 overwrite" >expect &&
@@ -382,8 +382,8 @@
 	test_path_is_missing sub/file1 &&
 	test_path_is_missing folder1/file1 &&
 	git ls-files -t >actual &&
-	! grep "H sub/file1" actual &&
-	grep "S folder1/file1" actual &&
+	test_grep ! "H sub/file1" actual &&
+	test_grep "S folder1/file1" actual &&
 
 	# compare file content before move and after move
 	echo "sub/file1 overwrite" >expect &&
@@ -421,9 +421,9 @@
 	test_path_is_missing sub/dir/e &&
 	test_path_is_missing folder1/file1 &&
 	git ls-files -t >actual &&
-	! grep "H sub/dir/file1" actual &&
-	! grep "H sub/dir/e" actual &&
-	grep "S folder1/dir/file1" actual &&
+	test_grep ! "H sub/dir/file1" actual &&
+	test_grep ! "H sub/dir/e" actual &&
+	test_grep "S folder1/dir/file1" actual &&
 
 	# compare file content before move and after move
 	echo test >expect &&
@@ -452,8 +452,8 @@
 	test_path_is_missing sub/d &&
 	test_path_is_file folder1/d &&
 	git ls-files -t >actual &&
-	! grep "^H sub/d\$" actual &&
-	grep "H folder1/d" actual
+	test_grep ! "^H sub/d\$" actual &&
+	test_grep "H folder1/d" actual
 '
 
 test_expect_success 'move dir from in-cone to out-of-cone' '
@@ -473,8 +473,8 @@
 	test_path_is_missing sub/dir &&
 	test_path_is_missing folder1 &&
 	git ls-files -t >actual &&
-	! grep "H sub/dir/e" actual &&
-	grep "S folder1/dir/e" actual
+	test_grep ! "H sub/dir/e" actual &&
+	test_grep "S folder1/dir/e" actual
 '
 
 test_expect_success 'move partially-dirty dir from in-cone to out-of-cone' '
@@ -506,12 +506,12 @@
 	test_path_is_file folder1/dir/e2 &&
 	test_path_is_file folder1/dir/e3 &&
 	git ls-files -t >actual &&
-	! grep "H sub/dir/e" actual &&
-	! grep "H sub/dir/e2" actual &&
-	! grep "H sub/dir/e3" actual &&
-	grep "S folder1/dir/e" actual &&
-	grep "H folder1/dir/e2" actual &&
-	grep "H folder1/dir/e3" actual
+	test_grep ! "H sub/dir/e" actual &&
+	test_grep ! "H sub/dir/e2" actual &&
+	test_grep ! "H sub/dir/e3" actual &&
+	test_grep "S folder1/dir/e" actual &&
+	test_grep "H folder1/dir/e2" actual &&
+	test_grep "H folder1/dir/e3" actual
 '
 
 test_done
diff --git a/t/t7003-filter-branch.sh b/t/t7003-filter-branch.sh
index c475769..86011e7 100755
--- a/t/t7003-filter-branch.sh
+++ b/t/t7003-filter-branch.sh
@@ -49,7 +49,7 @@
 test_expect_success 'rewrite bare repository identically' '
 	(git config core.bare true && cd .git &&
 	 git filter-branch branch > filter-output 2>&1 &&
-	! grep fatal filter-output)
+	test_grep ! fatal filter-output)
 '
 git config core.bare false
 test_expect_success 'result is really identical' '
@@ -65,7 +65,7 @@
 	git filter-branch -d "$TRASHDIR/dfoo" \
 		--index-filter "cp \"$TRASHDIR\"/dfoo/backup-refs \"$TRASHDIR\"" \
 	) &&
-	grep drepo "$TRASHDIR/backup-refs"
+	test_grep drepo "$TRASHDIR/backup-refs"
 '
 
 test_expect_success 'tree-filter works with -d' '
@@ -503,7 +503,7 @@
 	git tag -a -m "tag to a tree" treetag $new_tree &&
 	git reset --hard HEAD &&
 	git filter-branch -f -- --all >filter-output 2>&1 &&
-	! grep fatal filter-output
+	test_grep ! fatal filter-output
 '
 
 test_expect_success 'filter-branch handles ref deletion' '
@@ -512,7 +512,7 @@
 	git tag empty &&
 	git branch to-delete &&
 	git filter-branch -f --prune-empty to-delete >out 2>&1 &&
-	grep "to-delete.*was deleted" out &&
+	test_grep "to-delete.*was deleted" out &&
 	test_must_fail git rev-parse --verify to-delete
 '
 
@@ -523,8 +523,8 @@
 	git filter-branch -f \
 		--index-filter "git rm --ignore-unmatch --cached to-drop.t" \
 		 rewrite >out 2>&1 &&
-	grep "rewrite.*was rewritten" out &&
-	! grep -i warning out &&
+	test_grep "rewrite.*was rewritten" out &&
+	test_grep ! -i warning out &&
 	git diff-tree empty rewrite
 '
 
@@ -532,8 +532,8 @@
 	test_commit to-exclude &&
 	git branch ancestor &&
 	git filter-branch -f ancestor -- :^to-exclude.t >out 2>&1 &&
-	grep "ancestor.*was rewritten" out &&
-	! grep -i warning out &&
+	test_grep "ancestor.*was rewritten" out &&
+	test_grep ! -i warning out &&
 	git diff-tree HEAD^ ancestor
 '
 
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index d918005..8c795d7 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -937,7 +937,7 @@
 	test_cmp expect actual.focus &&
 
 	# Error reported for tags that point to non-commits.
-	grep "error: object [0-9a-f]* is a blob, not a commit" err
+	test_grep "error: object [0-9a-f]* is a blob, not a commit" err
 '
 
 # trying to verify annotated non-signed tags:
@@ -1611,6 +1611,24 @@
 	test_cmp expected actual
 '
 
+test_expect_success 'tag --contains rejects cyclic replacement histories' '
+	first=$(git rev-parse HEAD~2) &&
+	second=$(git rev-parse HEAD~) &&
+	third=$(git rev-parse HEAD) &&
+	test_when_finished "
+		git replace -d $first &&
+		git replace -d $third &&
+		git tag -d cycle-a cycle-b
+	" &&
+	git tag cycle-a "$first" &&
+	git tag cycle-b "$third" &&
+	git replace --graft "$first" "$third" "$second" &&
+	git replace --graft "$third" "$first" &&
+	test_must_fail git tag --contains="$second" --list "cycle-*" \
+		>/dev/null 2>err &&
+	test_grep "fatal: commit ancestry contains a cycle" err
+'
+
 # other ways of specifying the commit
 test_expect_success 'checking that first commit is in all tags (tag)' '
 	cat >expected <<-\EOF &&
diff --git a/t/t7006-pager.sh b/t/t7006-pager.sh
index 9717e82..75ca7a2 100755
--- a/t/t7006-pager.sh
+++ b/t/t7006-pager.sh
@@ -45,8 +45,8 @@
 
 		test_terminal git log
 	) &&
-	grep ^LESS= pager-env.out &&
-	grep ^LV= pager-env.out
+	test_grep ^LESS= pager-env.out &&
+	test_grep ^LV= pager-env.out
 '
 
 test_expect_success !MINGW,TTY 'LESS and LV envvars set by git-sh-setup' '
@@ -58,8 +58,8 @@
 		export PATH &&
 		test_terminal sh -c ". git-sh-setup && git_pager"
 	) &&
-	grep ^LESS= pager-env.out &&
-	grep ^LV= pager-env.out
+	test_grep ^LESS= pager-env.out &&
+	test_grep ^LV= pager-env.out
 '
 
 test_expect_success TTY 'some commands do not use a pager' '
@@ -685,7 +685,7 @@
 
 	grep child_exit trace.normal >child-exits &&
 	test_line_count = 1 child-exits &&
-	grep " code:0 " child-exits &&
+	test_grep " code:0 " child-exits &&
 	test_path_is_file pager-used
 '
 
@@ -706,7 +706,7 @@
 
 	grep child_exit trace.normal >child-exits &&
 	test_line_count = 1 child-exits &&
-	grep " code:1 " child-exits &&
+	test_grep " code:1 " child-exits &&
 	test_path_is_file pager-used
 '
 
@@ -721,7 +721,7 @@
 
 	grep child_exit trace.normal >child-exits &&
 	test_line_count = 1 child-exits &&
-	grep " code:1 " child-exits &&
+	test_grep " code:1 " child-exits &&
 	test_path_is_file pager-used
 '
 
@@ -749,7 +749,7 @@
 
 	grep child_exit trace.normal >child-exits &&
 	test_line_count = 1 child-exits &&
-	grep " code:143 " child-exits &&
+	test_grep " code:143 " child-exits &&
 	test_path_is_file pager-used
 '
 
diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh
index cd5c20f..526cc4e 100755
--- a/t/t7012-skip-worktree-writing.sh
+++ b/t/t7012-skip-worktree-writing.sh
@@ -30,7 +30,7 @@
 	git update-index --skip-worktree init.t &&
 	echo dirty >> init.t &&
 	test_must_fail git read-tree -m -u HEAD^ &&
-	grep -q dirty init.t &&
+	test_grep -q dirty init.t &&
 	test "$(git ls-files -t init.t)" = "S init.t" &&
 	git update-index --no-skip-worktree init.t
 '
@@ -48,7 +48,7 @@
 	git update-index --skip-worktree added &&
 	echo dirty >> added &&
 	test_must_fail git read-tree -m -u HEAD^ &&
-	grep -q dirty added &&
+	test_grep -q dirty added &&
 	test "$(git ls-files -t added)" = "S added" &&
 	git update-index --no-skip-worktree added
 '
@@ -173,7 +173,7 @@
 		echo in the way >modified &&
 		test_must_fail git stash apply 2>error&&
 
-		grep "changes.*would be overwritten by merge" error &&
+		test_grep "changes.*would be overwritten by merge" error &&
 
 		echo in the way >expect &&
 		test_cmp expect modified &&
diff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh
index 2c14707..909878d 100755
--- a/t/t7030-verify-tag.sh
+++ b/t/t7030-verify-tag.sh
@@ -8,9 +8,9 @@
 . "$TEST_DIRECTORY/lib-gpg.sh"
 
 test_expect_success GPG 'verify-tag does not crash with -h' '
-	test_expect_code 129 git verify-tag -h >usage &&
+	git verify-tag -h >usage &&
 	test_grep "[Uu]sage: git verify-tag " usage &&
-	test_expect_code 129 nongit git verify-tag -h >usage &&
+	nongit git verify-tag -h >usage &&
 	test_grep "[Uu]sage: git verify-tag " usage
 '
 
@@ -63,8 +63,8 @@
 		for tag in initial second merge fourth-signed sixth-signed seventh-signed
 		do
 			git verify-tag $tag 2>actual &&
-			grep "Good signature from" actual &&
-			! grep "BAD signature from" actual &&
+			test_grep "Good signature from" actual &&
+			test_grep ! "BAD signature from" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -72,8 +72,8 @@
 		for tag in fourth-unsigned fifth-unsigned sixth-unsigned
 		do
 			test_must_fail git verify-tag $tag 2>actual &&
-			! grep "Good signature from" actual &&
-			! grep "BAD signature from" actual &&
+			test_grep ! "Good signature from" actual &&
+			test_grep ! "BAD signature from" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -81,9 +81,9 @@
 		for tag in eighth-signed-alt
 		do
 			git verify-tag $tag 2>actual &&
-			grep "Good signature from" actual &&
-			! grep "BAD signature from" actual &&
-			grep "not certified" actual &&
+			test_grep "Good signature from" actual &&
+			test_grep ! "BAD signature from" actual &&
+			test_grep "not certified" actual &&
 			echo $tag OK || exit 1
 		done
 	)
@@ -91,32 +91,32 @@
 
 test_expect_success GPGSM 'verify and show signatures x509' '
 	git verify-tag ninth-signed-x509 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual &&
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual &&
 	echo ninth-signed-x509 OK
 '
 
 test_expect_success GPGSM 'verify and show signatures x509 with low minTrustLevel' '
 	test_config gpg.minTrustLevel undefined &&
 	git verify-tag ninth-signed-x509 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual &&
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual &&
 	echo ninth-signed-x509 OK
 '
 
 test_expect_success GPGSM 'verify and show signatures x509 with matching minTrustLevel' '
 	test_config gpg.minTrustLevel fully &&
 	git verify-tag ninth-signed-x509 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual &&
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual &&
 	echo ninth-signed-x509 OK
 '
 
 test_expect_success GPGSM 'verify and show signatures x509 with high minTrustLevel' '
 	test_config gpg.minTrustLevel ultimate &&
 	test_must_fail git verify-tag ninth-signed-x509 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual &&
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual &&
 	echo ninth-signed-x509 OK
 '
 
@@ -125,8 +125,8 @@
 	sed -e "/^tag / s/seventh/7th-forged/" raw >forged1 &&
 	git hash-object -w -t tag forged1 >forged1.tag &&
 	test_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&
-	grep "BAD signature from" actual1 &&
-	! grep "Good signature from" actual1
+	test_grep "BAD signature from" actual1 &&
+	test_grep ! "Good signature from" actual1
 '
 
 test_expect_success GPG 'verify signatures with --raw' '
@@ -134,8 +134,8 @@
 		for tag in initial second merge fourth-signed sixth-signed seventh-signed
 		do
 			git verify-tag --raw $tag 2>actual &&
-			grep "GOODSIG" actual &&
-			! grep "BADSIG" actual &&
+			test_grep "GOODSIG" actual &&
+			test_grep ! "BADSIG" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -143,8 +143,8 @@
 		for tag in fourth-unsigned fifth-unsigned sixth-unsigned
 		do
 			test_must_fail git verify-tag --raw $tag 2>actual &&
-			! grep "GOODSIG" actual &&
-			! grep "BADSIG" actual &&
+			test_grep ! "GOODSIG" actual &&
+			test_grep ! "BADSIG" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -152,9 +152,9 @@
 		for tag in eighth-signed-alt
 		do
 			git verify-tag --raw $tag 2>actual &&
-			grep "GOODSIG" actual &&
-			! grep "BADSIG" actual &&
-			grep "TRUST_UNDEFINED" actual &&
+			test_grep "GOODSIG" actual &&
+			test_grep ! "BADSIG" actual &&
+			test_grep "TRUST_UNDEFINED" actual &&
 			echo $tag OK || exit 1
 		done
 	)
@@ -162,8 +162,8 @@
 
 test_expect_success GPGSM 'verify signatures with --raw x509' '
 	git verify-tag --raw ninth-signed-x509 2>actual &&
-	grep "GOODSIG" actual &&
-	! grep "BADSIG" actual &&
+	test_grep "GOODSIG" actual &&
+	test_grep ! "BADSIG" actual &&
 	echo ninth-signed-x509 OK
 '
 
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
index 80359d4..3000beb 100755
--- a/t/t7031-verify-tag-signed-ssh.sh
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -71,8 +71,8 @@
 		for tag in initial second merge fourth-signed sixth-signed seventh-signed
 		do
 			git verify-tag $tag 2>actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -80,8 +80,8 @@
 		for tag in fourth-unsigned fifth-unsigned sixth-unsigned
 		do
 			test_must_fail git verify-tag $tag 2>actual &&
-			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -89,9 +89,9 @@
 		for tag in eighth-signed-alt
 		do
 			test_must_fail git verify-tag $tag 2>actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
-			grep "${GPGSSH_KEY_NOT_TRUSTED}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_KEY_NOT_TRUSTED}" actual &&
 			echo $tag OK || exit 1
 		done
 	)
@@ -100,26 +100,26 @@
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on expired signature key' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-tag expired-signed 2>actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on not yet valid signature key' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-tag notyetvalid-signed 2>actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag succeeds with tag date and key validity matching' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git verify-tag timeboxedvalid-signed 2>actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag fails with tag date outside of key validity' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-tag timeboxedinvalid-signed 2>actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH 'detect fudged ssh signature' '
@@ -128,9 +128,9 @@
 	sed -e "/^tag / s/seventh/7th-forged/" raw >forged1 &&
 	git hash-object -w -t tag forged1 >forged1.tag &&
 	test_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&
-	grep "${GPGSSH_BAD_SIGNATURE}" actual1 &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual1 &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual1
+	test_grep "${GPGSSH_BAD_SIGNATURE}" actual1 &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual1 &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual1
 '
 
 test_expect_success GPGSSH 'verify ssh signatures with --raw' '
@@ -139,8 +139,8 @@
 		for tag in initial second merge fourth-signed sixth-signed seventh-signed
 		do
 			git verify-tag --raw $tag 2>actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -148,8 +148,8 @@
 		for tag in fourth-unsigned fifth-unsigned sixth-unsigned
 		do
 			test_must_fail git verify-tag --raw $tag 2>actual &&
-			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $tag OK || exit 1
 		done
 	) &&
@@ -157,8 +157,8 @@
 		for tag in eighth-signed-alt
 		do
 			test_must_fail git verify-tag --raw $tag 2>actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $tag OK || exit 1
 		done
 	)
@@ -167,8 +167,8 @@
 test_expect_success GPGSSH 'verify signatures with --raw ssh' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git verify-tag --raw sixth-signed 2>actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 	echo sixth-signed OK
 '
 
diff --git a/t/t7102-reset.sh b/t/t7102-reset.sh
index 0503a64..298272c 100755
--- a/t/t7102-reset.sh
+++ b/t/t7102-reset.sh
@@ -87,7 +87,7 @@
 	test_expect_success "no 'git reset --no-$opt'" '
 		test_when_finished "rm -f err" &&
 		test_must_fail git reset --no-$opt 2>err &&
-		grep "error: unknown option .no-$opt." err
+		test_grep "error: unknown option .no-$opt." err
 	'
 done
 
diff --git a/t/t7110-reset-merge.sh b/t/t7110-reset-merge.sh
index 9a33507..a241ecf 100755
--- a/t/t7110-reset-merge.sh
+++ b/t/t7110-reset-merge.sh
@@ -29,16 +29,16 @@
 # file2:     C       D     D    D     --merge  C       D     D
 test_expect_success 'reset --merge is ok with changes in file it does not touch' '
 	git reset --merge HEAD^ &&
-	! grep 4 file1 &&
-	grep 4 file2 &&
+	test_grep ! 4 file1 &&
+	test_grep 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
 	test -z "$(git diff --cached)"
 '
 
 test_expect_success 'reset --merge is ok when switching back' '
 	git reset --merge second &&
-	grep 4 file1 &&
-	grep 4 file2 &&
+	test_grep 4 file1 &&
+	test_grep 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
 	test -z "$(git diff --cached)"
 '
@@ -53,16 +53,16 @@
 	git reset --hard second &&
 	cat file1 >file2 &&
 	git reset --keep HEAD^ &&
-	! grep 4 file1 &&
-	grep 4 file2 &&
+	test_grep ! 4 file1 &&
+	test_grep 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
 	test -z "$(git diff --cached)"
 '
 
 test_expect_success 'reset --keep is ok when switching back' '
 	git reset --keep second &&
-	grep 4 file1 &&
-	grep 4 file2 &&
+	test_grep 4 file1 &&
+	test_grep 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
 	test -z "$(git diff --cached)"
 '
@@ -79,9 +79,9 @@
 	echo "line 5" >> file1 &&
 	git add file1 &&
 	git reset --merge HEAD^ &&
-	! grep 4 file1 &&
-	! grep 5 file1 &&
-	grep 4 file2 &&
+	test_grep ! 4 file1 &&
+	test_grep ! 5 file1 &&
+	test_grep 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
 	test -z "$(git diff --cached)"
 '
@@ -91,9 +91,9 @@
 	echo "line 5" >> file2 &&
 	git add file2 &&
 	git reset --merge second &&
-	! grep 4 file2 &&
-	! grep 5 file1 &&
-	grep 4 file1 &&
+	test_grep ! 4 file2 &&
+	test_grep ! 5 file1 &&
+	test_grep 4 file1 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
 	test -z "$(git diff --cached)"
 '
@@ -121,7 +121,7 @@
 	echo "line 4" >> file2 &&
 	git add file2 &&
 	git reset --merge HEAD^ &&
-	! grep 4 file2 &&
+	test_grep ! 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
 	test -z "$(git diff)" &&
 	test -z "$(git diff --cached)"
@@ -130,8 +130,8 @@
 test_expect_success 'reset --merge is ok again when switching back (2)' '
 	git reset --hard initial &&
 	git reset --merge second &&
-	! grep 4 file2 &&
-	grep 4 file1 &&
+	test_grep ! 4 file2 &&
+	test_grep 4 file1 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
 	test -z "$(git diff --cached)"
 '
@@ -147,15 +147,15 @@
 	echo "line 4" >> file2 &&
 	git add file2 &&
 	git reset --keep HEAD^ &&
-	grep 4 file2 &&
+	test_grep 4 file2 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
 	test -z "$(git diff --cached)"
 '
 
 test_expect_success 'reset --keep keeps changes when switching back' '
 	git reset --keep second &&
-	grep 4 file2 &&
-	grep 4 file1 &&
+	test_grep 4 file2 &&
+	test_grep 4 file1 &&
 	test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
 	test -z "$(git diff --cached)"
 '
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
index 7613b1d..0ddd1ad 100755
--- a/t/t7201-co.sh
+++ b/t/t7201-co.sh
@@ -318,7 +318,7 @@
 	git checkout -f renamer &&
 	git clean -f &&
 	git checkout renamer^ 2>messages &&
-	grep "HEAD is now at $rev" messages &&
+	test_grep "HEAD is now at $rev" messages &&
 	test_line_count -gt 1 messages &&
 	H=$(git rev-parse --verify HEAD) &&
 	M=$(git show-ref -s --verify refs/heads/main) &&
@@ -794,7 +794,7 @@
 	git checkout main^0 &&
 	echo modified >one &&
 	test_must_fail git checkout renamer 2>error.log &&
-	! grep "^Previous HEAD" error.log
+	test_grep ! "^Previous HEAD" error.log
 '
 
 (
@@ -844,7 +844,7 @@
 	(
 		for t in filfre-common left right
 		do
-			grep $t arm || exit 1
+			test_grep $t arm || exit 1
 		done
 	) &&
 
diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh
index 00d4070..13ad3ee 100755
--- a/t/t7300-clean.sh
+++ b/t/t7300-clean.sh
@@ -793,7 +793,7 @@
 
 		git clean -fX modules/foobar >../output &&
 
-		grep Removing ../output &&
+		test_grep Removing ../output &&
 
 		test_path_is_missing modules/foobar/src/generated/code.c &&
 		test_path_is_file modules/foobar/Makefile
diff --git a/t/t7301-clean-interactive.sh b/t/t7301-clean-interactive.sh
index f743e5b..de59ba3 100755
--- a/t/t7301-clean-interactive.sh
+++ b/t/t7301-clean-interactive.sh
@@ -479,7 +479,7 @@
 	test_decode_color |
 	head -n 1 >header &&
 	# not i18ngrep
-	grep "^<BOLD>" header
+	test_grep "^<BOLD>" header
 '
 
 test_done
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index 65fcfae..eefdecb 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -20,13 +20,13 @@
 
 test_expect_success 'submodule usage: -h' '
 	git submodule -h >out 2>err &&
-	grep "^usage: git submodule" out &&
+	test_grep "^usage: git submodule" out &&
 	test_must_be_empty err
 '
 
 test_expect_success 'submodule usage: --recursive' '
 	test_expect_code 1 git submodule --recursive >out 2>err &&
-	grep "^usage: git submodule" err &&
+	test_grep "^usage: git submodule" err &&
 	test_must_be_empty out
 '
 
@@ -214,14 +214,14 @@
 test_expect_success 'redirected submodule add does not show progress' '
 	git -C addtest submodule add "file://$submodurl/parent" submod-redirected \
 		2>err &&
-	! grep % err &&
+	test_grep ! % err &&
 	test_grep ! "Checking connectivity" err
 '
 
 test_expect_success 'redirected submodule add --progress does show progress' '
 	git -C addtest submodule add --progress "file://$submodurl/parent" \
 		submod-redirected-progress 2>err && \
-	grep % err
+	test_grep % err
 '
 
 test_expect_success 'submodule add to .gitignored path fails' '
@@ -506,7 +506,7 @@
 
 test_expect_success 'status should initially be "missing"' '
 	git submodule status >lines &&
-	grep "^-$rev1" lines
+	test_grep "^-$rev1" lines
 '
 
 test_expect_success 'init should register submodule url in .git/config' '
@@ -524,7 +524,7 @@
 	mkdir init &&
 	git submodule status >lines &&
 	rm -fr init &&
-	grep "^-$rev1" lines
+	test_grep "^-$rev1" lines
 '
 
 test_failure_with_unknown_submodule () {
@@ -584,7 +584,7 @@
 
 test_expect_success 'status should be "up-to-date" after update' '
 	git submodule status >list &&
-	grep "^ $rev1" list
+	test_grep "^ $rev1" list
 '
 
 test_expect_success 'status "up-to-date" from subdirectory' '
@@ -593,8 +593,8 @@
 		cd sub &&
 		git submodule status >../list
 	) &&
-	grep "^ $rev1" list &&
-	grep "\\.\\./init" list
+	test_grep "^ $rev1" list &&
+	test_grep "\\.\\./init" list
 '
 
 test_expect_success 'status "up-to-date" from subdirectory with path' '
@@ -603,8 +603,8 @@
 		cd sub &&
 		git submodule status ../init >../list
 	) &&
-	grep "^ $rev1" list &&
-	grep "\\.\\./init" list
+	test_grep "^ $rev1" list &&
+	test_grep "\\.\\./init" list
 '
 
 test_expect_success 'status should be "modified" after submodule commit' '
@@ -619,7 +619,7 @@
 	test -n "$rev2" &&
 	git submodule status >list &&
 
-	grep "^+$rev2" list
+	test_grep "^+$rev2" list
 '
 
 test_expect_success '"submodule --cached" command forms should be identical' '
@@ -634,12 +634,12 @@
 
 test_expect_success 'the --cached sha1 should be rev1' '
 	git submodule --cached status >list &&
-	grep "^+$rev1" list
+	test_grep "^+$rev1" list
 '
 
 test_expect_success 'git diff should report the SHA1 of the new submodule commit' '
 	git diff >diff &&
-	grep "^+Subproject commit $rev2" diff
+	test_grep "^+Subproject commit $rev2" diff
 '
 
 test_expect_success 'update should checkout rev1' '
@@ -654,7 +654,7 @@
 
 test_expect_success 'status should be "up-to-date" after update' '
 	git submodule status >list &&
-	grep "^ $rev1" list
+	test_grep "^ $rev1" list
 '
 
 test_expect_success 'checkout superproject with subproject already present' '
@@ -1407,7 +1407,7 @@
 	done &&
 
 	test_must_fail git -C multisuper_clone submodule update 2>err &&
-	grep "cannot clone submodule .sub[0-3]. without a URL" err
+	test_grep "cannot clone submodule .sub[0-3]. without a URL" err
 '
 
 test_expect_success 'clone --recurse-submodules with a pathspec works' '
diff --git a/t/t7402-submodule-rebase.sh b/t/t7402-submodule-rebase.sh
index 06cee34..872eb2d 100755
--- a/t/t7402-submodule-rebase.sh
+++ b/t/t7402-submodule-rebase.sh
@@ -125,7 +125,7 @@
 	) >expect &&
 	test_cmp expect actual &&
 	sub_expect="go to submodule (submodule), and either merge commit $(git -C submodule rev-parse --short HEAD^0)" &&
-	grep "$sub_expect" actual_output
+	test_grep "$sub_expect" actual_output
 '
 
 test_done
diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh
index 6abb008..10adeab 100755
--- a/t/t7406-submodule-update.sh
+++ b/t/t7406-submodule-update.sh
@@ -256,6 +256,25 @@
 	)
 '
 
+test_expect_success 'submodule update --remote resolves URL rewrites' '
+	test_config_global "url.$(pwd)/.insteadOf" local: &&
+	mkdir alias-super alias-submodule &&
+	(
+		cd alias-submodule &&
+		git init &&
+		git commit --allow-empty --message "Initial commit"
+	) &&
+	(
+		cd alias-super &&
+		git init &&
+		git submodule add local:alias-submodule submodule &&
+		git submodule update --force &&
+		git -C submodule remote rename origin upstream &&
+		git -C submodule remote add fork user@host &&
+		git submodule update --remote
+	)
+'
+
 test_expect_success 'submodule update --remote should fetch upstream changes with .' '
 	(
 		cd super &&
@@ -627,10 +646,10 @@
 	  compare_head
 	 ) &&
 	 git diff --name-only >out &&
-	 grep ^submodule$ out &&
+	 test_grep ^submodule$ out &&
 	 git submodule update &&
 	 git diff --name-only >out &&
-	 grep ^submodule$ out &&
+	 test_grep ^submodule$ out &&
 	 (cd submodule &&
 	  compare_head
 	 ) &&
@@ -647,10 +666,10 @@
 	  compare_head
 	 ) &&
 	 git diff --name-only >out &&
-	 grep ^submodule$ out &&
+	 test_grep ^submodule$ out &&
 	 git submodule update --checkout &&
 	 git diff --name-only >out &&
-	 ! grep ^submodule$ out &&
+	 test_grep ! ^submodule$ out &&
 	 (cd submodule &&
 	  ! compare_head
 	 ) &&
@@ -679,10 +698,10 @@
 		git submodule init submodule &&
 
 		git submodule update submodule 2>err &&
-		! grep "Submodule path .* not initialized" err &&
+		test_grep ! "Submodule path .* not initialized" err &&
 
 		git submodule update rebasing 2>err &&
-		grep "Submodule path .rebasing. not initialized" err &&
+		test_grep "Submodule path .rebasing. not initialized" err &&
 
 		test_path_exists submodule/.git &&
 		test_path_is_missing rebasing/.git
@@ -699,7 +718,7 @@
 		git submodule update 2>err &&
 		test_path_exists submodule/.git &&
 		test_path_is_missing rebasing/.git &&
-		! grep "Submodule path .* not initialized" err
+		test_grep ! "Submodule path .* not initialized" err
 	)
 
 '
@@ -1046,12 +1065,12 @@
 test_expect_success 'submodule update can be run in parallel' '
 	(cd super2 &&
 	 GIT_TRACE=$(pwd)/trace.out git submodule update --jobs 7 &&
-	 grep "7 tasks" trace.out &&
+	 test_grep "7 tasks" trace.out &&
 	 git config submodule.fetchJobs 8 &&
 	 GIT_TRACE=$(pwd)/trace.out git submodule update &&
-	 grep "8 tasks" trace.out &&
+	 test_grep "8 tasks" trace.out &&
 	 GIT_TRACE=$(pwd)/trace.out git submodule update --jobs 9 &&
-	 grep "9 tasks" trace.out
+	 test_grep "9 tasks" trace.out
 	)
 '
 
@@ -1066,14 +1085,14 @@
 test_expect_success 'git clone passes the parallel jobs config on to submodules' '
 	test_when_finished "rm -rf super4" &&
 	GIT_TRACE=$(pwd)/trace.out git clone --recurse-submodules --jobs 7 . super4 &&
-	grep "7 tasks" trace.out &&
+	test_grep "7 tasks" trace.out &&
 	rm -rf super4 &&
 	git config --global submodule.fetchJobs 8 &&
 	GIT_TRACE=$(pwd)/trace.out git clone --recurse-submodules . super4 &&
-	grep "8 tasks" trace.out &&
+	test_grep "8 tasks" trace.out &&
 	rm -rf super4 &&
 	GIT_TRACE=$(pwd)/trace.out git clone --recurse-submodules --jobs 9 . super4 &&
-	grep "9 tasks" trace.out &&
+	test_grep "9 tasks" trace.out &&
 	rm -rf super4
 '
 
diff --git a/t/t7416-submodule-dash-url.sh b/t/t7416-submodule-dash-url.sh
index 3d944a0..9f4af88 100755
--- a/t/t7416-submodule-dash-url.sh
+++ b/t/t7416-submodule-dash-url.sh
@@ -49,7 +49,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'trailing backslash is handled correctly' '
@@ -79,7 +79,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects relative URL resolving to missing scheme' '
@@ -95,7 +95,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects empty URL scheme' '
@@ -111,7 +111,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects relative URL resolving to empty scheme' '
@@ -127,7 +127,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects empty hostname' '
@@ -143,7 +143,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects relative url that produced empty hostname' '
@@ -159,7 +159,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck permits embedded newline with unrecognized scheme' '
@@ -189,7 +189,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects embedded newline in relative url' '
@@ -204,7 +204,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_expect_success 'fsck rejects embedded newline in git url' '
@@ -219,7 +219,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesUrl err
+	test_grep gitmodulesUrl err
 '
 
 test_done
diff --git a/t/t7417-submodule-path-url.sh b/t/t7417-submodule-path-url.sh
index 5e3051d..7a6f615 100755
--- a/t/t7417-submodule-path-url.sh
+++ b/t/t7417-submodule-path-url.sh
@@ -29,7 +29,7 @@
 	git init --bare dst &&
 	git -C dst config transfer.fsckObjects true &&
 	test_must_fail git push dst HEAD 2>err &&
-	grep gitmodulesPath err
+	test_grep gitmodulesPath err
 '
 
 test_expect_success MINGW 'submodule paths disallows trailing spaces' '
diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh
index 69a17a9..72c7f6f 100755
--- a/t/t7450-bad-git-dotfiles.sh
+++ b/t/t7450-bad-git-dotfiles.sh
@@ -109,7 +109,7 @@
 
 test_expect_success 'clone evil superproject' '
 	git clone --recurse-submodules . victim >output 2>&1 &&
-	! grep "RUNNING POST CHECKOUT" output
+	test_grep ! "RUNNING POST CHECKOUT" output
 '
 
 test_expect_success 'fsck detects evil superproject' '
@@ -167,7 +167,7 @@
 	test_must_fail git -C dst.git index-pack --strict odd.pack 2>output &&
 	# Make sure we fail due to bad gitmodules content, not because we
 	# could not read the blob in the first place.
-	grep gitmodulesName output
+	test_grep gitmodulesName output
 '
 
 check_dotx_symlink () {
@@ -216,7 +216,7 @@
 			# Check not only that we fail, but that it is due to the
 			# symlink detector
 			$fsck_must_fail git fsck 2>output &&
-			grep "$fsck_prefix.*tree $tree: ${name}Symlink" output
+			test_grep "$fsck_prefix.*tree $tree: ${name}Symlink" output
 		)
 	'
 
@@ -228,7 +228,7 @@
 				    -c core.protectntfs \
 				    -c core.protecthfs \
 				    read-tree $tree 2>err &&
-			grep "invalid path.*$name" err &&
+			test_grep "invalid path.*$name" err &&
 			git -C $dir ls-files -s >out &&
 			test_must_be_empty out
 		'
@@ -317,7 +317,13 @@
 		test_must_fail git -c core.protectNTFS=false \
 			clone --recurse-submodules squatting squatting-clone 2>err &&
 		test_grep -e "directory not empty" -e "not an empty directory" err &&
-		! grep gitdir squatting-clone/d/a/git~2
+		# git~2 is an 8.3 short name, present only when 8.3 name
+		# generation is enabled. The "directory not empty" check
+		# above is the primary assertion.
+		if test -f squatting-clone/d/a/git~2
+		then
+			test_grep ! gitdir squatting-clone/d/a/git~2
+		fi
 	fi
 '
 
@@ -350,7 +356,7 @@
 test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '
 	test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err &&
 	cat err &&
-	grep -E "(already exists|is inside git dir|does not point to a valid repository)" err &&
+	test_grep -E "(already exists|is inside git dir|does not point to a valid repository)" err &&
 	{
 		test_path_is_missing .git/modules/hippo/HEAD ||
 		test_path_is_missing .git/modules/hippo/hooks/HEAD
@@ -370,7 +376,7 @@
 	) &&
 	test_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&
 	cat err &&
-	grep "is inside git dir" err &&
+	test_grep "is inside git dir" err &&
 	test_path_is_missing nested_checkout/thing2/.git
 '
 
diff --git a/t/t7501-commit-basic-functionality.sh b/t/t7501-commit-basic-functionality.sh
index a37509f..7794bab 100755
--- a/t/t7501-commit-basic-functionality.sh
+++ b/t/t7501-commit-basic-functionality.sh
@@ -342,12 +342,12 @@
 	echo gak >file &&
 	git commit -m author \
 		--author "Rubber Duck <rduck@convoy.org>" -a >output 2>&1 &&
-	grep Rubber.Duck output
+	test_grep Rubber.Duck output
 '
 
 test_expect_success 'interactive add' '
 	echo 7 | test_must_fail git commit --interactive >out &&
-	grep "What now" out
+	test_grep "What now" out
 '
 
 test_expect_success "commit --interactive doesn't change index if editor aborts" '
@@ -376,13 +376,13 @@
 
 	EDITOR=./editor git commit -a -F msg &&
 	git show -s --pretty=format:%s >subject &&
-	grep -q good subject &&
+	test_grep -q good subject &&
 
 	echo quack >file &&
 	echo Another good message. |
 	EDITOR=./editor git commit -a -F - &&
 	git show -s --pretty=format:%s >subject &&
-	grep -q good subject
+	test_grep -q good subject
 '
 
 test_expect_success 'partial commit that involves removal (1)' '
@@ -471,7 +471,7 @@
 
 test_expect_success 'commit mentions forced date in output' '
 	git commit --amend --date=2010-01-02T03:04:05 >output &&
-	grep "Date: *Sat Jan 2 03:04:05 2010" output
+	test_grep "Date: *Sat Jan 2 03:04:05 2010" output
 '
 
 test_expect_success 'commit complains about completely bogus dates' '
@@ -660,9 +660,9 @@
 	git add chz &&
 	git commit elif -m "tacocat is a palindrome" &&
 	git show --stat >stat &&
-	grep elif stat &&
+	test_grep elif stat &&
 	git diff --cached >diff &&
-	grep chz diff
+	test_grep chz diff
 '
 
 test_expect_success 'same tree (single parent)' '
@@ -676,7 +676,7 @@
 
 	git commit --allow-empty -m "forced empty" &&
 	git cat-file commit HEAD >commit &&
-	grep forced commit
+	test_grep forced commit
 
 '
 
diff --git a/t/t7502-commit-porcelain.sh b/t/t7502-commit-porcelain.sh
index 05f6da4..62e3970 100755
--- a/t/t7502-commit-porcelain.sh
+++ b/t/t7502-commit-porcelain.sh
@@ -529,7 +529,7 @@
 
 	test_config diff.noprefix true &&
 	git status -v >actual &&
-	grep "diff --git negative negative" actual
+	test_grep "diff --git negative negative" actual
 '
 
 mesg_with_comment_and_newlines='
diff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh
index b53d71c..8e9d8ff 100755
--- a/t/t7507-commit-verbose.sh
+++ b/t/t7507-commit-verbose.sh
@@ -106,13 +106,13 @@
 		export GIT_EDITOR &&
 		test_must_fail git -c core.commentchar="foo>" commit -a -v >out 2>err
 	) &&
-	grep "^foo> " out &&
+	test_grep "^foo> " out &&
 	test_grep "Aborting commit due to empty commit message." err
 '
 
 test_expect_success 'status does not verbose without --verbose' '
 	git status >actual &&
-	! grep "^diff --git" actual
+	test_grep ! "^diff --git" actual
 '
 
 test_expect_success 'setup -v -v' '
@@ -163,7 +163,7 @@
 
 test_expect_success "status ignores commit.verbose=true" '
 	git -c commit.verbose=true status >actual &&
-	! grep "^diff --git actual"
+	test_grep ! "^diff --git" actual
 '
 
 test_done
diff --git a/t/t7508-status.sh b/t/t7508-status.sh
index c2057bc..0fd7c79 100755
--- a/t/t7508-status.sh
+++ b/t/t7508-status.sh
@@ -16,7 +16,7 @@
 		cd broken &&
 		git init &&
 		echo "[status] showuntrackedfiles = CORRUPT" >>.git/config &&
-		test_expect_code 129 git status -h >usage 2>&1
+		git status -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage" broken/usage
 '
@@ -28,7 +28,7 @@
 		cd broken &&
 		git init &&
 		echo "[status] showuntrackedfiles = CORRUPT" >>.git/config &&
-		test_expect_code 129 git commit -h >usage 2>&1
+		git commit -h >usage 2>&1
 	) &&
 	test_grep "[Uu]sage" broken/usage
 '
@@ -863,7 +863,7 @@
 	test_cmp expect output &&
 
 	git status -s --ignored >output &&
-	grep "^!! \"expect with spaces\"$" output &&
+	test_grep "^!! \"expect with spaces\"$" output &&
 	grep -v "^!! " output >output-wo-ignored &&
 	test_cmp expect output-wo-ignored
 '
@@ -908,7 +908,7 @@
 	git clone upstream downstream &&
 	git -C downstream checkout @{u} &&
 	git -C downstream status >actual &&
-	grep -E "HEAD detached at [0-9a-f]+" actual
+	test_grep -E "HEAD detached at [0-9a-f]+" actual
 '
 
 test_expect_success 'setup status submodule summary' '
@@ -1127,7 +1127,7 @@
 		# make dir1/tracked stat-dirty
 		>dir1/tracked1 && mv -f dir1/tracked1 dir1/tracked &&
 		git status -s >output &&
-		! grep dir1/tracked output &&
+		test_grep ! dir1/tracked output &&
 		# make sure "status" succeeded without writing index out
 		git diff-files | grep dir1/tracked
 	)
@@ -1773,7 +1773,7 @@
 	)
 '
 
-test_expect_success EXPENSIVE 'status does not re-read unchanged 4 or 8 GiB file' '
+test_expect_success EXPENSIVE,SIZE_T_IS_64BIT 'status does not re-read unchanged 4 or 8 GiB file' '
 	(
 		mkdir large-file &&
 		cd large-file &&
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index aa9108d..0c9a26d 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -9,9 +9,9 @@
 . "$TEST_DIRECTORY/lib-gpg.sh"
 
 test_expect_success GPG 'verify-commit does not crash with -h' '
-	test_expect_code 129 git verify-commit -h >usage &&
+	git verify-commit -h >usage &&
 	test_grep "[Uu]sage: git verify-commit " usage &&
-	test_expect_code 129 nongit git verify-commit -h >usage &&
+	nongit git verify-commit -h >usage &&
 	test_grep "[Uu]sage: git verify-commit " usage
 '
 
@@ -89,8 +89,8 @@
 		do
 			git verify-commit $commit &&
 			git show --pretty=short --show-signature $commit >actual &&
-			grep "Good signature from" actual &&
-			! grep "BAD signature from" actual &&
+			test_grep "Good signature from" actual &&
+			test_grep ! "BAD signature from" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -100,8 +100,8 @@
 		do
 			test_must_fail git verify-commit $commit &&
 			git show --pretty=short --show-signature $commit >actual &&
-			! grep "Good signature from" actual &&
-			! grep "BAD signature from" actual &&
+			test_grep ! "Good signature from" actual &&
+			test_grep ! "BAD signature from" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -109,9 +109,9 @@
 		for commit in eighth-signed-alt twelfth-signed-alt
 		do
 			git show --pretty=short --show-signature $commit >actual &&
-			grep "Good signature from" actual &&
-			! grep "BAD signature from" actual &&
-			grep "not certified" actual &&
+			test_grep "Good signature from" actual &&
+			test_grep ! "BAD signature from" actual &&
+			test_grep "not certified" actual &&
 			echo $commit OK || exit 1
 		done
 	)
@@ -119,16 +119,16 @@
 
 test_expect_success GPG 'verify-commit exits failure on unknown signature' '
 	test_must_fail env GNUPGHOME="$GNUPGHOME_NOT_USED" git verify-commit initial 2>actual &&
-	! grep "Good signature from" actual &&
-	! grep "BAD signature from" actual &&
-	grep -q -F -e "No public key" -e "public key not found" actual
+	test_grep ! "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual &&
+	test_grep -q -F -e "No public key" -e "public key not found" actual
 '
 
 test_expect_success GPG 'verify-commit exits success on untrusted signature' '
 	git verify-commit eighth-signed-alt 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual &&
-	grep "not certified" actual
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual &&
+	test_grep "not certified" actual
 '
 
 test_expect_success GPG 'verify-commit exits success with matching minTrustLevel' '
@@ -151,8 +151,8 @@
 		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
 		do
 			git verify-commit --raw $commit 2>actual &&
-			grep "GOODSIG" actual &&
-			! grep "BADSIG" actual &&
+			test_grep "GOODSIG" actual &&
+			test_grep ! "BADSIG" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -160,8 +160,8 @@
 		for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned
 		do
 			test_must_fail git verify-commit --raw $commit 2>actual &&
-			! grep "GOODSIG" actual &&
-			! grep "BADSIG" actual &&
+			test_grep ! "GOODSIG" actual &&
+			test_grep ! "BADSIG" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -169,9 +169,9 @@
 		for commit in eighth-signed-alt
 		do
 			git verify-commit --raw $commit 2>actual &&
-			grep "GOODSIG" actual &&
-			! grep "BADSIG" actual &&
-			grep "TRUST_UNDEFINED" actual &&
+			test_grep "GOODSIG" actual &&
+			test_grep ! "BADSIG" actual &&
+			test_grep "TRUST_UNDEFINED" actual &&
 			echo $commit OK || exit 1
 		done
 	)
@@ -179,7 +179,7 @@
 
 test_expect_success GPG 'proper header is used for hash algorithm' '
 	git cat-file commit fourth-signed >output &&
-	grep "^$(test_oid header) -----BEGIN PGP SIGNATURE-----" output
+	test_grep "^$(test_oid header) -----BEGIN PGP SIGNATURE-----" output
 '
 
 test_expect_success GPG 'show signed commit with signature' '
@@ -201,8 +201,8 @@
 	git hash-object -w -t commit forged1 >forged1.commit &&
 	test_must_fail git verify-commit $(cat forged1.commit) &&
 	git show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&
-	grep "BAD signature from" actual1 &&
-	! grep "Good signature from" actual1
+	test_grep "BAD signature from" actual1 &&
+	test_grep ! "Good signature from" actual1
 '
 
 test_expect_success GPG 'detect fudged signature with NUL' '
@@ -212,8 +212,8 @@
 	git hash-object --literally -w -t commit forged2 >forged2.commit &&
 	test_must_fail git verify-commit $(cat forged2.commit) &&
 	git show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&
-	grep "BAD signature from" actual2 &&
-	! grep "Good signature from" actual2
+	test_grep "BAD signature from" actual2 &&
+	test_grep ! "Good signature from" actual2
 '
 
 test_expect_success GPG 'amending already signed commit' '
@@ -221,8 +221,8 @@
 	git commit --amend -S --no-edit &&
 	git verify-commit HEAD &&
 	git show -s --show-signature HEAD >actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual
 '
 
 test_expect_success GPG2 'bare signature' '
@@ -326,8 +326,8 @@
 test_expect_success GPG 'log.showsignature behaves like --show-signature' '
 	test_config log.showsignature true &&
 	git show initial >actual &&
-	grep "gpg: Signature made" actual &&
-	grep "gpg: Good signature" actual
+	test_grep "gpg: Signature made" actual &&
+	test_grep "gpg: Good signature" actual
 '
 
 test_expect_success GPG 'check config gpg.format values' '
@@ -349,8 +349,8 @@
 	git hash-object -w -t commit double-commit >double-commit.commit &&
 	test_must_fail git verify-commit $(cat double-commit.commit) &&
 	git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&
-	grep "BAD signature from" double-actual &&
-	grep "Good signature from" double-actual
+	test_grep "BAD signature from" double-actual &&
+	test_grep "Good signature from" double-actual
 '
 
 test_expect_success GPG 'show double signature with custom format' '
@@ -404,8 +404,8 @@
 	head=$(git hash-object -t commit -w commit) &&
 	git reset --hard $head &&
 	git verify-commit $head 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual
 '
 
 test_expect_success 'custom `gpg.program`' '
@@ -449,7 +449,7 @@
 
 	test_must_fail env LET_GPG_PROGRAM_FAIL=1 \
 	git commit -S --allow-empty -m must-fail 2>err &&
-	grep zOMG err &&
+	test_grep zOMG err &&
 
 	# `gpg.program` starts with `~`, the path should be interpreted to be relative to `$HOME`
 	test_config gpg.program "~/fake-gpg" &&
diff --git a/t/t7516-commit-races.sh b/t/t7516-commit-races.sh
index de7c4ca..29669f6 100755
--- a/t/t7516-commit-races.sh
+++ b/t/t7516-commit-races.sh
@@ -10,7 +10,7 @@
 	EOF
 	test_must_fail env EDITOR=./hare-editor git commit --allow-empty -m tortoise -e &&
 	git show -s --pretty=format:%s >subject &&
-	grep hare subject &&
+	test_grep hare subject &&
 	git show -s --pretty=format:%P >out &&
 	test_must_be_empty out
 '
@@ -24,7 +24,7 @@
 	git rev-parse HEAD >base &&
 	test_must_fail env EDITOR=./airplane-editor git commit --allow-empty -m ship -e &&
 	git show -s --pretty=format:%s >subject &&
-	grep airplane subject &&
+	test_grep airplane subject &&
 	git rev-parse HEAD^ >parent &&
 	test_cmp base parent
 '
diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh
index 7ee69ec..93973ed 100755
--- a/t/t7519-status-fsmonitor.sh
+++ b/t/t7519-status-fsmonitor.sh
@@ -63,19 +63,19 @@
 	test_must_fail \
 		git -C ./bare-clone -c core.fsmonitor=foo \
 			update-index --fsmonitor 2>actual &&
-	grep "bare repository .* is incompatible with fsmonitor" actual &&
+	test_grep "bare repository .* is incompatible with fsmonitor" actual &&
 
 	test_must_fail \
 		git -C ./bare-clone -c core.fsmonitor=true \
 			update-index --fsmonitor 2>actual &&
-	grep "bare repository .* is incompatible with fsmonitor" actual
+	test_grep "bare repository .* is incompatible with fsmonitor" actual
 '
 
 test_expect_success FSMONITOR_DAEMON 'run fsmonitor-daemon in bare repo' '
 	test_when_finished "rm -rf ./bare-clone actual" &&
 	git init --bare bare-clone &&
 	test_must_fail git -C ./bare-clone fsmonitor--daemon run 2>actual &&
-	grep "bare repository .* is incompatible with fsmonitor" actual
+	test_grep "bare repository .* is incompatible with fsmonitor" actual
 '
 
 test_expect_success MINGW,FSMONITOR_DAEMON 'run fsmonitor-daemon in virtual repo' '
@@ -84,7 +84,7 @@
 	test_must_fail git -C ./fake-virtual-clone \
 			   -c core.virtualfilesystem=true \
 			   fsmonitor--daemon run 2>actual &&
-	grep "virtual repository .* is incompatible with fsmonitor" actual
+	test_grep "virtual repository .* is incompatible with fsmonitor" actual
 '
 
 test_expect_success 'setup' '
@@ -111,21 +111,21 @@
 # test that the fsmonitor extension is off by default
 test_expect_success 'fsmonitor extension is off by default' '
 	test-tool dump-fsmonitor >actual &&
-	grep "^no fsmonitor" actual
+	test_grep "^no fsmonitor" actual
 '
 
 # test that "update-index --fsmonitor" adds the fsmonitor extension
 test_expect_success 'update-index --fsmonitor" adds the fsmonitor extension' '
 	git update-index --fsmonitor &&
 	test-tool dump-fsmonitor >actual &&
-	grep "^fsmonitor last update" actual
+	test_grep "^fsmonitor last update" actual
 '
 
 # test that "update-index --no-fsmonitor" removes the fsmonitor extension
 test_expect_success 'update-index --no-fsmonitor" removes the fsmonitor extension' '
 	git update-index --no-fsmonitor &&
 	test-tool dump-fsmonitor >actual &&
-	grep "^no fsmonitor" actual
+	test_grep "^no fsmonitor" actual
 '
 
 cat >expect <<EOF &&
diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh
index d881e27..8619577 100755
--- a/t/t7527-builtin-fsmonitor.sh
+++ b/t/t7527-builtin-fsmonitor.sh
@@ -158,7 +158,7 @@
 	GIT_TRACE2_EVENT="$PWD/.git/trace" \
 		test-tool -C test_implicit fsmonitor-client query --token 0 >actual &&
 	nul_to_q <actual >actual.filtered &&
-	grep "builtin:" actual.filtered &&
+	test_grep "builtin:" actual.filtered &&
 
 	# confirm that a daemon was started in the background.
 	#
@@ -312,7 +312,7 @@
 	start_daemon -C test_multiple &&
 
 	test_must_fail git -C test_multiple fsmonitor--daemon start 2>actual &&
-	grep "fsmonitor--daemon is already running" actual &&
+	test_grep "fsmonitor--daemon is already running" actual &&
 
 	git -C test_multiple fsmonitor--daemon stop &&
 	test_must_fail git -C test_multiple fsmonitor--daemon status
@@ -622,7 +622,7 @@
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000001:0" >actual_1 &&
 	nul_to_q <actual_1 >actual_q1 &&
 
-	grep "file_1" actual_q1 &&
+	test_grep "file_1" actual_q1 &&
 
 	# Force a flush.  This will change the <token_id>, reset the <seq_nr>, and
 	# flush the file data.  Then create some events and ensure that the file
@@ -630,19 +630,19 @@
 
 	test-tool -C test_flush fsmonitor-client flush >flush_0 &&
 	nul_to_q <flush_0 >flush_q0 &&
-	grep "^builtin:test_00000002:0Q/Q$" flush_q0 &&
+	test_grep "^builtin:test_00000002:0Q/Q$" flush_q0 &&
 
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 &&
 	nul_to_q <actual_2 >actual_q2 &&
 
-	grep "^builtin:test_00000002:0Q$" actual_q2 &&
+	test_grep "^builtin:test_00000002:0Q$" actual_q2 &&
 
 	>test_flush/file_3 &&
 
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_3 &&
 	nul_to_q <actual_3 >actual_q3 &&
 
-	grep "file_3" actual_q3
+	test_grep "file_3" actual_q3
 '
 
 # The next few test cases create repos where the .git directory is NOT
@@ -815,7 +815,7 @@
 
 		start_daemon -C "$u" &&
 		git -C "$u" status >actual &&
-		grep "new file:   file1" actual
+		test_grep "new file:   file1" actual
 	'
 done
 
@@ -1069,9 +1069,9 @@
 	# directories and files that we touched.  We may or may not get a
 	# trailing slash on modified directories.
 	#
-	grep -E "^event: abc/?$"       ./insensitive.trace &&
-	grep -E "^event: abc/def/?$"   ./insensitive.trace &&
-	grep -E "^event: abc/def/xyz$" ./insensitive.trace
+	test_grep -E "^event: abc/?$"       ./insensitive.trace &&
+	test_grep -E "^event: abc/def/?$"   ./insensitive.trace &&
+	test_grep -E "^event: abc/def/xyz$" ./insensitive.trace
 '
 
 # The variable "unicode_debug" is defined in the following library
@@ -1113,20 +1113,20 @@
 	then
 		# We should have seen NFC event from OS.
 		# We should not have synthesized an NFD event.
-		grep -E    "^event: nfc/c_${utf8_nfc}/?$" ./unicode.trace &&
-		grep -E -v "^event: nfc/c_${utf8_nfd}/?$" ./unicode.trace
+		test_grep -E    "^event: nfc/c_${utf8_nfc}/?$" ./unicode.trace &&
+		test_grep -E -v "^event: nfc/c_${utf8_nfd}/?$" ./unicode.trace
 	else
 		# We should have seen NFD event from OS.
 		# We should have synthesized an NFC event.
-		grep -E "^event: nfc/c_${utf8_nfd}/?$" ./unicode.trace &&
-		grep -E "^event: nfc/c_${utf8_nfc}/?$" ./unicode.trace
+		test_grep -E "^event: nfc/c_${utf8_nfd}/?$" ./unicode.trace &&
+		test_grep -E "^event: nfc/c_${utf8_nfc}/?$" ./unicode.trace
 	fi &&
 
 	# We assume UNICODE_NFD_PRESERVED.
 	# We should have seen explicit NFD from OS.
 	# We should have synthesized an NFC event.
-	grep -E "^event: nfd/d_${utf8_nfd}/?$" ./unicode.trace &&
-	grep -E "^event: nfd/d_${utf8_nfc}/?$" ./unicode.trace
+	test_grep -E "^event: nfd/d_${utf8_nfd}/?$" ./unicode.trace &&
+	test_grep -E "^event: nfd/d_${utf8_nfc}/?$" ./unicode.trace
 '
 
 test_expect_success 'split-index and FSMonitor work well together' '
@@ -1240,21 +1240,21 @@
 		<"$PWD/subdir_case_wrong.log" \
 		>"$PWD/subdir_case_wrong.log1" &&
 
-	grep -q "AAA.*pos 0" "$PWD/subdir_case_wrong.log1" &&
-	grep -q "zzz.*pos 6" "$PWD/subdir_case_wrong.log1" &&
+	test_grep -q "AAA.*pos 0" "$PWD/subdir_case_wrong.log1" &&
+	test_grep -q "zzz.*pos 6" "$PWD/subdir_case_wrong.log1" &&
 
-	grep -q "dir1/DIR2/dir3/file3.*pos -3" "$PWD/subdir_case_wrong.log1" &&
+	test_grep -q "dir1/DIR2/dir3/file3.*pos -3" "$PWD/subdir_case_wrong.log1" &&
 
 	# Verify that we get a mapping event to correct the case.
-	grep -q "MAP:.*dir1/DIR2/dir3/file3.*dir1/dir2/dir3/file3" \
+	test_grep -q "MAP:.*dir1/DIR2/dir3/file3.*dir1/dir2/dir3/file3" \
 		"$PWD/subdir_case_wrong.log1" &&
 
 	# The refresh-callbacks should have caused "git status" to clear
 	# the CE_FSMONITOR_VALID bit on each of those files and caused
 	# the worktree scan to visit them and mark them as modified.
-	grep -q " M AAA" "$PWD/subdir_case_wrong.out" &&
-	grep -q " M zzz" "$PWD/subdir_case_wrong.out" &&
-	grep -q " M dir1/dir2/dir3/file3" "$PWD/subdir_case_wrong.out"
+	test_grep -q " M AAA" "$PWD/subdir_case_wrong.out" &&
+	test_grep -q " M zzz" "$PWD/subdir_case_wrong.out" &&
+	test_grep -q " M dir1/dir2/dir3/file3" "$PWD/subdir_case_wrong.out"
 '
 
 test_expect_success CASE_INSENSITIVE_FS 'fsmonitor file case wrong on disk' '
@@ -1327,10 +1327,10 @@
 	GIT_TRACE_FSMONITOR="$PWD/file_case_wrong-try1.log" \
 		git -C file_case_wrong status --short \
 			>"$PWD/file_case_wrong-try1.out" &&
-	grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try1.log" &&
-	grep -q "fsmonitor_refresh_callback.*file-3-a.*pos 4"  "$PWD/file_case_wrong-try1.log" &&
-	grep -q "fsmonitor_refresh_callback.*FILE-4-A.*pos 6"  "$PWD/file_case_wrong-try1.log" &&
-	grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try1.log" &&
+	test_grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try1.log" &&
+	test_grep -q "fsmonitor_refresh_callback.*file-3-a.*pos 4"  "$PWD/file_case_wrong-try1.log" &&
+	test_grep -q "fsmonitor_refresh_callback.*FILE-4-A.*pos 6"  "$PWD/file_case_wrong-try1.log" &&
+	test_grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try1.log" &&
 
 	# FSM refresh will have invalidated the FSM bit and cause a regular
 	# (real) scan of these tracked files, so they should have "H" status.
@@ -1338,8 +1338,8 @@
 	# command).)
 
 	git -C file_case_wrong ls-files -f >"$PWD/file_case_wrong-lsf1.out" &&
-	grep -q "H dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf1.out" &&
-	grep -q "H dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf1.out" &&
+	test_grep -q "H dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf1.out" &&
+	test_grep -q "H dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf1.out" &&
 
 
 	# Try the status again. We assume that the above status command
@@ -1348,17 +1348,17 @@
 	GIT_TRACE_FSMONITOR="$PWD/file_case_wrong-try2.log" \
 		git -C file_case_wrong status --short \
 			>"$PWD/file_case_wrong-try2.out" &&
-	! grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos" "$PWD/file_case_wrong-try2.log" &&
-	! grep -q "fsmonitor_refresh_callback.*file-3-a.*pos" "$PWD/file_case_wrong-try2.log" &&
-	! grep -q "fsmonitor_refresh_callback.*FILE-4-A.*pos" "$PWD/file_case_wrong-try2.log" &&
-	! grep -q "fsmonitor_refresh_callback.*file-4-a.*pos" "$PWD/file_case_wrong-try2.log" &&
+	test_grep ! -q "fsmonitor_refresh_callback.*FILE-3-A.*pos" "$PWD/file_case_wrong-try2.log" &&
+	test_grep ! -q "fsmonitor_refresh_callback.*file-3-a.*pos" "$PWD/file_case_wrong-try2.log" &&
+	test_grep ! -q "fsmonitor_refresh_callback.*FILE-4-A.*pos" "$PWD/file_case_wrong-try2.log" &&
+	test_grep ! -q "fsmonitor_refresh_callback.*file-4-a.*pos" "$PWD/file_case_wrong-try2.log" &&
 
 	# FSM refresh saw nothing, so it will mark all files as valid,
 	# so they should now have "h" status.
 
 	git -C file_case_wrong ls-files -f >"$PWD/file_case_wrong-lsf2.out" &&
-	grep -q "h dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf2.out" &&
-	grep -q "h dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf2.out" &&
+	test_grep -q "h dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf2.out" &&
+	test_grep -q "h dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf2.out" &&
 
 
 	# We now have files with clean content, but with case-incorrect
@@ -1373,20 +1373,20 @@
 			>"$PWD/file_case_wrong-try3.out" &&
 
 	# Verify that we get a mapping event to correct the case.
-	grep -q "fsmonitor_refresh_callback MAP:.*dir1/dir2/dir3/FILE-3-A.*dir1/dir2/dir3/file-3-a" \
+	test_grep -q "fsmonitor_refresh_callback MAP:.*dir1/dir2/dir3/FILE-3-A.*dir1/dir2/dir3/file-3-a" \
 		"$PWD/file_case_wrong-try3.log" &&
-	grep -q "fsmonitor_refresh_callback MAP:.*dir1/dir2/dir4/file-4-a.*dir1/dir2/dir4/FILE-4-A" \
+	test_grep -q "fsmonitor_refresh_callback MAP:.*dir1/dir2/dir4/file-4-a.*dir1/dir2/dir4/FILE-4-A" \
 		"$PWD/file_case_wrong-try3.log" &&
 
 	# FSEvents are in observed case.
-	grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try3.log" &&
-	grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try3.log" &&
+	test_grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try3.log" &&
+	test_grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try3.log" &&
 
 	# The refresh-callbacks should have caused "git status" to clear
 	# the CE_FSMONITOR_VALID bit on each of those files and caused
 	# the worktree scan to visit them and mark them as modified.
-	grep -q " M dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-try3.out" &&
-	grep -q " M dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-try3.out"
+	test_grep -q " M dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-try3.out" &&
+	test_grep -q " M dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-try3.out"
 '
 
 test_done
diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh
index b50306b..4439646 100755
--- a/t/t7528-signed-commit-ssh.sh
+++ b/t/t7528-signed-commit-ssh.sh
@@ -133,8 +133,8 @@
 		do
 			git verify-commit $commit &&
 			git show --pretty=short --show-signature $commit >actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -144,8 +144,8 @@
 		do
 			test_must_fail git verify-commit $commit &&
 			git show --pretty=short --show-signature $commit >actual &&
-			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -153,9 +153,9 @@
 		for commit in eighth-signed-alt twelfth-signed-alt
 		do
 			git show --pretty=short --show-signature $commit >actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
-			grep "${GPGSSH_KEY_NOT_TRUSTED}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_KEY_NOT_TRUSTED}" actual &&
 			echo $commit OK || exit 1
 		done
 	)
@@ -164,34 +164,34 @@
 test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-commit eighth-signed-alt 2>actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
-	grep "${GPGSSH_KEY_NOT_TRUSTED}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
+	test_grep "${GPGSSH_KEY_NOT_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on expired signature key' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-commit expired-signed 2>actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on not yet valid signature key' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-commit notyetvalid-signed 2>actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit succeeds with commit date and key validity matching' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	git verify-commit timeboxedvalid-signed 2>actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual
 '
 
 test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure with commit date outside of key validity' '
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_must_fail git verify-commit timeboxedinvalid-signed 2>actual &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '
@@ -217,8 +217,8 @@
 		for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
 		do
 			git verify-commit --raw $commit 2>actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -226,8 +226,8 @@
 		for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned
 		do
 			test_must_fail git verify-commit --raw $commit 2>actual &&
-			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $commit OK || exit 1
 		done
 	) &&
@@ -235,8 +235,8 @@
 		for commit in eighth-signed-alt
 		do
 			test_must_fail git verify-commit --raw $commit 2>actual &&
-			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
-			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
+			test_grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual &&
 			echo $commit OK || exit 1
 		done
 	)
@@ -244,7 +244,7 @@
 
 test_expect_success GPGSSH 'proper header is used for hash algorithm' '
 	git cat-file commit fourth-signed >output &&
-	grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output
+	test_grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output
 '
 
 test_expect_success GPGSSH 'show signed commit with signature' '
@@ -268,9 +268,9 @@
 	git hash-object -w -t commit forged1 >forged1.commit &&
 	test_must_fail git verify-commit $(cat forged1.commit) &&
 	git show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&
-	grep "${GPGSSH_BAD_SIGNATURE}" actual1 &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual1 &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual1
+	test_grep "${GPGSSH_BAD_SIGNATURE}" actual1 &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual1 &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual1
 '
 
 test_expect_success GPGSSH 'detect fudged signature with NUL' '
@@ -281,8 +281,8 @@
 	git hash-object --literally -w -t commit forged2 >forged2.commit &&
 	test_must_fail git verify-commit $(cat forged2.commit) &&
 	git show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&
-	grep "${GPGSSH_BAD_SIGNATURE}" actual2 &&
-	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual2
+	test_grep "${GPGSSH_BAD_SIGNATURE}" actual2 &&
+	test_grep ! "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual2
 '
 
 test_expect_success GPGSSH 'amending already signed commit' '
@@ -293,8 +293,8 @@
 	git commit --amend -S --no-edit &&
 	git verify-commit HEAD &&
 	git show -s --show-signature HEAD >actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
-	! grep "${GPGSSH_BAD_SIGNATURE}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
+	test_grep ! "${GPGSSH_BAD_SIGNATURE}" actual
 '
 
 test_expect_success GPGSSH 'show good signature with custom format' '
@@ -386,7 +386,7 @@
 	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
 	test_config log.showsignature true &&
 	git show initial >actual &&
-	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual
 '
 
 test_expect_success GPGSSH 'check config gpg.format values' '
@@ -410,8 +410,8 @@
 	git hash-object -w -t commit double-commit >double-commit.commit &&
 	test_must_fail git verify-commit $(cat double-commit.commit) &&
 	git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&
-	grep "BAD signature from" double-actual &&
-	grep "Good signature from" double-actual
+	test_grep "BAD signature from" double-actual &&
+	test_grep "Good signature from" double-actual
 '
 
 test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '
@@ -463,8 +463,8 @@
 	head=$(git hash-object -t commit -w commit) &&
 	git reset --hard $head &&
 	git verify-commit $head 2>actual &&
-	grep "Good signature from" actual &&
-	! grep "BAD signature from" actual
+	test_grep "Good signature from" actual &&
+	test_grep ! "BAD signature from" actual
 '
 
 test_done
diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh
index f877d9a..7f2a1db 100755
--- a/t/t7600-merge.sh
+++ b/t/t7600-merge.sh
@@ -173,7 +173,7 @@
 		cd broken &&
 		git init &&
 		>.git/index &&
-		test_expect_code 129 git merge -h >usage
+		git merge -h >usage
 	) &&
 	test_grep "[Uu]sage: git merge" broken/usage
 '
@@ -342,8 +342,8 @@
 	>unrelated &&
 	git add unrelated &&
 	test_must_fail git merge --squash c7 --autostash >out 2>err &&
-	! grep "Applying autostash resulted in conflicts." err &&
-	grep "When finished, apply stashed changes with \`git stash pop\`" out
+	test_grep ! "Applying autostash resulted in conflicts." err &&
+	test_grep "When finished, apply stashed changes with \`git stash pop\`" out
 '
 
 test_expect_success 'merge c3 with c7 with commit.cleanup = scissors' '
@@ -990,7 +990,7 @@
 	EDITOR=./editor git merge --no-ff --edit c1 &&
 	verify_parents $c0 $c1 &&
 	git cat-file commit HEAD >raw &&
-	grep "work done on the side branch" raw &&
+	test_grep "work done on the side branch" raw &&
 	sed "1,/^$/d" >actual raw &&
 	test_cmp expected actual
 '
@@ -1157,13 +1157,13 @@
 	git pack-refs --all --prune &&
 
 	test_must_fail git merge not-local 2>stderr &&
-	grep origin/not-local stderr
+	test_grep origin/not-local stderr
 '
 
 test_expect_success 'suggested names are not ambiguous' '
 	git update-ref refs/heads/origin/not-local HEAD &&
 	test_must_fail git merge not-local 2>stderr &&
-	grep remotes/origin/not-local stderr
+	test_grep remotes/origin/not-local stderr
 '
 
 test_done
diff --git a/t/t7603-merge-reduce-heads.sh b/t/t7603-merge-reduce-heads.sh
index 1f8c3b7..92007dc 100755
--- a/t/t7603-merge-reduce-heads.sh
+++ b/t/t7603-merge-reduce-heads.sh
@@ -59,11 +59,11 @@
 	test_path_is_file c4.c &&
 	test_path_is_file c5.c &&
 	git show --format=%s -s >actual &&
-	! grep c1 actual &&
-	grep c2 actual &&
-	grep c3 actual &&
-	! grep c4 actual &&
-	grep c5 actual
+	test_grep ! c1 actual &&
+	test_grep c2 actual &&
+	test_grep c3 actual &&
+	test_grep ! c4 actual &&
+	test_grep c5 actual
 '
 
 test_expect_success 'pull c2, c3, c4, c5 into c1' '
@@ -82,11 +82,11 @@
 	test_path_is_file c4.c &&
 	test_path_is_file c5.c &&
 	git show --format=%s -s >actual &&
-	! grep c1 actual &&
-	grep c2 actual &&
-	grep c3 actual &&
-	! grep c4 actual &&
-	grep c5 actual
+	test_grep ! c1 actual &&
+	test_grep c2 actual &&
+	test_grep c3 actual &&
+	test_grep ! c4 actual &&
+	test_grep c5 actual
 '
 
 test_expect_success 'setup' '
diff --git a/t/t7606-merge-custom.sh b/t/t7606-merge-custom.sh
index 81fb7c4..0ae2f59 100755
--- a/t/t7606-merge-custom.sh
+++ b/t/t7606-merge-custom.sh
@@ -60,7 +60,7 @@
 	test_cmp second-parent.expected second-parent &&
 	test_cmp tree.expected tree &&
 	test -f c0.c &&
-	grep c1c1 c1.c &&
+	test_grep c1c1 c1.c &&
 	test -f c2.c
 '
 
diff --git a/t/t7607-merge-state.sh b/t/t7607-merge-state.sh
index 89a62ac..bfe782d 100755
--- a/t/t7607-merge-state.sh
+++ b/t/t7607-merge-state.sh
@@ -20,8 +20,8 @@
 	# just hit conflicts, it completely fails and says that it cannot
 	# handle this type of merge.
 	test_expect_code 2 git merge branch2 branch3 >output 2>&1 &&
-	grep "fatal: merge program failed" output &&
-	grep "Should not be doing an octopus" output &&
+	test_grep "fatal: merge program failed" output &&
+	test_grep "Should not be doing an octopus" output &&
 
 	# Make sure we did not leave stray changes around when no appropriate
 	# merge strategy was found
diff --git a/t/t7610-mergetool.sh b/t/t7610-mergetool.sh
index 957f8e2..0128b14 100755
--- a/t/t7610-mergetool.sh
+++ b/t/t7610-mergetool.sh
@@ -738,7 +738,7 @@
 	test_config mergetool.myecho.trustExitCode true &&
 	test_must_fail git merge main &&
 	git mergetool --no-prompt --tool myecho -- both >actual &&
-	grep ^\./both_LOCAL_ actual
+	test_grep ^\./both_LOCAL_ actual
 '
 
 test_lazy_prereq MKTEMP '
@@ -755,8 +755,8 @@
 	test_config mergetool.myecho.trustExitCode true &&
 	test_must_fail git merge main &&
 	git mergetool --no-prompt --tool myecho -- both >actual &&
-	! grep ^\./both_LOCAL_ actual &&
-	grep /both_LOCAL_ actual
+	test_grep ! ^\./both_LOCAL_ actual &&
+	test_grep /both_LOCAL_ actual
 '
 
 test_expect_success 'diff.orderFile configuration is honored' '
@@ -816,12 +816,12 @@
 test_expect_success 'mergetool --tool-help shows recognized tools' '
 	# Check a few known tools are correctly shown
 	git mergetool --tool-help >mergetools &&
-	grep vimdiff mergetools &&
-	grep vimdiff3 mergetools &&
-	grep gvimdiff2 mergetools &&
-	grep araxis mergetools &&
-	grep xxdiff mergetools &&
-	grep meld mergetools
+	test_grep vimdiff mergetools &&
+	test_grep vimdiff3 mergetools &&
+	test_grep gvimdiff2 mergetools &&
+	test_grep araxis mergetools &&
+	test_grep xxdiff mergetools &&
+	test_grep meld mergetools
 '
 
 test_expect_success 'mergetool hideResolved' '
diff --git a/t/t7614-merge-signoff.sh b/t/t7614-merge-signoff.sh
index fee258d..e58bf07 100755
--- a/t/t7614-merge-signoff.sh
+++ b/t/t7614-merge-signoff.sh
@@ -45,7 +45,8 @@
 	test_commit main-branch-2 file2 2 &&
 	git checkout other-branch &&
 	git merge main --signoff --no-edit &&
-	git cat-file commit HEAD | sed -e "1,/^\$/d" >actual &&
+	git cat-file commit HEAD >commit &&
+	sed -e "1,/^\$/d" commit >actual &&
 	test_cmp expected-signed actual
 '
 
@@ -55,7 +56,8 @@
 	test_commit main-branch-3 file3 3 &&
 	git checkout other-branch &&
 	git merge main --no-edit &&
-	git cat-file commit HEAD | sed -e "1,/^\$/d" >actual &&
+	git cat-file commit HEAD >commit &&
+	sed -e "1,/^\$/d" commit >actual &&
 	test_cmp expected-unsigned actual
 '
 
@@ -65,7 +67,8 @@
 	test_commit main-branch-4 file4 4 &&
 	git checkout other-branch &&
 	git merge main --no-edit --signoff --no-signoff &&
-	git cat-file commit HEAD | sed -e "1,/^\$/d" >actual &&
+	git cat-file commit HEAD >commit &&
+	sed -e "1,/^\$/d" commit >actual &&
 	test_cmp expected-unsigned actual
 '
 
diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh
index 63ef63f..f0a390e 100755
--- a/t/t7700-repack.sh
+++ b/t/t7700-repack.sh
@@ -177,7 +177,7 @@
 	rm -f .git/objects/pack/* &&
 	mv pack-* .git/objects/pack/ &&
 	git verify-pack -v -- .git/objects/pack/*.idx >packlist &&
-	! grep "^$coid " packlist &&
+	test_grep ! "^$coid " packlist &&
 	echo >.git/objects/info/alternates &&
 	test_must_fail git show $coid
 '
@@ -194,7 +194,7 @@
 	rm -f .git/objects/pack/* &&
 	mv pack-* .git/objects/pack/ &&
 	git verify-pack -v -- .git/objects/pack/*.idx >packlist &&
-	! grep "^$coid " &&
+	test_grep ! "^$coid " packlist &&
 	echo >.git/objects/info/alternates &&
 	test_must_fail git show $coid
 '
@@ -226,8 +226,8 @@
 		test_line_count = 4 old-counts &&
 		git repack -a -d --keep-pack $P1 --keep-pack $P4 &&
 		ls .git/objects/pack/*.pack >new-counts &&
-		grep -q $P1 new-counts &&
-		grep -q $P4 new-counts &&
+		test_grep -q $P1 new-counts &&
+		test_grep -q $P4 new-counts &&
 		test_line_count = 3 new-counts &&
 		git fsck &&
 
@@ -276,7 +276,7 @@
 
 		test_must_fail git fsck &&
 		test_must_fail env GIT_COMMIT_GRAPH_PARANOIA=true git repack --cruft -d 2>err &&
-		grep "bad object" err &&
+		test_grep "bad object" err &&
 
 		# Before failing, the repack did not modify the
 		# pack directory.
@@ -460,8 +460,8 @@
 
 		# Object bar is in both the old .keep pack and the new
 		# pack that contained the filtered out objects
-		grep "$bar_pack" bar_pack_1 &&
-		grep "$foo_pack_1" bar_pack_1 &&
+		test_grep "$bar_pack" bar_pack_1 &&
+		test_grep "$foo_pack_1" bar_pack_1 &&
 		test "$foo_pack_1" != "$head_pack_1"
 	)
 '
diff --git a/t/t7703-repack-geometric.sh b/t/t7703-repack-geometric.sh
index 04d5d8f..f3a0650 100755
--- a/t/t7703-repack-geometric.sh
+++ b/t/t7703-repack-geometric.sh
@@ -112,7 +112,7 @@
 		find $objdir/pack -name "*.pack" | sort >after &&
 		test_line_count = 3 after &&
 		comm -3 small before | tr -d "\t" >large &&
-		grep -qFf large after
+		test_grep -qFf large after
 	)
 '
 
@@ -210,7 +210,7 @@
 
 		# Packs should not have changed (only one non-kept pack, no
 		# loose objects), but $midx should now exist.
-		grep "Nothing new to pack" out &&
+		test_grep "Nothing new to pack" out &&
 		test_path_is_file $midx &&
 
 		test_cmp packs.before packs.after &&
diff --git a/t/t7704-repack-cruft.sh b/t/t7704-repack-cruft.sh
index 9e03b04..b342e82 100755
--- a/t/t7704-repack-cruft.sh
+++ b/t/t7704-repack-cruft.sh
@@ -166,9 +166,9 @@
 		test-tool pack-mtimes $(basename "$cruft_foo") >foo.objects &&
 		test-tool pack-mtimes $(basename "$cruft_bar") >bar.objects &&
 
-		grep "^$foo" foo.objects &&
+		test_grep "^$foo" foo.objects &&
 		test_line_count = 1 foo.objects &&
-		grep "^$bar" bar.objects &&
+		test_grep "^$bar" bar.objects &&
 		test_line_count = 1 bar.objects
 	)
 '
@@ -188,8 +188,8 @@
 		cruft=$(ls $packdir/pack-*.mtimes) &&
 		test-tool pack-mtimes $(basename "$cruft") >cruft.objects &&
 
-		grep "^$foo" cruft.objects &&
-		grep "^$bar" cruft.objects &&
+		test_grep "^$foo" cruft.objects &&
+		test_grep "^$bar" cruft.objects &&
 		test_line_count = 2 cruft.objects
 	)
 '
@@ -693,7 +693,7 @@
 		git -c pack.window=1 -c repack.cruftWindow=2 repack \
 		       --cruft --window=3 &&
 
-		grep "pack-objects.*--window=2.*--cruft" event.trace
+		test_grep "pack-objects.*--window=2.*--cruft" event.trace
 	)
 '
 
@@ -708,7 +708,7 @@
 		GIT_TRACE2_EVENT=$(pwd)/event.trace \
 		git -c pack.window=2 repack --cruft --window=3 &&
 
-		grep "pack-objects.*--window=3.*--cruft" event.trace
+		test_grep "pack-objects.*--window=3.*--cruft" event.trace
 	)
 '
 
diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh
index 8a91ff3..0079aa9 100755
--- a/t/t7800-difftool.sh
+++ b/t/t7800-difftool.sh
@@ -27,12 +27,11 @@
 }
 
 test_expect_success 'basic usage requires no repo' '
-	test_expect_code 129 git difftool -h >output &&
+	git difftool -h >output &&
 	test_grep ^usage: output &&
 	# create a ceiling directory to prevent Git from finding a repo
 	mkdir -p not/repo &&
 	test_when_finished rm -r not &&
-	test_expect_code 129 \
 	env GIT_CEILING_DIRECTORIES="$(pwd)/not" \
 	git -C not/repo difftool -h >output &&
 	test_grep ^usage: output
@@ -431,32 +430,32 @@
 test_expect_success 'say no to the first file' '
 	(echo n && echo) >input &&
 	git difftool -x cat branch <input >output &&
-	grep m2 output &&
-	grep br2 output &&
-	! grep main output &&
-	! grep branch output
+	test_grep m2 output &&
+	test_grep br2 output &&
+	test_grep ! main output &&
+	test_grep ! branch output
 '
 
 test_expect_success 'say no to the second file' '
 	(echo && echo n) >input &&
 	git difftool -x cat branch <input >output &&
-	grep main output &&
-	grep branch output &&
-	! grep m2 output &&
-	! grep br2 output
+	test_grep main output &&
+	test_grep branch output &&
+	test_grep ! m2 output &&
+	test_grep ! br2 output
 '
 
 test_expect_success 'ending prompt input with EOF' '
 	git difftool -x cat branch </dev/null >output &&
-	! grep main output &&
-	! grep branch output &&
-	! grep m2 output &&
-	! grep br2 output
+	test_grep ! main output &&
+	test_grep ! branch output &&
+	test_grep ! m2 output &&
+	test_grep ! br2 output
 '
 
 test_expect_success 'difftool --tool-help' '
 	git difftool --tool-help >output &&
-	grep tool output
+	test_grep tool output
 '
 
 test_expect_success 'setup change in subdirectory' '
diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh
index d61c4a4..ac7ea9c 100755
--- a/t/t7810-grep.sh
+++ b/t/t7810-grep.sh
@@ -1101,19 +1101,19 @@
 '
 
 test_expect_success ' includes preceding comment' '
-	grep "# Say hello" function-context-userdiff-actual
+	test_grep "# Say hello" function-context-userdiff-actual
 '
 
 test_expect_success ' includes function line' '
-	grep "=function hello" function-context-userdiff-actual
+	test_grep "=function hello" function-context-userdiff-actual
 '
 
 test_expect_success ' includes matching line' '
-	grep ":  echo" function-context-userdiff-actual
+	test_grep ":  echo" function-context-userdiff-actual
 '
 
 test_expect_success ' includes last line of the function' '
-	grep "} # hello" function-context-userdiff-actual
+	test_grep "} # hello" function-context-userdiff-actual
 '
 
 for threads in $(test_seq 0 10)
@@ -1137,16 +1137,16 @@
 	git grep --threads=2 Hello hello_world 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 1 warnings &&
-	grep -F "no threads support, ignoring --threads" err &&
+	test_grep -F "no threads support, ignoring --threads" err &&
 	git -c grep.threads=2 grep Hello hello_world 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 1 warnings &&
-	grep -F "no threads support, ignoring grep.threads" err &&
+	test_grep -F "no threads support, ignoring grep.threads" err &&
 	git -c grep.threads=2 grep --threads=4 Hello hello_world 2>err &&
 	grep ^warning: err >warnings &&
 	test_line_count = 2 warnings &&
-	grep -F "no threads support, ignoring --threads" err &&
-	grep -F "no threads support, ignoring grep.threads" err &&
+	test_grep -F "no threads support, ignoring --threads" err &&
+	test_grep -F "no threads support, ignoring grep.threads" err &&
 	git -c grep.threads=0 grep --threads=0 Hello hello_world 2>err &&
 	test_line_count = 0 err
 '
@@ -1267,21 +1267,21 @@
 		export GIT_CEILING_DIRECTORIES &&
 		cd non/git &&
 		test_expect_code 128 git grep --no-index search .. 2>error &&
-		grep "is outside the directory tree" error
+		test_grep "is outside the directory tree" error
 	) &&
 	(
 		GIT_CEILING_DIRECTORIES="$(pwd)/non" &&
 		export GIT_CEILING_DIRECTORIES &&
 		cd non/git &&
 		test_expect_code 128 git grep --no-index search ../tig 2>error &&
-		grep "is outside the directory tree" error
+		test_grep "is outside the directory tree" error
 	) &&
 	(
 		GIT_CEILING_DIRECTORIES="$(pwd)/non" &&
 		export GIT_CEILING_DIRECTORIES &&
 		cd non/git &&
 		test_expect_code 128 git grep --no-index search ../non 2>error &&
-		grep "no such path in the working tree" error
+		test_grep "no such path in the working tree" error
 	)
 '
 
diff --git a/t/t7814-grep-recurse-submodules.sh b/t/t7814-grep-recurse-submodules.sh
index 167fe66..e1cf53d 100755
--- a/t/t7814-grep-recurse-submodules.sh
+++ b/t/t7814-grep-recurse-submodules.sh
@@ -590,7 +590,7 @@
 			--recurse-submodules HEAD^ >actual &&
 		test_cmp expect actual &&
 		# Verify that we actually fetched data from the promisor remote:
-		grep \"category\":\"promisor\",\"key\":\"fetch_count\",\"value\":\"1\" trace2.log
+		test_grep \"category\":\"promisor\",\"key\":\"fetch_count\",\"value\":\"1\" trace2.log
 	)
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 6d87da2..4238569 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -41,7 +41,7 @@
 }
 
 test_expect_success 'help text' '
-	test_expect_code 129 git maintenance -h >actual &&
+	git maintenance -h >actual &&
 	test_grep "usage: git maintenance <subcommand>" actual &&
 	test_expect_code 129 git maintenance barf 2>err &&
 	test_grep "unknown subcommand: \`barf'\''" err &&
@@ -323,7 +323,7 @@
 	test_cmp_rev refs/remotes/remote2/two refs/prefetch/remotes/remote2/two &&
 
 	git log --oneline --decorate --all >log &&
-	! grep "prefetch" log &&
+	test_grep ! "prefetch" log &&
 
 	test_when_finished git config --unset remote.remote1.skipFetchAll &&
 	git config remote.remote1.skipFetchAll true &&
@@ -413,15 +413,15 @@
 
 	GIT_PROGRESS_DELAY=0 \
 	git -C loose-batch maintenance run --no-quiet --task=loose-objects 2>err &&
-	grep "Enumerating objects: 50, done." err &&
+	test_grep "Enumerating objects: 50, done." err &&
 
 	GIT_PROGRESS_DELAY=0 \
 	git -C loose-batch maintenance run --no-quiet --task=loose-objects 2>err &&
-	grep "Enumerating objects: 50, done." err &&
+	test_grep "Enumerating objects: 50, done." err &&
 
 	GIT_PROGRESS_DELAY=0 \
 	git -C loose-batch maintenance run --no-quiet --task=loose-objects 2>err &&
-	grep "Enumerating objects: 2, done." err &&
+	test_grep "Enumerating objects: 2, done." err &&
 
 	GIT_PROGRESS_DELAY=0 \
 	git -C loose-batch maintenance run --no-quiet --task=loose-objects 2>err &&
@@ -479,36 +479,42 @@
 '
 
 test_expect_success EXPENSIVE 'incremental-repack 2g limit' '
-	test_config core.compression 0 &&
+	test_when_finished rm -rf expensive-repo &&
+	git init expensive-repo &&
+	(
+		cd expensive-repo &&
+		git config set core.compression 0 &&
+		git config set maintenance.auto false &&
 
-	for i in $(test_seq 1 5)
-	do
-		test-tool genrandom foo$i $((512 * 1024 * 1024 + 1)) >>big ||
-		return 1
-	done &&
-	git add big &&
-	git commit -qm "Add big file (1)" &&
+		for i in $(test_seq 1 5)
+		do
+			test-tool genrandom foo$i $((512 * 1024 * 1024 + 1)) >>big ||
+			return 1
+		done &&
+		git add big &&
+		git commit -qm "Add big file (1)" &&
 
-	# ensure any possible loose objects are in a pack-file
-	git maintenance run --task=loose-objects &&
+		# ensure any possible loose objects are in a pack-file
+		git maintenance run --task=loose-objects &&
 
-	rm big &&
-	for i in $(test_seq 6 10)
-	do
-		test-tool genrandom foo$i $((512 * 1024 * 1024 + 1)) >>big ||
-		return 1
-	done &&
-	git add big &&
-	git commit -qm "Add big file (2)" &&
+		rm big &&
+		for i in $(test_seq 6 10)
+		do
+			test-tool genrandom foo$i $((512 * 1024 * 1024 + 1)) >>big ||
+			return 1
+		done &&
+		git add big &&
+		git commit -qm "Add big file (2)" &&
 
-	# ensure any possible loose objects are in a pack-file
-	git maintenance run --task=loose-objects &&
+		# ensure any possible loose objects are in a pack-file
+		git maintenance run --task=loose-objects &&
 
-	# Now run the incremental-repack task and check the batch-size
-	GIT_TRACE2_EVENT="$(pwd)/run-2g.txt" git maintenance run \
-		--task=incremental-repack 2>/dev/null &&
-	test_subcommand git multi-pack-index repack \
-		 --no-progress --batch-size=2147483647 <run-2g.txt
+		# Now run the incremental-repack task and check the batch-size
+		GIT_TRACE2_EVENT="$(pwd)/run-2g.txt" git maintenance run \
+			--task=incremental-repack 2>/dev/null &&
+		test_subcommand git multi-pack-index repack \
+			--no-progress --batch-size=2147483647 <run-2g.txt
+	)
 '
 
 run_incremental_repack_and_verify () {
@@ -1203,7 +1209,7 @@
 	test_when_finished git config --global --unset-all maintenance.repo &&
 
 	test_must_fail git maintenance unregister 2>err &&
-	grep "is not registered" err &&
+	test_grep "is not registered" err &&
 	git maintenance unregister --force &&
 
 	git config --global --add maintenance.repo /existing1 &&
@@ -1237,11 +1243,11 @@
 	test_cmp before actual &&
 
 	test_must_fail git maintenance unregister 2>err &&
-	grep "is not registered" err &&
+	test_grep "is not registered" err &&
 	git maintenance unregister --force &&
 
 	test_must_fail git maintenance unregister --config-file ./other 2>err &&
-	grep "is not registered" err &&
+	test_grep "is not registered" err &&
 	git maintenance unregister --config-file ./other --force
 '
 
@@ -1330,9 +1336,9 @@
 	# start registers the repo
 	git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
 
-	grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=daily" cron.txt &&
-	grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=hourly" cron.txt &&
-	grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
+	test_grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=daily" cron.txt &&
+	test_grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=hourly" cron.txt &&
+	test_grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
 '
 
 test_expect_success 'stop from existing schedule' '
@@ -1349,7 +1355,7 @@
 test_expect_success 'start preserves existing schedule' '
 	echo "Important information!" >cron.txt &&
 	GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance start --scheduler=crontab &&
-	grep "Important information!" cron.txt
+	test_grep "Important information!" cron.txt
 '
 
 test_expect_success 'magic markers are correct' '
@@ -1364,8 +1370,8 @@
 test_expect_success 'stop preserves surrounding schedule' '
 	echo "Crucial information!" >>cron.txt &&
 	GIT_TEST_MAINT_SCHEDULER="crontab:test-tool crontab cron.txt" git maintenance stop &&
-	grep "Important information!" cron.txt &&
-	grep "Crucial information!" cron.txt
+	test_grep "Important information!" cron.txt &&
+	test_grep "Crucial information!" cron.txt
 '
 
 test_expect_success 'start and stop macOS maintenance' '
@@ -1395,7 +1401,7 @@
 	do
 		PLIST="$pfx/Library/LaunchAgents/org.git-scm.git.$frequency.plist" &&
 		test_xmllint "$PLIST" &&
-		grep schedule=$frequency "$PLIST" &&
+		test_grep schedule=$frequency "$PLIST" &&
 		echo "bootout gui/[UID] $PLIST" >>expect &&
 		echo "bootstrap gui/[UID] $PLIST" >>expect || return 1
 	done &&
@@ -1452,7 +1458,7 @@
 
 	for frequency in hourly daily weekly
 	do
-		grep "/create /tn Git Maintenance ($frequency) /f /xml" args &&
+		test_grep "/create /tn Git Maintenance ($frequency) /f /xml" args &&
 		file=$(ls .git/schedule_${frequency}*.xml) &&
 		test_xmllint "$file" || return 1
 	done &&
@@ -1491,8 +1497,8 @@
 	test_systemd_analyze_verify "systemd/user/git-maintenance@daily.service" &&
 	test_systemd_analyze_verify "systemd/user/git-maintenance@weekly.service" &&
 
-	grep "core.askPass=true" "systemd/user/git-maintenance@.service" &&
-	grep "credential.interactive=false" "systemd/user/git-maintenance@.service" &&
+	test_grep "core.askPass=true" "systemd/user/git-maintenance@.service" &&
+	test_grep "credential.interactive=false" "systemd/user/git-maintenance@.service" &&
 
 	printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
 	test_cmp expect args &&
diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh
index 7822947..bf04b82 100755
--- a/t/t8002-blame.sh
+++ b/t/t8002-blame.sh
@@ -113,8 +113,7 @@
 '
 
 test_expect_success 'blame --abbrev=<n> works' '
-	# non-boundary commits get +1 for alignment
-	check_abbrev 31 --abbrev=30 HEAD &&
+	check_abbrev 30 --abbrev=30 HEAD &&
 	check_abbrev 30 --abbrev=30 ^HEAD
 '
 
@@ -141,10 +140,8 @@
 '
 
 test_expect_success 'blame --abbrev -b truncates the blank boundary' '
-	# Note that `--abbrev=` always gets incremented by 1, which is why we
-	# expect 11 leading spaces and not 10.
 	cat >expect <<-EOF &&
-	$(printf "%11s" "") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev
+	$(printf "%10s" "") (<author@example.com> 2005-04-07 15:45:13 -0700 1) abbrev
 	EOF
 	git blame -b --abbrev=10 ^HEAD -- abbrev.t >actual &&
 	test_cmp expect actual
diff --git a/t/t8008-blame-formats.sh b/t/t8008-blame-formats.sh
index c12a419..016a837 100755
--- a/t/t8008-blame-formats.sh
+++ b/t/t8008-blame-formats.sh
@@ -103,7 +103,7 @@
 			"committer C <c@d.e> 123456789 +0000" |
 		git hash-object -w -t commit --stdin) &&
 		git blame --porcelain $commit -- single-file >output &&
-		grep "^summary oneline$" output
+		test_grep "^summary oneline$" output
 	)
 '
 
diff --git a/t/t8010-cat-file-filters.sh b/t/t8010-cat-file-filters.sh
index b3be2aa..a4afb33 100755
--- a/t/t8010-cat-file-filters.sh
+++ b/t/t8010-cat-file-filters.sh
@@ -49,7 +49,7 @@
 	sha1=$(git rev-parse -q --verify HEAD:world.txt) &&
 	test_must_fail git cat-file --path=hello.txt blob $sha1 >actual 2>err &&
 	test_must_be_empty actual &&
-	grep "path.*needs.*filters" err
+	test_grep "path.*needs.*filters" err
 '
 
 test_expect_success '--textconv/--filters complain without path' '
diff --git a/t/t8012-blame-colors.sh b/t/t8012-blame-colors.sh
index 5562eba..d2653b3 100755
--- a/t/t8012-blame-colors.sh
+++ b/t/t8012-blame-colors.sh
@@ -66,7 +66,7 @@
 
 	grep "<CYAN>" <actual >colored &&
 	test_line_count = 1 colored &&
-	grep qfunc colored
+	test_grep qfunc colored
 '
 
 test_done
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index e7ab645..e9d814a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -348,9 +348,9 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches \
 		2>errors &&
-		grep "^From: A U Thor <author@example.com>\$" msgtxt1 &&
-		grep "^To: to@example.com\$" msgtxt1 &&
-		grep "^In-Reply-To: <my-message-id@example.com>" msgtxt1
+		test_grep "^From: A U Thor <author@example.com>\$" msgtxt1 &&
+		test_grep "^To: to@example.com\$" msgtxt1 &&
+		test_grep "^In-Reply-To: <my-message-id@example.com>" msgtxt1
 '
 
 test_expect_success $PREREQ,AUTOIDENT 'implicit ident is allowed' '
@@ -403,7 +403,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		tocmd.patch \
 		&&
-	grep "^To: tocmd@example.com" msgtxt1
+	test_grep "^To: tocmd@example.com" msgtxt1
 '
 
 test_expect_success $PREREQ 'cccmd works' '
@@ -417,7 +417,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		cccmd.patch \
 		&&
-	grep "^	cccmd@example.com" msgtxt1
+	test_grep "^	cccmd@example.com" msgtxt1
 '
 
 test_expect_success $PREREQ 'headercmd works' '
@@ -431,7 +431,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		headercmd.patch \
 		&&
-	grep "^X-Debbugs-CC: dummy@example.com" msgtxt1
+	test_grep "^X-Debbugs-CC: dummy@example.com" msgtxt1
 '
 
 test_expect_success $PREREQ '--no-header-cmd works' '
@@ -446,7 +446,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		headercmd.patch \
 		&&
-	! grep "^X-Debbugs-CC: dummy@example.com" msgtxt1
+	test_grep ! "^X-Debbugs-CC: dummy@example.com" msgtxt1
 '
 
 test_expect_success $PREREQ 'multiline fields are correctly unfolded' '
@@ -464,7 +464,7 @@
 		--header-cmd=./headercmd-multiline \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		headercmd.patch &&
-	grep "^FoldedField: This is a tale best told using multiple lines.$" msgtxt1
+	test_grep "^FoldedField: This is a tale best told using multiple lines.$" msgtxt1
 '
 
 # Blank lines in the middle of the output of a command are invalid.
@@ -521,7 +521,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
 	sed "1,/^\$/d" <msgtxt1 >msgbody1 &&
-	grep "From: A <author@example.com>" msgbody1
+	test_grep "From: A <author@example.com>" msgbody1
 '
 
 test_expect_success $PREREQ 'Author From: not in message body' '
@@ -532,7 +532,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
 	sed "1,/^\$/d" <msgtxt1 >msgbody1 &&
-	! grep "From: A <author@example.com>" msgbody1
+	test_grep ! "From: A <author@example.com>" msgbody1
 '
 
 test_expect_success $PREREQ 'allow long lines with --no-validate' '
@@ -553,7 +553,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--transfer-encoding=auto \
 		$patches &&
-	grep "Content-Transfer-Encoding: 8bit" msgtxt1
+	test_grep "Content-Transfer-Encoding: 8bit" msgtxt1
 '
 
 test_expect_success $PREREQ 'long lines with auto encoding are quoted-printable' '
@@ -565,7 +565,7 @@
 		--transfer-encoding=auto \
 		--no-validate \
 		longline.patch &&
-	grep "Content-Transfer-Encoding: quoted-printable" msgtxt1
+	test_grep "Content-Transfer-Encoding: quoted-printable" msgtxt1
 '
 
 test_expect_success $PREREQ 'carriage returns with auto encoding are quoted-printable' '
@@ -579,7 +579,7 @@
 		--transfer-encoding=auto \
 		--no-validate \
 		cr.patch &&
-	grep "Content-Transfer-Encoding: quoted-printable" msgtxt1
+	test_grep "Content-Transfer-Encoding: quoted-printable" msgtxt1
 '
 
 for enc in auto quoted-printable base64
@@ -705,7 +705,7 @@
 			--smtp-server="$(pwd)/fake.sendmail" \
 			--transfer-encoding=$enc \
 			$patches &&
-		grep "Content-Transfer-Encoding: $enc" msgtxt1
+		test_grep "Content-Transfer-Encoding: $enc" msgtxt1
 	'
 done
 
@@ -718,7 +718,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches \
 		2>errors &&
-	! grep "^In-Reply-To: < *>" msgtxt1
+	test_grep ! "^In-Reply-To: < *>" msgtxt1
 '
 
 test_expect_success $PREREQ 'Valid In-Reply-To when prompting' '
@@ -729,7 +729,7 @@
 	) | GIT_SEND_EMAIL_NOTTY=1 git send-email \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches 2>errors &&
-	! grep "^In-Reply-To: < *>" msgtxt1
+	test_grep ! "^In-Reply-To: < *>" msgtxt1
 '
 
 test_expect_success $PREREQ 'In-Reply-To without --chain-reply-to' '
@@ -792,8 +792,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches 2>err &&
-	grep "I am about to error" err &&
-	grep "the editor exited uncleanly, aborting everything" err
+	test_grep "I am about to error" err &&
+	test_grep "the editor exited uncleanly, aborting everything" err
 '
 
 test_expect_success $PREREQ 'setup fake editor' '
@@ -814,11 +814,11 @@
 '
 
 test_expect_success $PREREQ 'first message is compose text' '
-	grep "^fake edit" msgtxt1
+	test_grep "^fake edit" msgtxt1
 '
 
 test_expect_success $PREREQ 'second message is patch' '
-	grep "Subject:.*Second" msgtxt2
+	test_grep "Subject:.*Second" msgtxt2
 '
 
 test_expect_success $PREREQ 'setup expect' "
@@ -1237,8 +1237,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^utf8 body" msgtxt1 &&
-	grep "^Content-Type: text/plain; charset=UTF-8" msgtxt1
+	test_grep "^utf8 body" msgtxt1 &&
+	test_grep "^Content-Type: text/plain; charset=UTF-8" msgtxt1
 '
 
 test_expect_success $PREREQ '--compose respects user mime type' '
@@ -1260,9 +1260,9 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^utf8 body" msgtxt1 &&
-	grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1 &&
-	! grep "^Content-Type: text/plain; charset=UTF-8" msgtxt1
+	test_grep "^utf8 body" msgtxt1 &&
+	test_grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1 &&
+	test_grep ! "^Content-Type: text/plain; charset=UTF-8" msgtxt1
 '
 
 test_expect_success $PREREQ '--compose adds MIME for utf8 subject' '
@@ -1274,8 +1274,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^fake edit" msgtxt1 &&
-	grep "^Subject: =?UTF-8?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1
+	test_grep "^fake edit" msgtxt1 &&
+	test_grep "^Subject: =?UTF-8?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1
 '
 
 test_expect_success $PREREQ 'utf8 author is correctly passed on' '
@@ -1288,7 +1288,7 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		funny_name.patch &&
-	grep "^From: Füñný Nâmé <odd_?=mail@example.com>" msgtxt1
+	test_grep "^From: Füñný Nâmé <odd_?=mail@example.com>" msgtxt1
 '
 
 test_expect_success $PREREQ 'utf8 sender is not duplicated' '
@@ -1361,8 +1361,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^utf8 body" msgtxt1 &&
-	grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1
+	test_grep "^utf8 body" msgtxt1 &&
+	test_grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1
 '
 
 test_expect_success $PREREQ '--compose-encoding works' '
@@ -1378,8 +1378,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^utf8 body" msgtxt1 &&
-	grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1
+	test_grep "^utf8 body" msgtxt1 &&
+	test_grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1
 '
 
 test_expect_success $PREREQ '--compose-encoding overrides sendemail.composeencoding' '
@@ -1396,8 +1396,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^utf8 body" msgtxt1 &&
-	grep "^Content-Type: text/plain; charset=iso-8859-2" msgtxt1
+	test_grep "^utf8 body" msgtxt1 &&
+	test_grep "^Content-Type: text/plain; charset=iso-8859-2" msgtxt1
 '
 
 test_expect_success $PREREQ '--compose-encoding adds correct MIME for subject' '
@@ -1410,8 +1410,8 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		$patches &&
-	grep "^fake edit" msgtxt1 &&
-	grep "^Subject: =?iso-8859-2?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1
+	test_grep "^fake edit" msgtxt1 &&
+	test_grep "^Subject: =?iso-8859-2?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1
 '
 
 test_expect_success $PREREQ 'detects ambiguous reference/file conflict' '
@@ -1419,7 +1419,7 @@
 	git add main &&
 	git commit -m"add main" &&
 	test_must_fail git send-email --dry-run main 2>errors &&
-	grep disambiguate errors
+	test_grep disambiguate errors
 '
 
 test_expect_success $PREREQ 'feed two files' '
@@ -1443,7 +1443,7 @@
 		--in-reply-to="<in-reply-id@example.com>" \
 		--no-thread \
 		$patches >out &&
-	grep "In-Reply-To: <in-reply-id@example.com>" out
+	test_grep "In-Reply-To: <in-reply-id@example.com>" out
 '
 
 test_expect_success $PREREQ 'no in-reply-to and no threading' '
@@ -1453,7 +1453,7 @@
 		--to=nobody@example.com \
 		--no-thread \
 		$patches >stdout &&
-	! grep "In-Reply-To: " stdout
+	test_grep ! "In-Reply-To: " stdout
 '
 
 test_expect_success $PREREQ 'threading but no chain-reply-to' '
@@ -1464,7 +1464,7 @@
 		--thread \
 		--no-chain-reply-to \
 		$patches $patches >stdout &&
-	grep "In-Reply-To: " stdout
+	test_grep "In-Reply-To: " stdout
 '
 
 test_expect_success $PREREQ 'override in-reply-to if no threading' '
@@ -1475,7 +1475,7 @@
 		--no-thread \
 		--in-reply-to="override" \
 		$threaded_patches >stdout &&
-	grep "In-Reply-To: <override>" stdout
+	test_grep "In-Reply-To: <override>" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.to works' '
@@ -1484,7 +1484,7 @@
 		--dry-run \
 		--from="Example <nobody@example.com>" \
 		$patches >stdout &&
-	grep "To: Somebody <somebody@ex.com>" stdout
+	test_grep "To: Somebody <somebody@ex.com>" stdout
 '
 
 test_expect_success $PREREQ 'setup sendemail.identity' '
@@ -1498,7 +1498,7 @@
 		--dry-run \
 		--from="nobody@example.com" \
 		$patches >stdout &&
-	grep "To: cloud@example.com" stdout
+	test_grep "To: cloud@example.com" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.identity: identity overrides sendemail.identity' '
@@ -1507,7 +1507,7 @@
 		--dry-run \
 		--from="nobody@example.com" \
 		$patches >stdout &&
-	grep "To: isp@example.com" stdout
+	test_grep "To: isp@example.com" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.identity: --no-identity clears previous identity' '
@@ -1516,7 +1516,7 @@
 		--dry-run \
 		--from="nobody@example.com" \
 		$patches >stdout &&
-	grep "To: default@example.com" stdout
+	test_grep "To: default@example.com" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.identity: bool identity variable existence overrides' '
@@ -1527,8 +1527,8 @@
 		--dry-run \
 		--from="nobody@example.com" \
 		$patches >stdout &&
-	grep "To: cloud@example.com" stdout &&
-	! grep "X-Mailer" stdout
+	test_grep "To: cloud@example.com" stdout &&
+	test_grep ! "X-Mailer" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.identity: bool variable fallback' '
@@ -1538,8 +1538,8 @@
 		--dry-run \
 		--from="nobody@example.com" \
 		$patches >stdout &&
-	grep "To: cloud@example.com" stdout &&
-	! grep "X-Mailer" stdout
+	test_grep "To: cloud@example.com" stdout &&
+	test_grep ! "X-Mailer" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.identity: bool variable without a value' '
@@ -1548,8 +1548,8 @@
 		--dry-run \
 		--from="nobody@example.com" \
 		$patches >stdout &&
-	grep "To: default@example.com" stdout &&
-	grep "X-Mailer" stdout
+	test_grep "To: default@example.com" stdout &&
+	test_grep "X-Mailer" stdout
 '
 
 test_expect_success $PREREQ '--no-to overrides sendemail.to' '
@@ -1559,8 +1559,8 @@
 		--no-to \
 		--to=nobody@example.com \
 		$patches >stdout &&
-	grep "To: nobody@example.com" stdout &&
-	! grep "To: Somebody <somebody@ex.com>" stdout
+	test_grep "To: nobody@example.com" stdout &&
+	test_grep ! "To: Somebody <somebody@ex.com>" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.cc works' '
@@ -1570,7 +1570,7 @@
 		--from="Example <nobody@example.com>" \
 		--to=nobody@example.com \
 		$patches >stdout &&
-	grep "Cc: Somebody <somebody@ex.com>" stdout
+	test_grep "Cc: Somebody <somebody@ex.com>" stdout
 '
 
 test_expect_success $PREREQ '--no-cc overrides sendemail.cc' '
@@ -1581,8 +1581,8 @@
 		--cc=bodies@example.com \
 		--to=nobody@example.com \
 		$patches >stdout &&
-	grep "Cc: bodies@example.com" stdout &&
-	! grep "Cc: Somebody <somebody@ex.com>" stdout
+	test_grep "Cc: bodies@example.com" stdout &&
+	test_grep ! "Cc: Somebody <somebody@ex.com>" stdout
 '
 
 test_expect_success $PREREQ 'sendemail.bcc works' '
@@ -1593,7 +1593,7 @@
 		--to=nobody@example.com \
 		--smtp-server relay.example.com \
 		$patches >stdout &&
-	grep "RCPT TO:<other@ex.com>" stdout
+	test_grep "RCPT TO:<other@ex.com>" stdout
 '
 
 test_expect_success $PREREQ '--no-bcc overrides sendemail.bcc' '
@@ -1605,8 +1605,8 @@
 		--to=nobody@example.com \
 		--smtp-server relay.example.com \
 		$patches >stdout &&
-	grep "RCPT TO:<bodies@example.com>" stdout &&
-	! grep "RCPT TO:<other@ex.com>" stdout
+	test_grep "RCPT TO:<bodies@example.com>" stdout &&
+	test_grep ! "RCPT TO:<other@ex.com>" stdout
 '
 
 test_expect_success $PREREQ 'patches To headers are used by default' '
@@ -1617,7 +1617,7 @@
 		--from="Example <nobody@example.com>" \
 		--smtp-server relay.example.com \
 		$patch >stdout &&
-	grep "RCPT TO:<bodies@example.com>" stdout
+	test_grep "RCPT TO:<bodies@example.com>" stdout
 '
 
 test_expect_success $PREREQ 'patches To headers are appended to' '
@@ -1629,8 +1629,8 @@
 		--to=nobody@example.com \
 		--smtp-server relay.example.com \
 		$patch >stdout &&
-	grep "RCPT TO:<bodies@example.com>" stdout &&
-	grep "RCPT TO:<nobody@example.com>" stdout
+	test_grep "RCPT TO:<bodies@example.com>" stdout &&
+	test_grep "RCPT TO:<nobody@example.com>" stdout
 '
 
 test_expect_success $PREREQ 'To headers from files reset each patch' '
@@ -1691,9 +1691,9 @@
 	git send-email --from=author@example.com --to=nobody@example.com \
 			--smtp-server="$(pwd)/fake.sendmail" \
 			email-using-8bit >stdout &&
-	grep "do not declare a Content-Transfer-Encoding" stdout &&
-	grep email-using-8bit stdout &&
-	grep "Declare which 8bit encoding to use" stdout &&
+	test_grep "do not declare a Content-Transfer-Encoding" stdout &&
+	test_grep email-using-8bit stdout &&
+	test_grep "Declare which 8bit encoding to use" stdout &&
 	grep -E "Content|MIME" msgtxt1 >actual &&
 	test_cmp content-type-decl actual
 '
@@ -1790,7 +1790,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		email-using-8bit \
 		2>errors >out &&
-	grep "cannot send message as 7bit" errors &&
+	test_grep "cannot send message as 7bit" errors &&
 	test -z "$(ls msgtxt*)"
 '
 
@@ -1801,7 +1801,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		email-using-8bit \
 		2>errors >out &&
-	grep "cannot send message as 7bit" errors &&
+	test_grep "cannot send message as 7bit" errors &&
 	test -z "$(ls msgtxt*)"
 '
 
@@ -1812,7 +1812,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		email-using-8bit \
 		2>errors >out &&
-	grep "cannot send message as 7bit" errors &&
+	test_grep "cannot send message as 7bit" errors &&
 	test -z "$(ls msgtxt*)"
 '
 
@@ -1939,7 +1939,7 @@
 		outdir/0000-*.patch \
 		outdir/0001-*.patch \
 		2>errors >out &&
-	grep "SUBJECT HERE" errors &&
+	test_grep "SUBJECT HERE" errors &&
 	test -z "$(ls msgtxt*)"
 '
 
@@ -1956,7 +1956,7 @@
 		outdir/0000-*.patch \
 		outdir/0001-*.patch \
 		2>errors >out &&
-	! grep "SUBJECT HERE" errors &&
+	test_grep ! "SUBJECT HERE" errors &&
 	test -n "$(ls msgtxt*)"
 '
 
@@ -2016,8 +2016,8 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		outdir/0001-*.patch \
 		2>errors >out &&
-	grep "^!somebody@example\.org!$" commandline1 &&
-	grep -F "To: \"Dot U. Sir\" <somebody@example.org>" out
+	test_grep "^!somebody@example\.org!$" commandline1 &&
+	test_grep -F "To: \"Dot U. Sir\" <somebody@example.org>" out
 '
 
 test_expect_success $PREREQ 'sendemail.aliasfiletype=mailrc' '
@@ -2031,7 +2031,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		outdir/0001-*.patch \
 		2>errors >out &&
-	grep "^!somebody@example\.org!$" commandline1
+	test_grep "^!somebody@example\.org!$" commandline1
 '
 
 test_expect_success $PREREQ 'sendemail.aliasesfile=~/.mailrc' '
@@ -2045,7 +2045,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		outdir/0001-*.patch \
 		2>errors >out &&
-	grep "^!someone@example\.org!$" commandline1
+	test_grep "^!someone@example\.org!$" commandline1
 '
 
 test_dump_aliases () {
@@ -2264,7 +2264,7 @@
 			2>errors >out &&
 		for i in $expect
 		do
-			grep "^!$i!$" commandline1 || return 1
+			test_grep "^!$i!$" commandline1 || return 1
 		done
 	'
 }
@@ -2328,7 +2328,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		aliased.patch \
 		2>errors >out &&
-	grep "^!someone@example\.org!$" commandline1
+	test_grep "^!someone@example\.org!$" commandline1
 '
 
 test_expect_success $PREREQ 'alias support in Cc header' '
@@ -2342,7 +2342,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		aliased.patch \
 		2>errors >out &&
-	grep "^!someone@example\.org!$" commandline1
+	test_grep "^!someone@example\.org!$" commandline1
 '
 
 test_expect_success $PREREQ 'tocmd works with aliases' '
@@ -2358,7 +2358,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		tocmd.patch \
 		2>errors >out &&
-	grep "^!someone@example\.org!$" commandline1
+	test_grep "^!someone@example\.org!$" commandline1
 '
 
 test_expect_success $PREREQ 'cccmd works with aliases' '
@@ -2374,7 +2374,7 @@
 		--smtp-server="$(pwd)/fake.sendmail" \
 		cccmd.patch \
 		2>errors >out &&
-	grep "^!someone@example\.org!$" commandline1
+	test_grep "^!someone@example\.org!$" commandline1
 '
 
 do_xmailer_test () {
@@ -2511,7 +2511,7 @@
 		--mailmap \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.com!$" commandline1
+	test_grep "^!someone@example\.com!$" commandline1
 '
 
 test_expect_success $PREREQ 'sendemail.mailmap configuration' '
@@ -2528,7 +2528,7 @@
 		--to=someone@example.org \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.com!$" commandline1
+	test_grep "^!someone@example\.com!$" commandline1
 '
 
 test_expect_success $PREREQ 'sendemail.mailmap.file configuration' '
@@ -2545,7 +2545,7 @@
 		--to=someone@example.org \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.com!$" commandline1
+	test_grep "^!someone@example\.com!$" commandline1
 '
 
 test_expect_success $PREREQ 'sendemail.mailmap identity overrides configuration' '
@@ -2564,7 +2564,7 @@
 		--to=someone@example.org \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.com!$" commandline1
+	test_grep "^!someone@example\.com!$" commandline1
 '
 
 test_expect_success $PREREQ '--no-mailmap overrides configuration' '
@@ -2584,7 +2584,7 @@
 		--no-mailmap \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.org!$" commandline1
+	test_grep "^!someone@example\.org!$" commandline1
 '
 
 test_expect_success $PREREQ 'mailmap support in To header' '
@@ -2600,7 +2600,7 @@
 		--mailmap \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.com!$" commandline1
+	test_grep "^!someone@example\.com!$" commandline1
 '
 
 test_expect_success $PREREQ 'mailmap support in Cc header' '
@@ -2616,7 +2616,7 @@
 		--mailmap \
 		a.patch \
 		2>errors >out &&
-	grep "^!someone@example\.com!$" commandline1
+	test_grep "^!someone@example\.com!$" commandline1
 '
 
 test_expect_success $PREREQ 'test using command name with --sendmail-cmd' '
@@ -2676,9 +2676,9 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		has-reply.patch no-reply.patch &&
-	grep "In-Reply-To: <replied.to@example.com>" msgtxt1 &&
-	grep "References: <replied.to@example.com>" msgtxt1 &&
-	! grep replied.to@example.com msgtxt2
+	test_grep "In-Reply-To: <replied.to@example.com>" msgtxt1 &&
+	test_grep "References: <replied.to@example.com>" msgtxt1 &&
+	test_grep ! replied.to@example.com msgtxt2
 '
 
 test_expect_success $PREREQ 'cmdline in-reply-to used with --no-thread' '
@@ -2689,10 +2689,10 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		has-reply.patch no-reply.patch &&
-	grep "In-Reply-To: <cmdline.reply@example.com>" msgtxt1 &&
-	grep "References: <cmdline.reply@example.com>" msgtxt1 &&
-	grep "In-Reply-To: <cmdline.reply@example.com>" msgtxt2 &&
-	grep "References: <cmdline.reply@example.com>" msgtxt2
+	test_grep "In-Reply-To: <cmdline.reply@example.com>" msgtxt1 &&
+	test_grep "References: <cmdline.reply@example.com>" msgtxt1 &&
+	test_grep "In-Reply-To: <cmdline.reply@example.com>" msgtxt2 &&
+	test_grep "References: <cmdline.reply@example.com>" msgtxt2
 '
 
 test_expect_success $PREREQ 'invoke hook' '
@@ -2775,7 +2775,7 @@
 test_expect_success $PREREQ 'send-email relays -v 3 to format-patch' '
 	test_when_finished "rm -f out" &&
 	git send-email --dry-run -v 3 -1 >out &&
-	grep "PATCH v3" out
+	test_grep "PATCH v3" out
 '
 
 test_expect_success $PREREQ 'test that sendmail config is rejected' '
@@ -2819,7 +2819,7 @@
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		HEAD^ &&
-	grep "From: edited-from@example.com" msgtxt1
+	test_grep "From: edited-from@example.com" msgtxt1
 '
 
 test_expect_success $PREREQ '--compose handles to headers' '
diff --git a/t/t9003-help-autocorrect.sh b/t/t9003-help-autocorrect.sh
index 8da318d..87b438f 100755
--- a/t/t9003-help-autocorrect.sh
+++ b/t/t9003-help-autocorrect.sh
@@ -34,10 +34,10 @@
 		git config help.autocorrect $show &&
 
 		test_must_fail git lfg 2>actual &&
-		grep "^	lgf" actual &&
+		test_grep "^	lgf" actual &&
 
 		test_must_fail git distimdist 2>actual &&
-		grep "^	distimdistim" actual
+		test_grep "^	distimdistim" actual
 	'
 done
 
@@ -60,7 +60,7 @@
 	git config help.autocorrect never &&
 
 	test_must_fail git lfg 2>actual &&
-	grep "is not a git command" actual &&
+	test_grep "is not a git command" actual &&
 	test_line_count = 1 actual
 '
 
diff --git a/t/t9106-git-svn-commit-diff-clobber.sh b/t/t9106-git-svn-commit-diff-clobber.sh
index bca496c..376100f 100755
--- a/t/t9106-git-svn-commit-diff-clobber.sh
+++ b/t/t9106-git-svn-commit-diff-clobber.sh
@@ -94,7 +94,7 @@
 
 test_expect_success 'check that rebase really failed' '
 	git status >output &&
-	grep currently.rebasing output
+	test_grep currently.rebasing output
 '
 
 test_expect_success 'resolve, continue the rebase and dcommit' "
diff --git a/t/t9107-git-svn-migrate.sh b/t/t9107-git-svn-migrate.sh
index aa908bb..6d7d2aa 100755
--- a/t/t9107-git-svn-migrate.sh
+++ b/t/t9107-git-svn-migrate.sh
@@ -46,7 +46,7 @@
 test_expect_success 'initialize a multi-repository repo' '
 	git svn init "$svnrepo" -T trunk -t tags -b branches &&
 	git config --get-all svn-remote.svn.fetch > fetch.out &&
-	grep "^trunk:refs/remotes/origin/trunk$" fetch.out &&
+	test_grep "^trunk:refs/remotes/origin/trunk$" fetch.out &&
 	test -n "$(git config --get svn-remote.svn.branches \
 		    "^branches/\*:refs/remotes/origin/\*$")" &&
 	test -n "$(git config --get svn-remote.svn.tags \
@@ -63,13 +63,13 @@
 			$i:refs/remotes/origin/$i || return 1
 	done &&
 	git config --get-all svn-remote.svn.fetch > fetch.out &&
-	grep "^trunk:refs/remotes/origin/trunk$" fetch.out &&
-	grep "^branches/a:refs/remotes/origin/a$" fetch.out &&
-	grep "^branches/b:refs/remotes/origin/b$" fetch.out &&
-	grep "^tags/0\.1:refs/remotes/origin/tags/0\.1$" fetch.out &&
-	grep "^tags/0\.2:refs/remotes/origin/tags/0\.2$" fetch.out &&
-	grep "^tags/0\.3:refs/remotes/origin/tags/0\.3$" fetch.out &&
-	grep "^:refs/remotes/git-svn" fetch.out
+	test_grep "^trunk:refs/remotes/origin/trunk$" fetch.out &&
+	test_grep "^branches/a:refs/remotes/origin/a$" fetch.out &&
+	test_grep "^branches/b:refs/remotes/origin/b$" fetch.out &&
+	test_grep "^tags/0\.1:refs/remotes/origin/tags/0\.1$" fetch.out &&
+	test_grep "^tags/0\.2:refs/remotes/origin/tags/0\.2$" fetch.out &&
+	test_grep "^tags/0\.3:refs/remotes/origin/tags/0\.3$" fetch.out &&
+	test_grep "^:refs/remotes/git-svn" fetch.out
 	'
 
 # refs should all be different, but the trees should all be the same:
@@ -109,13 +109,13 @@
 	git svn migrate --minimize &&
 	test -z "$(git config -l | grep "^svn-remote\.git-svn\.")" &&
 	git config --get-all svn-remote.svn.fetch > fetch.out &&
-	grep "^trunk:refs/remotes/origin/trunk$" fetch.out &&
-	grep "^branches/a:refs/remotes/origin/a$" fetch.out &&
-	grep "^branches/b:refs/remotes/origin/b$" fetch.out &&
-	grep "^tags/0\.1:refs/remotes/origin/tags/0\.1$" fetch.out &&
-	grep "^tags/0\.2:refs/remotes/origin/tags/0\.2$" fetch.out &&
-	grep "^tags/0\.3:refs/remotes/origin/tags/0\.3$" fetch.out &&
-	grep "^:refs/remotes/git-svn" fetch.out
+	test_grep "^trunk:refs/remotes/origin/trunk$" fetch.out &&
+	test_grep "^branches/a:refs/remotes/origin/a$" fetch.out &&
+	test_grep "^branches/b:refs/remotes/origin/b$" fetch.out &&
+	test_grep "^tags/0\.1:refs/remotes/origin/tags/0\.1$" fetch.out &&
+	test_grep "^tags/0\.2:refs/remotes/origin/tags/0\.2$" fetch.out &&
+	test_grep "^tags/0\.3:refs/remotes/origin/tags/0\.3$" fetch.out &&
+	test_grep "^:refs/remotes/git-svn" fetch.out
 	'
 
 test_expect_success  ".rev_db auto-converted to .rev_map.UUID" '
diff --git a/t/t9110-git-svn-use-svm-props.sh b/t/t9110-git-svn-use-svm-props.sh
index ad37d98..9dc4a31 100755
--- a/t/t9110-git-svn-use-svm-props.sh
+++ b/t/t9110-git-svn-use-svm-props.sh
@@ -22,36 +22,36 @@
 bar_url=http://mayonaise/svnrepo/bar
 test_expect_success 'verify metadata for /bar' "
 	git cat-file commit refs/remotes/bar >actual &&
-	grep '^git-svn-id: $bar_url@12 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@12 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~1 >actual &&
-	grep '^git-svn-id: $bar_url@11 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@11 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~2 >actual &&
-	grep '^git-svn-id: $bar_url@10 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@10 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~3 >actual &&
-	grep '^git-svn-id: $bar_url@9 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@9 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~4 >actual &&
-	grep '^git-svn-id: $bar_url@6 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@6 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~5 >actual &&
-	grep '^git-svn-id: $bar_url@1 $uuid$' actual
+	test_grep '^git-svn-id: $bar_url@1 $uuid$' actual
 	"
 
 e_url=http://mayonaise/svnrepo/dir/a/b/c/d/e
 test_expect_success 'verify metadata for /dir/a/b/c/d/e' "
 	git cat-file commit refs/remotes/e >actual &&
-	grep '^git-svn-id: $e_url@1 $uuid$' actual
+	test_grep '^git-svn-id: $e_url@1 $uuid$' actual
 	"
 
 dir_url=http://mayonaise/svnrepo/dir
 test_expect_success 'verify metadata for /dir' "
 	git cat-file commit refs/remotes/dir >actual &&
-	grep '^git-svn-id: $dir_url@2 $uuid$' actual &&
+	test_grep '^git-svn-id: $dir_url@2 $uuid$' actual &&
 	git cat-file commit refs/remotes/dir~1 >actual &&
-	grep '^git-svn-id: $dir_url@1 $uuid$' actual
+	test_grep '^git-svn-id: $dir_url@1 $uuid$' actual
 	"
 
 test_expect_success 'find commit based on SVN revision number' "
 	git svn find-rev r12 >actual &&
-	grep $(git rev-parse HEAD) actual
+	test_grep $(git rev-parse HEAD) actual
         "
 
 test_expect_success 'empty rebase' "
diff --git a/t/t9111-git-svn-use-svnsync-props.sh b/t/t9111-git-svn-use-svnsync-props.sh
index 6c93073..e45d81b 100755
--- a/t/t9111-git-svn-use-svnsync-props.sh
+++ b/t/t9111-git-svn-use-svnsync-props.sh
@@ -21,31 +21,31 @@
 bar_url=http://mayonaise/svnrepo/bar
 test_expect_success 'verify metadata for /bar' "
 	git cat-file commit refs/remotes/bar >actual &&
-	grep '^git-svn-id: $bar_url@12 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@12 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~1 >actual &&
-	grep '^git-svn-id: $bar_url@11 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@11 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~2 >actual &&
-	grep '^git-svn-id: $bar_url@10 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@10 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~3 >actual &&
-	grep '^git-svn-id: $bar_url@9 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@9 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~4 >actual &&
-	grep '^git-svn-id: $bar_url@6 $uuid$' actual &&
+	test_grep '^git-svn-id: $bar_url@6 $uuid$' actual &&
 	git cat-file commit refs/remotes/bar~5 >actual &&
-	grep '^git-svn-id: $bar_url@1 $uuid$' actual
+	test_grep '^git-svn-id: $bar_url@1 $uuid$' actual
 	"
 
 e_url=http://mayonaise/svnrepo/dir/a/b/c/d/e
 test_expect_success 'verify metadata for /dir/a/b/c/d/e' "
 	git cat-file commit refs/remotes/e >actual &&
-	grep '^git-svn-id: $e_url@1 $uuid$' actual
+	test_grep '^git-svn-id: $e_url@1 $uuid$' actual
 	"
 
 dir_url=http://mayonaise/svnrepo/dir
 test_expect_success 'verify metadata for /dir' "
 	git cat-file commit refs/remotes/dir >actual &&
-	grep '^git-svn-id: $dir_url@2 $uuid$' actual &&
+	test_grep '^git-svn-id: $dir_url@2 $uuid$' actual &&
 	git cat-file commit refs/remotes/dir~1 >actual &&
-	grep '^git-svn-id: $dir_url@1 $uuid$' actual
+	test_grep '^git-svn-id: $dir_url@1 $uuid$' actual
 	"
 
 test_done
diff --git a/t/t9114-git-svn-dcommit-merge.sh b/t/t9114-git-svn-dcommit-merge.sh
index e06538b..2240b58 100755
--- a/t/t9114-git-svn-dcommit-merge.sh
+++ b/t/t9114-git-svn-dcommit-merge.sh
@@ -69,7 +69,7 @@
 	test x\$(git rev-parse --verify refs/heads/svn^2) = \
 	     x\$(git rev-parse --verify refs/heads/merge) &&
 	git cat-file commit refs/heads/svn^ >actual &&
-	grep '^friend$' actual
+	test_grep '^friend$' actual
 	"
 
 test_expect_success 'git svn dcommit merges' "
@@ -84,12 +84,12 @@
 	test x\$(git rev-parse --verify refs/heads/svn^2) = \
 	     x\$(git rev-parse --verify refs/heads/merge) &&
 	git cat-file commit refs/heads/svn^ >actual &&
-	grep '^friend$' actual
+	test_grep '^friend$' actual
 	"
 
 test_expect_success 'verify merge commit message' "
 	git rev-list --pretty=raw -1 refs/heads/svn >actual &&
-	grep \"    Merge branch 'merge' into svn\" actual
+	test_grep \"    Merge branch 'merge' into svn\" actual
 	"
 
 test_done
diff --git a/t/t9116-git-svn-log.sh b/t/t9116-git-svn-log.sh
index d74d7b2..b2e1248 100755
--- a/t/t9116-git-svn-log.sh
+++ b/t/t9116-git-svn-log.sh
@@ -45,17 +45,17 @@
 test_expect_success 'run log' "
 	git reset --hard origin/a &&
 	git svn log -r2 origin/trunk >out &&
-	grep ^r2 out &&
+	test_grep ^r2 out &&
 	git svn log -r4 origin/trunk >out &&
-	grep ^r4 out &&
+	test_grep ^r4 out &&
 	git svn log -r3 >out &&
-	grep ^r3 out
+	test_grep ^r3 out
 	"
 
 test_expect_success 'run log against a from trunk' "
 	git reset --hard origin/trunk &&
 	git svn log -r3 origin/a >out &&
-	grep ^r3 out
+	test_grep ^r3 out
 	"
 
 printf 'r1 \nr2 \nr4 \n' > expected-range-r1-r2-r4
diff --git a/t/t9117-git-svn-init-clone.sh b/t/t9117-git-svn-init-clone.sh
index 3b038c3..b2815b6 100755
--- a/t/t9117-git-svn-init-clone.sh
+++ b/t/t9117-git-svn-init-clone.sh
@@ -49,7 +49,7 @@
 test_expect_success 'init without -s/-T/-b/-t does not warn' '
 	test ! -d trunk &&
 	git svn init "$svnrepo"/project/trunk trunk 2>warning &&
-	! grep -q prefix warning &&
+	test_grep ! -q prefix warning &&
 	rm -rf trunk &&
 	rm -f warning
 	'
@@ -57,7 +57,7 @@
 test_expect_success 'clone without -s/-T/-b/-t does not warn' '
 	test ! -d trunk &&
 	git svn clone "$svnrepo"/project/trunk 2>warning &&
-	! grep -q prefix warning &&
+	test_grep ! -q prefix warning &&
 	rm -rf trunk &&
 	rm -f warning
 	'
@@ -80,7 +80,7 @@
 test_expect_success 'init with -s/-T/-b/-t assumes --prefix=origin/' '
 	test ! -d project &&
 	git svn init -s "$svnrepo"/project project 2>warning &&
-	! grep -q prefix warning &&
+	test_grep ! -q prefix warning &&
 	test_svn_configured_prefix "origin/" &&
 	rm -rf project &&
 	rm -f warning
@@ -89,7 +89,7 @@
 test_expect_success 'clone with -s/-T/-b/-t assumes --prefix=origin/' '
 	test ! -d project &&
 	git svn clone -s "$svnrepo"/project 2>warning &&
-	! grep -q prefix warning &&
+	test_grep ! -q prefix warning &&
 	test_svn_configured_prefix "origin/" &&
 	rm -rf project &&
 	rm -f warning
@@ -98,7 +98,7 @@
 test_expect_success 'init with -s/-T/-b/-t and --prefix "" still works' '
 	test ! -d project &&
 	git svn init -s "$svnrepo"/project project --prefix "" 2>warning &&
-	! grep -q prefix warning &&
+	test_grep ! -q prefix warning &&
 	test_svn_configured_prefix "" &&
 	rm -rf project &&
 	rm -f warning
@@ -107,7 +107,7 @@
 test_expect_success 'clone with -s/-T/-b/-t and --prefix "" still works' '
 	test ! -d project &&
 	git svn clone -s "$svnrepo"/project --prefix "" 2>warning &&
-	! grep -q prefix warning &&
+	test_grep ! -q prefix warning &&
 	test_svn_configured_prefix "" &&
 	rm -rf project &&
 	rm -f warning
diff --git a/t/t9119-git-svn-info.sh b/t/t9119-git-svn-info.sh
index 088d1c5..0dbb2e4 100755
--- a/t/t9119-git-svn-info.sh
+++ b/t/t9119-git-svn-info.sh
@@ -333,27 +333,27 @@
 	echo two > gitwc/unknown-file &&
 	(cd gitwc && test_must_fail git svn info unknown-file) \
 		 2> actual.info-unknown-file &&
-	grep unknown-file actual.info-unknown-file
+	test_grep unknown-file actual.info-unknown-file
 	"
 
 test_expect_success 'info --url unknown-file' '
 	echo two > gitwc/unknown-file &&
 	(cd gitwc && test_must_fail git svn info --url unknown-file) \
 		 2> actual.info-url-unknown-file &&
-	grep unknown-file actual.info-url-unknown-file
+	test_grep unknown-file actual.info-url-unknown-file
 	'
 
 test_expect_success 'info unknown-directory' "
 	mkdir gitwc/unknown-directory svnwc/unknown-directory &&
 	(cd gitwc && test_must_fail git svn info unknown-directory) \
 		 2> actual.info-unknown-directory &&
-	grep unknown-directory actual.info-unknown-directory
+	test_grep unknown-directory actual.info-unknown-directory
 	"
 
 test_expect_success 'info --url unknown-directory' '
 	(cd gitwc && test_must_fail git svn info --url unknown-directory) \
 		 2> actual.info-url-unknown-directory &&
-	grep unknown-directory actual.info-url-unknown-directory
+	test_grep unknown-directory actual.info-url-unknown-directory
 	'
 
 test_expect_success 'info unknown-symlink-file' "
@@ -363,13 +363,13 @@
 	) &&
 	(cd gitwc && test_must_fail git svn info unknown-symlink-file) \
 		 2> actual.info-unknown-symlink-file &&
-	grep unknown-symlink-file actual.info-unknown-symlink-file
+	test_grep unknown-symlink-file actual.info-unknown-symlink-file
 	"
 
 test_expect_success 'info --url unknown-symlink-file' '
 	(cd gitwc && test_must_fail git svn info --url unknown-symlink-file) \
 		 2> actual.info-url-unknown-symlink-file &&
-	grep unknown-symlink-file actual.info-url-unknown-symlink-file
+	test_grep unknown-symlink-file actual.info-url-unknown-symlink-file
 	'
 
 test_expect_success 'info unknown-symlink-directory' "
@@ -379,13 +379,13 @@
 	) &&
 	(cd gitwc && test_must_fail git svn info unknown-symlink-directory) \
 		 2> actual.info-unknown-symlink-directory &&
-	grep unknown-symlink-directory actual.info-unknown-symlink-directory
+	test_grep unknown-symlink-directory actual.info-unknown-symlink-directory
 	"
 
 test_expect_success 'info --url unknown-symlink-directory' '
 	(cd gitwc && test_must_fail git svn info --url unknown-symlink-directory) \
 		 2> actual.info-url-unknown-symlink-directory &&
-	grep unknown-symlink-directory actual.info-url-unknown-symlink-directory
+	test_grep unknown-symlink-directory actual.info-url-unknown-symlink-directory
 	'
 
 test_done
diff --git a/t/t9122-git-svn-author.sh b/t/t9122-git-svn-author.sh
index 0fc289a..21401ad 100755
--- a/t/t9122-git-svn-author.sh
+++ b/t/t9122-git-svn-author.sh
@@ -57,15 +57,15 @@
 	test "z$myself" = "z$unaffected" &&
 
 	# Make sure lack of --add-author-from did not add cruft
-	! grep "^    From: A U Thor " actual.2 &&
+	test_grep ! "^    From: A U Thor " actual.2 &&
 
 	# Make sure --add-author-from added cruft
-	grep "^    From: A U Thor " actual.3 &&
-	grep "^    From: A U Thor " actual.4 &&
+	test_grep "^    From: A U Thor " actual.3 &&
+	test_grep "^    From: A U Thor " actual.4 &&
 
 	# Make sure --add-author-from with --use-log-author affected
 	# the authorship information
-	grep "^Author: A U Thor " actual.4 &&
+	test_grep "^Author: A U Thor " actual.4 &&
 
 	# Make sure there are no commit messages with excess blank lines
 	test $(grep "^ " actual.2 | wc -l) = 3 &&
diff --git a/t/t9130-git-svn-authors-file.sh b/t/t9130-git-svn-authors-file.sh
index 90325db..67a4d88 100755
--- a/t/t9130-git-svn-authors-file.sh
+++ b/t/t9130-git-svn-authors-file.sh
@@ -29,9 +29,9 @@
 		git rev-list refs/remotes/git-svn >actual &&
 		test_line_count = 2 actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn >actual &&
-		grep "^author BBBBBBB BBBBBBB <bb@example\.com> " actual &&
+		test_grep "^author BBBBBBB BBBBBBB <bb@example\.com> " actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~1 >actual &&
-		grep "^author AAAAAAA AAAAAAA <aa@example\.com> " actual
+		test_grep "^author AAAAAAA AAAAAAA <aa@example\.com> " actual
 	)
 	'
 
@@ -47,9 +47,9 @@
 		git rev-list refs/remotes/git-svn >actual &&
 		test_line_count = 4 actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn >actual &&
-		grep "^author DDDDDDD DDDDDDD <dd@example\.com> " actual &&
+		test_grep "^author DDDDDDD DDDDDDD <dd@example\.com> " actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~1 >actual &&
-		grep "^author CCCCCCC CCCCCCC <cc@example\.com> " actual
+		test_grep "^author CCCCCCC CCCCCCC <cc@example\.com> " actual
 	)
 	'
 
diff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh
index 5bb38cb..5fdf324 100755
--- a/t/t9138-git-svn-authors-prog.sh
+++ b/t/t9138-git-svn-authors-prog.sh
@@ -48,15 +48,15 @@
 	(
 		cd x &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~1 >actual &&
-		grep "^author ee-foo <ee-foo@example\.com> " actual &&
+		test_grep "^author ee-foo <ee-foo@example\.com> " actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~2 >actual &&
-		grep "^author dd <dd@sub\.example\.com> " actual &&
+		test_grep "^author dd <dd@sub\.example\.com> " actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~3 >actual &&
-		grep "^author cc <cc@sub\.example\.com> " actual &&
+		test_grep "^author cc <cc@sub\.example\.com> " actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~4 >actual &&
-		grep "^author bb <bb@example\.com> " actual &&
+		test_grep "^author bb <bb@example\.com> " actual &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn~5 >actual &&
-		grep "^author aa <aa@example\.com> " actual
+		test_grep "^author aa <aa@example\.com> " actual
 	)
 '
 
@@ -64,7 +64,7 @@
 	(
 		cd x &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn >actual &&
-		grep "^author FFFFFFF FFFFFFF <fFf@other\.example\.com> " actual
+		test_grep "^author FFFFFFF FFFFFFF <fFf@other\.example\.com> " actual
 	)
 '
 
@@ -99,7 +99,7 @@
 		cd x &&
 		git svn --authors-prog=../svn-authors-prog fetch &&
 		git rev-list -1 --pretty=raw refs/remotes/git-svn >actual &&
-		grep "^author xyz; touch evil <xyz; touch evil@example\.com> " actual &&
+		test_grep "^author xyz; touch evil <xyz; touch evil@example\.com> " actual &&
 		! test -f evil
 	)
 '
diff --git a/t/t9140-git-svn-reset.sh b/t/t9140-git-svn-reset.sh
index a420b2a..c70a3b3 100755
--- a/t/t9140-git-svn-reset.sh
+++ b/t/t9140-git-svn-reset.sh
@@ -43,7 +43,7 @@
 	  git svn find-rev refs/remotes/git-svn > ../expect &&
 	  test_must_fail git svn fetch 2> ../errors &&
 	  git svn find-rev refs/remotes/git-svn > ../expect2 ) &&
-	grep "not found in commit" errors &&
+	test_grep "not found in commit" errors &&
 	test_cmp expect expect2
 '
 
@@ -59,7 +59,7 @@
 	( cd g &&
 	  git svn fetch &&
 	  git svn rebase &&
-	  grep "mod hidden" hid/hid.txt
+	  test_grep "mod hidden" hid/hid.txt
 	)
 '
 
diff --git a/t/t9153-git-svn-rewrite-uuid.sh b/t/t9153-git-svn-rewrite-uuid.sh
index 8cb2b5c..2387c7d 100755
--- a/t/t9153-git-svn-rewrite-uuid.sh
+++ b/t/t9153-git-svn-rewrite-uuid.sh
@@ -17,9 +17,9 @@
 
 test_expect_success 'verify uuid' "
 	git cat-file commit refs/remotes/git-svn~0 >actual &&
-	grep '^git-svn-id: .*@2 $uuid$' actual &&
+	test_grep '^git-svn-id: .*@2 $uuid$' actual &&
 	git cat-file commit refs/remotes/git-svn~1 >actual &&
-	grep '^git-svn-id: .*@1 $uuid$' actual
+	test_grep '^git-svn-id: .*@1 $uuid$' actual
 	"
 
 test_done
diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh
index 581cf3d..8e0272d 100755
--- a/t/t9200-git-cvsexportcommit.sh
+++ b/t/t9200-git-cvsexportcommit.sh
@@ -336,7 +336,7 @@
 	 echo Hello >> " space" &&
 	 git commit -m "Another change" " space" &&
 	 git cvsexportcommit -W -p -u -c HEAD &&
-	 grep Hello " space" &&
+	 test_grep Hello " space" &&
 	 git diff-files)
 
 '
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index f2a6df7..c8463d0 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -21,13 +21,13 @@
 		# Register
 		scalar register ${enlistment_root} &&
 		scalar list >out &&
-		grep "$(pwd)/${enlistment_root}/src\$" out &&
+		test_grep "$(pwd)/${enlistment_root}/src\$" out &&
 
 		# Delete (including enlistment root)
 		scalar delete $enlistment_root &&
 		test_path_is_missing $enlistment_root &&
 		scalar list >out &&
-		! grep "^$(pwd)/${enlistment_root}/src\$" out || return 1
+		test_grep ! "^$(pwd)/${enlistment_root}/src\$" out || return 1
 	done
 '
 
@@ -41,13 +41,13 @@
 		# Register
 		scalar register ${enlistment_root}/src &&
 		scalar list >out &&
-		grep "$(pwd)/${enlistment_root}/src\$" out &&
+		test_grep "$(pwd)/${enlistment_root}/src\$" out &&
 
 		# Delete (will not include enlistment root)
 		scalar delete ${enlistment_root}/src &&
 		test_path_is_dir $enlistment_root &&
 		scalar list >out &&
-		! grep "^$(pwd)/${enlistment_root}/src\$" out || return 1
+		test_grep ! "^$(pwd)/${enlistment_root}/src\$" out || return 1
 	done
 '
 
@@ -61,13 +61,13 @@
 		# Register
 		scalar register ${enlistment_root} &&
 		scalar list >out &&
-		grep "$(pwd)/${enlistment_root}\$" out &&
+		test_grep "$(pwd)/${enlistment_root}\$" out &&
 
 		# Delete (will not include enlistment root)
 		scalar delete ${enlistment_root} &&
 		test_path_is_missing $enlistment_root &&
 		scalar list >out &&
-		! grep "^$(pwd)/${enlistment_root}\$" out &&
+		test_grep ! "^$(pwd)/${enlistment_root}\$" out &&
 
 		# Make sure we did not accidentally delete the trash dir
 		test_path_is_dir "$TRASH_DIRECTORY" || return 1
@@ -81,7 +81,7 @@
 	mkdir -p test/src/deep &&
 	GIT_CEILING_DIRECTORIES="$(pwd)/test/src" &&
 	! scalar register test/src/deep 2>err &&
-	grep "not a git repository" err
+	test_grep "not a git repository" err
 '
 
 test_expect_success 'scalar enlistments need a worktree' '
@@ -89,11 +89,11 @@
 
 	git init --bare bare/src &&
 	! scalar register bare/src 2>err &&
-	grep "Scalar enlistments require a worktree" err &&
+	test_grep "Scalar enlistments require a worktree" err &&
 
 	git init test/src &&
 	! scalar register test/src/.git 2>err &&
-	grep "Scalar enlistments require a worktree" err
+	test_grep "Scalar enlistments require a worktree" err
 '
 
 test_expect_success FSMONITOR_DAEMON 'scalar register starts fsmon daemon' '
@@ -108,7 +108,7 @@
 	git init register-repo &&
 	GIT_TEST_MAINT_SCHEDULER="crontab:false,launchctl:false,schtasks:false" \
 		scalar register register-repo 2>err &&
-	grep "could not toggle maintenance" err
+	test_grep "could not toggle maintenance" err
 '
 
 test_expect_success 'scalar unregister' '
@@ -117,13 +117,13 @@
 	git config --get --global --fixed-value \
 		maintenance.repo "$(pwd)/vanish/src" &&
 	scalar list >scalar.repos &&
-	grep -F "$(pwd)/vanish/src" scalar.repos &&
+	test_grep -F "$(pwd)/vanish/src" scalar.repos &&
 	rm -rf vanish/src/.git &&
 	scalar unregister vanish &&
 	test_must_fail git config --get --global --fixed-value \
 		maintenance.repo "$(pwd)/vanish/src" &&
 	scalar list >scalar.repos &&
-	! grep -F "$(pwd)/vanish/src" scalar.repos &&
+	test_grep ! -F "$(pwd)/vanish/src" scalar.repos &&
 
 	# scalar unregister should be idempotent
 	scalar unregister vanish
@@ -278,7 +278,7 @@
 	git init stale/src &&
 	scalar register stale &&
 	scalar list >scalar.repos &&
-	grep stale scalar.repos &&
+	test_grep stale scalar.repos &&
 
 	grep -v stale scalar.repos >expect &&
 
@@ -307,7 +307,7 @@
 
 test_expect_success '`scalar [...] <dir>` errors out when dir is missing' '
 	! scalar run config cloned 2>err &&
-	grep "cloned. does not exist" err
+	test_grep "cloned. does not exist" err
 '
 
 SQ="'"
@@ -318,7 +318,7 @@
 	echo "$(pwd)/.git/objects/" >>cloned/src/.git/objects/info/alternates &&
 	test_commit -C cloned/src loose &&
 	scalar diagnose cloned >out 2>err &&
-	grep "Available space" out &&
+	test_grep "Available space" out &&
 	sed -n "s/.*$SQ\\(.*\\.zip\\)$SQ.*/\\1/p" <err >zip_path &&
 	zip_path=$(cat zip_path) &&
 	test -n "$zip_path" &&
@@ -328,9 +328,9 @@
 	"$GIT_UNZIP" -p "$zip_path" diagnostics.log >out &&
 	test_file_not_empty out &&
 	"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
-	grep "$(pwd)/.git/objects" out &&
+	test_grep "$(pwd)/.git/objects" out &&
 	"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
-	grep "^Total: [1-9]" out
+	test_grep "^Total: [1-9]" out
 '
 
 test_done
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index 2043f48..002d6ec 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -86,7 +86,7 @@
 	test_config -C to-clone uploadpack.allowanysha1inwant false &&
 
 	scalar clone "file://$(pwd)/to-clone" $enlistment 2>err &&
-	grep "filtering not recognized by server, ignoring" err &&
+	test_grep "filtering not recognized by server, ignoring" err &&
 
 	(
 		cd $enlistment/src &&
@@ -133,7 +133,7 @@
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
 		test_line_count = 2 out &&
-		grep "refs/remotes/origin/base" out
+		test_grep "refs/remotes/origin/base" out
 	) &&
 
 	cleanup_clone $enlistment
@@ -147,8 +147,8 @@
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
 		test_line_count = 3 out &&
-		grep "refs/remotes/origin/base" out &&
-		grep "refs/remotes/origin/parallel" out
+		test_grep "refs/remotes/origin/base" out &&
+		test_grep "refs/remotes/origin/parallel" out
 	) &&
 
 	cleanup_clone $enlistment
@@ -174,15 +174,15 @@
 	test_config -C to-clone uploadpack.allowanysha1inwant true &&
 
 	GIT_PROGRESS_DELAY=0 scalar clone "file://$(pwd)/to-clone" "$enlistment" 2>stderr &&
-	! grep "Enumerating objects" stderr &&
-	! grep "Updating files" stderr &&
+	test_grep ! "Enumerating objects" stderr &&
+	test_grep ! "Updating files" stderr &&
 	cleanup_clone $enlistment
 '
 
 test_expect_success 'scalar clone warns when background maintenance fails' '
 	GIT_TEST_MAINT_SCHEDULER="crontab:false,launchctl:false,schtasks:false" \
 		scalar clone "file://$(pwd)/to-clone" maint-fail 2>err &&
-	grep "could not toggle maintenance" err
+	test_grep "could not toggle maintenance" err
 '
 
 test_expect_success 'scalar clone --no-maintenance' '
@@ -190,7 +190,7 @@
 	GIT_TRACE2_EVENT="$(pwd)/no-maint.event" \
 	GIT_TRACE2_EVENT_DEPTH=100 \
 		scalar clone --no-maintenance "file://$(pwd)/to-clone" no-maint 2>err &&
-	! grep "could not toggle maintenance" err &&
+	test_grep ! "could not toggle maintenance" err &&
 	test_subcommand ! git maintenance unregister --force <no-maint.event
 '
 
diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh
index 4794377..fe6c261 100755
--- a/t/t9300-fast-import.sh
+++ b/t/t9300-fast-import.sh
@@ -436,7 +436,7 @@
 	git init invalid-timezone &&
 	git -C invalid-timezone fast-import --date-format=raw-permissive <input &&
 	git -C invalid-timezone cat-file -p invalid-timezone >out &&
-	grep "1234567890 [+]051800" out
+	test_grep "1234567890 [+]051800" out
 '
 
 test_expect_success 'B: accept and fixup committer with no name' '
@@ -2326,7 +2326,7 @@
 	EOF
 
 	git fast-import --allow-unsafe-features <input &&
-	grep :1 git.marks
+	test_grep :1 git.marks
 '
 
 test_expect_success 'R: export-marks options can be overridden by commandline options' '
@@ -2340,7 +2340,7 @@
 	EOF
 	git fast-import --allow-unsafe-features \
 			--export-marks=cmdline-sub/other.marks <input &&
-	grep :1 cmdline-sub/other.marks &&
+	test_grep :1 cmdline-sub/other.marks &&
 	test_path_is_missing feature-sub
 '
 
@@ -3816,9 +3816,9 @@
 		sed -e s/othername/somename/ tmp >tmp2 &&
 		git fast-import --force <tmp2 2>msgs &&
 
-		grep "dropping.*since it would point to itself" msgs &&
+		test_grep "dropping.*since it would point to itself" msgs &&
 		git show-ref >refs &&
-		! grep refs/replace refs
+		test_grep ! refs/replace refs
 	)
 '
 
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index 784d68b..a487476 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -139,9 +139,9 @@
 	test $(($(test_oid hexsz) + 181)) -eq "$(git -C new cat-file -s i18n)" &&
 	# ...and for the expected translation of bytes.
 	git -C new cat-file commit i18n >actual &&
-	grep $(printf "\317\200") actual &&
+	test_grep $(printf "\317\200") actual &&
 	# Also make sure the commit does not have the "encoding" header
-	! grep ^encoding actual
+	test_grep ! ^encoding actual
 '
 
 test_expect_success 'aborting on iso-8859-7' '
@@ -170,9 +170,9 @@
 	test $(($(test_oid hexsz) + 200)) -eq "$(git -C new cat-file -s i18n-no-recoding)" &&
 	# ...as well as the expected byte.
 	git -C new cat-file commit i18n-no-recoding >actual &&
-	grep $(printf "\360") actual &&
+	test_grep $(printf "\360") actual &&
 	# Also make sure the commit has the "encoding" header
-	grep ^encoding actual
+	test_grep ^encoding actual
 '
 
 test_expect_success 'encoding preserved if reencoding fails' '
@@ -186,13 +186,13 @@
 	git -C new cat-file commit i18n-invalid >actual &&
 
 	# Make sure the commit still has the encoding header
-	grep ^encoding actual &&
+	test_grep ^encoding actual &&
 	# Verify that the commit has the expected size; i.e.
 	# that no bytes were re-encoded to a different encoding.
 	test $(($(test_oid hexsz) + 212)) -eq "$(git -C new cat-file -s i18n-invalid)" &&
 	# ...and check for the original special bytes
-	grep $(printf "\360") actual &&
-	grep $(printf "\377") actual
+	test_grep $(printf "\360") actual &&
+	test_grep $(printf "\377") actual
 '
 
 test_expect_success 'import/export-marks' '
@@ -244,14 +244,14 @@
 test_expect_success 'signed-tags=verbatim' '
 
 	git fast-export --signed-tags=verbatim sign-your-name > output &&
-	grep PGP output
+	test_grep PGP output
 
 '
 
 test_expect_success 'signed-tags=warn-verbatim' '
 
 	git fast-export --signed-tags=warn-verbatim sign-your-name >output 2>err &&
-	grep PGP output &&
+	test_grep PGP output &&
 	test -s err
 
 '
@@ -261,7 +261,7 @@
 test_expect_success 'signed-tags=warn' '
 
 	git fast-export --signed-tags=warn sign-your-name >output 2>err &&
-	grep PGP output &&
+	test_grep PGP output &&
 	test -s err
 
 '
@@ -269,13 +269,13 @@
 test_expect_success 'signed-tags=strip' '
 
 	git fast-export --signed-tags=strip sign-your-name > output &&
-	! grep PGP output
+	test_grep ! PGP output
 
 '
 
 test_expect_success 'signed-tags=warn-strip' '
 	git fast-export --signed-tags=warn-strip sign-your-name >output 2>err &&
-	! grep PGP output &&
+	test_grep ! PGP output &&
 	test -s err
 '
 
@@ -345,7 +345,7 @@
 
 	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
 	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
-	grep "encoding ISO-8859-1" output &&
+	test_grep "encoding ISO-8859-1" output &&
 	git -C new fast-import <output &&
 	STRIPPED=$(git -C new rev-parse --verify refs/heads/commit-signing) &&
 	test $COMMIT_SIGNING = $STRIPPED
@@ -356,7 +356,7 @@
 
 	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
 	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
-	grep "encoding ISO-8859-1" output &&
+	test_grep "encoding ISO-8859-1" output &&
 	test -s err &&
 	git -C new fast-import <output &&
 	STRIPPED=$(git -C new rev-parse --verify refs/heads/commit-signing) &&
@@ -367,8 +367,8 @@
 test_expect_success GPG 'signed-commits=strip' '
 
 	git fast-export --signed-commits=strip --reencode=no commit-signing >output &&
-	! grep ^gpgsig output &&
-	grep "^encoding ISO-8859-1" output &&
+	test_grep ! ^gpgsig output &&
+	test_grep "^encoding ISO-8859-1" output &&
 	sed "s/commit-signing/commit-strip-signing/" output | git -C new fast-import &&
 	STRIPPED=$(git -C new rev-parse --verify refs/heads/commit-strip-signing) &&
 	test $COMMIT_SIGNING != $STRIPPED
@@ -378,8 +378,8 @@
 test_expect_success GPG 'signed-commits=warn-strip' '
 
 	git fast-export --signed-commits=warn-strip --reencode=no commit-signing >output 2>err &&
-	! grep ^gpgsig output &&
-	grep "^encoding ISO-8859-1" output &&
+	test_grep ! ^gpgsig output &&
+	test_grep "^encoding ISO-8859-1" output &&
 	test -s err &&
 	sed "s/commit-signing/commit-strip-signing/" output | git -C new fast-import &&
 	STRIPPED=$(git -C new rev-parse --verify refs/heads/commit-strip-signing) &&
@@ -406,7 +406,7 @@
 	test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
 	git -C new fast-import <output &&
 	git -C new cat-file commit refs/heads/x509-signing >actual &&
-	grep "^gpgsig" actual &&
+	test_grep "^gpgsig" actual &&
 	IMPORTED=$(git -C new rev-parse refs/heads/x509-signing) &&
 	test $X509_COMMIT = $IMPORTED
 
@@ -431,7 +431,7 @@
 	test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
 	git -C new fast-import <output &&
 	git -C new cat-file commit refs/heads/ssh-signing >actual &&
-	grep "^gpgsig" actual &&
+	test_grep "^gpgsig" actual &&
 	IMPORTED=$(git -C new rev-parse refs/heads/ssh-signing) &&
 	test $SSH_COMMIT = $IMPORTED
 
@@ -520,7 +520,7 @@
 	mkdir new &&
 	git --git-dir=new/.git init &&
 	git fast-export -C -C --signed-tags=strip --all > output &&
-	grep "^C file2 file4\$" output &&
+	test_grep "^C file2 file4\$" output &&
 	git -C new fast-import <output &&
 	test $ENTRY = $(git -C new rev-parse --verify refs/heads/copy)
 
@@ -548,11 +548,11 @@
 	ANNOTATED_TAG_COUNT=$((ANNOTATED_TAG_COUNT + 1)) &&
 	git fast-export -C -C --signed-tags=strip --all > output &&
 	test $(grep -c "^tag " output) = $ANNOTATED_TAG_COUNT &&
-	! grep "Unspecified Tagger" output &&
+	test_grep ! "Unspecified Tagger" output &&
 	git fast-export -C -C --signed-tags=strip --all \
 		--fake-missing-tagger > output &&
 	test $(grep -c "^tag " output) = $ANNOTATED_TAG_COUNT &&
-	grep "Unspecified Tagger" output
+	test_grep "Unspecified Tagger" output
 
 '
 
@@ -628,7 +628,7 @@
 		test_commit bar &&
 
 		git fast-export --tag-of-filtered-object=rewrite --all -- bar.t >output &&
-		grep from.$ZERO_OID output
+		test_grep from.$ZERO_OID output
 	)
 '
 
@@ -670,7 +670,7 @@
 	test_tick &&
 	git commit -mnext file &&
 	git fast-export --import-marks=marks simple -- file file0 >actual &&
-	grep file0 actual
+	test_grep file0 actual
 '
 
 test_expect_success 'path limiting works' '
@@ -753,7 +753,7 @@
 test_expect_success 'handling nested tags' '
 	git tag -a -m "This is a nested tag" nested muss &&
 	git fast-export --mark-tags nested >output &&
-	grep "^from $ZERO_OID$" output &&
+	test_grep "^from $ZERO_OID$" output &&
 	grep "^tag nested$" output >tag_lines &&
 	test_line_count = 2 tag_lines
 '
@@ -926,7 +926,7 @@
 
 		echo ":1 $(git rev-parse HEAD^^)" >marks &&
 		git fast-export --import-marks=marks main >out &&
-		grep Yeah out
+		test_grep Yeah out
 	)
 '
 
diff --git a/t/t9351-fast-export-anonymize.sh b/t/t9351-fast-export-anonymize.sh
index 156a647..ed784a5 100755
--- a/t/t9351-fast-export-anonymize.sh
+++ b/t/t9351-fast-export-anonymize.sh
@@ -33,44 +33,44 @@
 
 # this also covers commit messages
 test_expect_success 'stream omits path names' '
-	! grep base stream &&
-	! grep foo stream &&
-	! grep subdir stream &&
-	! grep bar stream &&
-	! grep xyzzy stream
+	test_grep ! base stream &&
+	test_grep ! foo stream &&
+	test_grep ! subdir stream &&
+	test_grep ! bar stream &&
+	test_grep ! xyzzy stream
 '
 
 test_expect_success 'stream contains user-specified names' '
-	grep retain-me stream &&
-	! grep should-not-appear stream &&
-	grep custom-name stream
+	test_grep retain-me stream &&
+	test_grep ! should-not-appear stream &&
+	test_grep custom-name stream
 '
 
 test_expect_success 'stream omits gitlink oids' '
 	# avoid relying on the whole oid to remain hash-agnostic; this is
 	# plenty to be unique within our test case
-	! grep a000000000000000000 stream
+	test_grep ! a000000000000000000 stream
 '
 
 test_expect_success 'stream retains other as refname' '
-	grep other stream
+	test_grep other stream
 '
 
 test_expect_success 'stream omits other refnames' '
-	! grep main stream &&
-	! grep mytag stream &&
-	! grep longtag stream
+	test_grep ! main stream &&
+	test_grep ! mytag stream &&
+	test_grep ! longtag stream
 '
 
 test_expect_success 'stream omits identities' '
-	! grep "$GIT_COMMITTER_NAME" stream &&
-	! grep "$GIT_COMMITTER_EMAIL" stream &&
-	! grep "$GIT_AUTHOR_NAME" stream &&
-	! grep "$GIT_AUTHOR_EMAIL" stream
+	test_grep ! "$GIT_COMMITTER_NAME" stream &&
+	test_grep ! "$GIT_COMMITTER_EMAIL" stream &&
+	test_grep ! "$GIT_AUTHOR_NAME" stream &&
+	test_grep ! "$GIT_AUTHOR_EMAIL" stream
 '
 
 test_expect_success 'stream omits tag message' '
-	! grep "annotated tag" stream
+	test_grep ! "annotated tag" stream
 '
 
 # NOTE: we chdir to the new, anonymized repository
diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh
index 4b45398..b2379e4 100755
--- a/t/t9400-git-cvsserver-server.sh
+++ b/t/t9400-git-cvsserver-server.sh
@@ -254,7 +254,7 @@
    else
      true
    fi &&
-   grep "GITCVS emulation disabled" cvs.log &&
+   test_grep "GITCVS emulation disabled" cvs.log &&
    test ! -d cvswork2'
 
 rm -fr cvswork2
@@ -276,7 +276,7 @@
 	else
 		true
 	fi &&
-	grep "GITCVS emulation disabled" cvs.log &&
+	test_grep "GITCVS emulation disabled" cvs.log &&
 	test ! -d cvswork2
 '
 
diff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh
index 32814e7..1093fda 100755
--- a/t/t9501-gitweb-standalone-http-status.sh
+++ b/t/t9501-gitweb-standalone-http-status.sh
@@ -37,13 +37,13 @@
 test_expect_success \
     'snapshots: tgz only default format enabled' \
     'gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tgz" &&
-    grep "Status: 200 OK" gitweb.output &&
+    test_grep "Status: 200 OK" gitweb.output &&
     gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tbz2" &&
-    grep "403 - Unsupported snapshot format" gitweb.output &&
+    test_grep "403 - Unsupported snapshot format" gitweb.output &&
     gitweb_run "p=.git;a=snapshot;h=HEAD;sf=txz" &&
-    grep "403 - Snapshot format not allowed" gitweb.output &&
+    test_grep "403 - Snapshot format not allowed" gitweb.output &&
     gitweb_run "p=.git;a=snapshot;h=HEAD;sf=zip" &&
-    grep "403 - Unsupported snapshot format" gitweb.output'
+    test_grep "403 - Unsupported snapshot format" gitweb.output'
 
 
 cat >>gitweb_config.perl <<\EOF
@@ -53,13 +53,13 @@
 test_expect_success \
     'snapshots: all enabled in default, use default disabled value' \
     'gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tgz" &&
-    grep "Status: 200 OK" gitweb.output &&
+    test_grep "Status: 200 OK" gitweb.output &&
     gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tbz2" &&
-    grep "Status: 200 OK" gitweb.output &&
+    test_grep "Status: 200 OK" gitweb.output &&
     gitweb_run "p=.git;a=snapshot;h=HEAD;sf=txz" &&
-    grep "403 - Snapshot format not allowed" gitweb.output &&
+    test_grep "403 - Snapshot format not allowed" gitweb.output &&
     gitweb_run "p=.git;a=snapshot;h=HEAD;sf=zip" &&
-    grep "Status: 200 OK" gitweb.output'
+    test_grep "Status: 200 OK" gitweb.output'
 
 
 cat >>gitweb_config.perl <<\EOF
@@ -69,7 +69,7 @@
 test_expect_success \
     'snapshots: zip explicitly disabled' \
     'gitweb_run "p=.git;a=snapshot;h=HEAD;sf=zip" &&
-    grep "403 - Snapshot format not allowed" gitweb.output'
+    test_grep "403 - Snapshot format not allowed" gitweb.output'
 test_debug 'cat gitweb.output'
 
 
@@ -80,7 +80,7 @@
 test_expect_success \
     'snapshots: tgz explicitly enabled' \
     'gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tgz" &&
-    grep "Status: 200 OK" gitweb.output'
+    test_grep "Status: 200 OK" gitweb.output'
 test_debug 'cat gitweb.headers'
 
 
@@ -89,13 +89,13 @@
 
 test_expect_success 'snapshots: good tree-ish id' '
 	gitweb_run "p=.git;a=snapshot;h=main;sf=tgz" &&
-	grep "Status: 200 OK" gitweb.output
+	test_grep "Status: 200 OK" gitweb.output
 '
 test_debug 'cat gitweb.headers'
 
 test_expect_success 'snapshots: bad tree-ish id' '
 	gitweb_run "p=.git;a=snapshot;h=frizzumFrazzum;sf=tgz" &&
-	grep "404 - Object does not exist" gitweb.output
+	test_grep "404 - Object does not exist" gitweb.output
 '
 test_debug 'cat gitweb.output'
 
@@ -105,20 +105,20 @@
 	test_tick && git commit -m "Object to be tagged" &&
 	git tag tagged-object $(git hash-object tag-object) &&
 	gitweb_run "p=.git;a=snapshot;h=tagged-object;sf=tgz" &&
-	grep "400 - Object is not a tree-ish" gitweb.output
+	test_grep "400 - Object is not a tree-ish" gitweb.output
 '
 test_debug 'cat gitweb.output'
 
 test_expect_success 'snapshots: good object id' '
 	ID=$(git rev-parse --verify HEAD) &&
 	gitweb_run "p=.git;a=snapshot;h=$ID;sf=tgz" &&
-	grep "Status: 200 OK" gitweb.output
+	test_grep "Status: 200 OK" gitweb.output
 '
 test_debug 'cat gitweb.headers'
 
 test_expect_success 'snapshots: bad object id' '
 	gitweb_run "p=.git;a=snapshot;h=abcdef01234;sf=tgz" &&
-	grep "404 - Object does not exist" gitweb.output
+	test_grep "404 - Object does not exist" gitweb.output
 '
 test_debug 'cat gitweb.output'
 
@@ -127,8 +127,8 @@
 
 test_expect_success DATE_PARSER 'modification: feed last-modified' '
 	gitweb_run "p=.git;a=atom;h=main" &&
-	grep "Status: 200 OK" gitweb.headers &&
-	grep "Last-modified: Thu, 7 Apr 2005 22:14:13 +0000" gitweb.headers
+	test_grep "Status: 200 OK" gitweb.headers &&
+	test_grep "Last-modified: Thu, 7 Apr 2005 22:14:13 +0000" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
@@ -137,7 +137,7 @@
 	export HTTP_IF_MODIFIED_SINCE &&
 	test_when_finished "unset HTTP_IF_MODIFIED_SINCE" &&
 	gitweb_run "p=.git;a=atom;h=main" &&
-	grep "Status: 200 OK" gitweb.headers
+	test_grep "Status: 200 OK" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
@@ -146,14 +146,14 @@
 	export HTTP_IF_MODIFIED_SINCE &&
 	test_when_finished "unset HTTP_IF_MODIFIED_SINCE" &&
 	gitweb_run "p=.git;a=atom;h=main" &&
-	grep "Status: 304 Not Modified" gitweb.headers
+	test_grep "Status: 304 Not Modified" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
 test_expect_success DATE_PARSER 'modification: snapshot last-modified' '
 	gitweb_run "p=.git;a=snapshot;h=main;sf=tgz" &&
-	grep "Status: 200 OK" gitweb.headers &&
-	grep "Last-modified: Thu, 7 Apr 2005 22:14:13 +0000" gitweb.headers
+	test_grep "Status: 200 OK" gitweb.headers &&
+	test_grep "Last-modified: Thu, 7 Apr 2005 22:14:13 +0000" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
@@ -162,7 +162,7 @@
 	export HTTP_IF_MODIFIED_SINCE &&
 	test_when_finished "unset HTTP_IF_MODIFIED_SINCE" &&
 	gitweb_run "p=.git;a=snapshot;h=main;sf=tgz" &&
-	grep "Status: 200 OK" gitweb.headers
+	test_grep "Status: 200 OK" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
@@ -171,7 +171,7 @@
 	export HTTP_IF_MODIFIED_SINCE &&
 	test_when_finished "unset HTTP_IF_MODIFIED_SINCE" &&
 	gitweb_run "p=.git;a=snapshot;h=main;sf=tgz" &&
-	grep "Status: 304 Not Modified" gitweb.headers
+	test_grep "Status: 304 Not Modified" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
@@ -181,8 +181,8 @@
 	export HTTP_IF_MODIFIED_SINCE &&
 	test_when_finished "unset HTTP_IF_MODIFIED_SINCE" &&
 	gitweb_run "p=.git;a=snapshot;h=$ID;sf=tgz" &&
-	grep "Status: 200 OK" gitweb.headers &&
-	! grep -i "last-modified" gitweb.headers
+	test_grep "Status: 200 OK" gitweb.headers &&
+	test_grep ! -i "last-modified" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
@@ -196,8 +196,8 @@
 
 test_expect_success 'load checking: load too high (default action)' '
 	gitweb_run "p=.git" &&
-	grep "Status: 503 Service Unavailable" gitweb.headers &&
-	grep "503 - The load average on the server is too high" gitweb.body
+	test_grep "Status: 503 Service Unavailable" gitweb.headers &&
+	test_grep "503 - The load average on the server is too high" gitweb.body
 '
 test_debug 'cat gitweb.headers'
 
@@ -212,8 +212,8 @@
 
 test_expect_success 'invalid arguments: invalid regexp (in project search)' '
 	gitweb_run "a=project_list;s=*\.git;sr=1" &&
-	grep "Status: 400" gitweb.headers &&
-	grep "400 - Invalid.*regexp" gitweb.body
+	test_grep "Status: 400" gitweb.headers &&
+	test_grep "400 - Invalid.*regexp" gitweb.body
 '
 test_debug 'cat gitweb.headers'
 
diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh
index 81d5625..00cf077 100755
--- a/t/t9502-gitweb-standalone-parse-output.sh
+++ b/t/t9502-gitweb-standalone-parse-output.sh
@@ -111,10 +111,23 @@
 
 test_expect_success 'snapshot: hierarchical branch name (xx/test)' '
 	gitweb_run "p=.git;a=snapshot;h=xx/test;sf=tar" &&
-	! grep "filename=.*/" gitweb.headers
+	test_grep ! "filename=.*/" gitweb.headers
 '
 test_debug 'cat gitweb.headers'
 
+test_expect_success 'commitdiff: index line shortens hashes with mode' '
+	old_blob=$(git rev-parse HEAD:foo) &&
+	old_short=$(git rev-parse --short=7 HEAD:foo) &&
+	echo changed >foo &&
+	git commit -am "change foo" &&
+	new_blob=$(git rev-parse HEAD:foo) &&
+	new_short=$(git rev-parse --short=7 HEAD:foo) &&
+	gitweb_run "p=.git;a=commitdiff;h=HEAD" &&
+	test_grep ">${old_short}</a>\\.\\.<a [^>]*>${new_short}</a> 100644<span class=\"info\"> (file)</span>" \
+		gitweb.body &&
+	test_grep ! "index ${old_blob}\\.\\.${new_blob} 100644" gitweb.body
+'
+
 # ----------------------------------------------------------------------
 # forks of projects
 
@@ -139,13 +152,13 @@
 
 test_expect_success 'forks: not skipped unless "forks" feature enabled' '
 	gitweb_run "a=project_list" &&
-	grep -q ">\\.git<"               gitweb.body &&
-	grep -q ">foo\\.git<"            gitweb.body &&
-	grep -q ">foo_baz\\.git<"        gitweb.body &&
-	grep -q ">foo\\.bar\\.git<"      gitweb.body &&
-	grep -q ">foo_baz\\.git<"        gitweb.body &&
-	grep -q ">foo/foo-forked\\.git<" gitweb.body &&
-	grep -q ">fork of .*<"           gitweb.body
+	test_grep -q ">\\.git<"               gitweb.body &&
+	test_grep -q ">foo\\.git<"            gitweb.body &&
+	test_grep -q ">foo_baz\\.git<"        gitweb.body &&
+	test_grep -q ">foo\\.bar\\.git<"      gitweb.body &&
+	test_grep -q ">foo_baz\\.git<"        gitweb.body &&
+	test_grep -q ">foo/foo-forked\\.git<" gitweb.body &&
+	test_grep -q ">fork of .*<"           gitweb.body
 '
 
 test_expect_success 'enable forks feature' '
@@ -156,25 +169,25 @@
 
 test_expect_success 'forks: forks skipped if "forks" feature enabled' '
 	gitweb_run "a=project_list" &&
-	grep -q ">\\.git<"               gitweb.body &&
-	grep -q ">foo\\.git<"            gitweb.body &&
-	grep -q ">foo_baz\\.git<"        gitweb.body &&
-	grep -q ">foo\\.bar\\.git<"      gitweb.body &&
-	grep -q ">foo_baz\\.git<"        gitweb.body &&
-	grep -v ">foo/foo-forked\\.git<" gitweb.body &&
-	grep -v ">fork of .*<"           gitweb.body
+	test_grep -q ">\\.git<"               gitweb.body &&
+	test_grep -q ">foo\\.git<"            gitweb.body &&
+	test_grep -q ">foo_baz\\.git<"        gitweb.body &&
+	test_grep -q ">foo\\.bar\\.git<"      gitweb.body &&
+	test_grep -q ">foo_baz\\.git<"        gitweb.body &&
+	test_grep -v ">foo/foo-forked\\.git<" gitweb.body &&
+	test_grep -v ">fork of .*<"           gitweb.body
 '
 
 test_expect_success 'forks: "forks" action for forked repository' '
 	gitweb_run "p=foo.git;a=forks" &&
-	grep -q ">foo/foo-forked\\.git<" gitweb.body &&
-	grep -q ">fork of foo<"          gitweb.body
+	test_grep -q ">foo/foo-forked\\.git<" gitweb.body &&
+	test_grep -q ">fork of foo<"          gitweb.body
 '
 
 test_expect_success 'forks: can access forked repository' '
 	gitweb_run "p=foo/foo-forked.git;a=summary" &&
-	grep -q "200 OK"        gitweb.headers &&
-	grep -q ">fork of foo<" gitweb.body
+	test_grep -q "200 OK"        gitweb.headers &&
+	test_grep -q ">fork of foo<" gitweb.body
 '
 
 test_expect_success 'forks: project_index lists all projects (incl. forks)' '
diff --git a/t/t9800-git-p4-basic.sh b/t/t9800-git-p4-basic.sh
index 0816763..140a3a7 100755
--- a/t/t9800-git-p4-basic.sh
+++ b/t/t9800-git-p4-basic.sh
@@ -35,7 +35,7 @@
 
 test_expect_success 'depot typo error' '
 	test_must_fail git p4 clone --dest="$git" /depot 2>errs &&
-	grep "Depot paths must start with" errs
+	test_grep "Depot paths must start with" errs
 '
 
 test_expect_success 'git p4 clone @all' '
@@ -356,7 +356,7 @@
 		P4PORT=nosuchhost:65537 &&
 		export P4PORT &&
 		test_expect_code 1 git p4 sync >out 2>err &&
-		grep "connect to nosuchhost" err
+		test_grep "connect to nosuchhost" err
 	)
 '
 
@@ -374,7 +374,7 @@
 		git commit -m "add hello.txt" &&
 		git config git-p4.skipSubmitEdit true &&
 		git p4 submit --dry-run >out &&
-		grep "Would apply" out
+		test_grep "Would apply" out
 	) &&
 	test_hook -C "$git" p4-pre-submit <<-\EOF &&
 	exit 0
@@ -382,7 +382,7 @@
 	(
 		cd "$git" &&
 		git p4 submit --dry-run >out &&
-		grep "Would apply" out
+		test_grep "Would apply" out
 	) &&
 	test_hook -C "$git" --clobber p4-pre-submit <<-\EOF &&
 	exit 1
@@ -390,7 +390,7 @@
 	(
 		cd "$git" &&
 		test_must_fail git p4 submit --dry-run >errs 2>&1 &&
-		! grep "Would apply" errs
+		test_grep ! "Would apply" errs
 	)
 '
 
diff --git a/t/t9801-git-p4-branch.sh b/t/t9801-git-p4-branch.sh
index c598011..9912b48 100755
--- a/t/t9801-git-p4-branch.sh
+++ b/t/t9801-git-p4-branch.sh
@@ -216,17 +216,17 @@
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch2 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test ! -f file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch3 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		cd "$cli" &&
 		cd branch1 &&
 		p4 edit file2 &&
@@ -235,7 +235,7 @@
 		cd "$git" &&
 		git reset --hard p4/depot/branch1 &&
 		git p4 rebase &&
-		grep file2_ file2
+		test_grep file2_ file2
 	)
 '
 
@@ -292,29 +292,29 @@
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch2 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_missing file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch3 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch4 &&
 		git diff-tree --quiet HEAD &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_missing file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch5 &&
 		git diff-tree --quiet HEAD &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		test_must_fail git show-ref --verify refs/git-p4-tmp
 	)
 '
@@ -346,27 +346,27 @@
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch2 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_missing file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch3 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_missing file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch4 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch5 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		test_must_fail git show-ref --verify refs/git-p4-tmp
 	)
 '
@@ -398,27 +398,27 @@
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch2 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_missing file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch3 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_missing file3 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch4 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch5 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch6 &&
 		test_path_is_file file1 &&
 		test_path_is_missing file2 &&
@@ -622,19 +622,19 @@
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
 		test_path_is_file sub_file1 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		git reset --hard p4/depot/branch2 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test ! -f file3 &&
 		test_path_is_file sub_file1 &&
-		! grep update file2 &&
+		test_grep ! update file2 &&
 		git reset --hard p4/depot/branch3 &&
 		test_path_is_file file1 &&
 		test_path_is_file file2 &&
 		test_path_is_file file3 &&
 		test_path_is_file sub_file1 &&
-		grep update file2 &&
+		test_grep update file2 &&
 		cd "$cli" &&
 		cd branch1 &&
 		p4 edit file2 &&
@@ -643,7 +643,7 @@
 		cd "$git" &&
 		git reset --hard p4/depot/branch1 &&
 		git p4 rebase &&
-		grep file2_ file2
+		test_grep file2_ file2
 	)
 '
 
@@ -661,7 +661,7 @@
 		cd "$cli" &&
 		p4 sync ... &&
 		cd branch1 &&
-		grep "client spec" file1
+		test_grep "client spec" file1
 	)
 '
 
diff --git a/t/t9806-git-p4-options.sh b/t/t9806-git-p4-options.sh
index c26d297..dce710e 100755
--- a/t/t9806-git-p4-options.sh
+++ b/t/t9806-git-p4-options.sh
@@ -49,7 +49,7 @@
 	(
 		cd "$git" &&
 		test_must_fail git p4 sync 2>err &&
-		grep "Error: no branch refs/remotes/p4/master" err
+		test_grep "Error: no branch refs/remotes/p4/master" err
 	)
 '
 
@@ -85,9 +85,9 @@
 		git p4 sync --branch=refs/remotes/p4/b2 //depot@2 &&
 		git p4 sync &&
 		git show -s --format=%s refs/remotes/p4/b1 >show &&
-		grep "Initial import" show &&
+		test_grep "Initial import" show &&
 		git show -s --format=%s refs/remotes/p4/b2 >show &&
-		grep "Initial import" show
+		test_grep "Initial import" show
 	)
 '
 
@@ -100,9 +100,9 @@
 		git p4 sync --branch=b2 //depot@2 &&
 		git p4 sync --branch=b2 &&
 		git show -s --format=%s refs/remotes/p4/b1 >show &&
-		grep "Initial import" show &&
+		test_grep "Initial import" show &&
 		git show -s --format=%s refs/remotes/p4/b2 >show &&
-		grep "change 3" show
+		test_grep "change 3" show
 	)
 '
 
diff --git a/t/t9807-git-p4-submit.sh b/t/t9807-git-p4-submit.sh
index 6ae7ced..adbdf93 100755
--- a/t/t9807-git-p4-submit.sh
+++ b/t/t9807-git-p4-submit.sh
@@ -542,7 +542,7 @@
 		cd "$cli" &&
 		change=$(last_shelve) &&
 		p4 unshelve -c $change -s $change &&
-		grep -q updated-line shelf.t &&
+		test_grep -q updated-line shelf.t &&
 		p4 describe -S $change | grep added-file.t &&
 		test_path_is_missing shelved-change-1.t &&
 		p4 revert ...
diff --git a/t/t9810-git-p4-rcs.sh b/t/t9810-git-p4-rcs.sh
index 5fe8331..70355a3 100755
--- a/t/t9810-git-p4-rcs.sh
+++ b/t/t9810-git-p4-rcs.sh
@@ -299,12 +299,12 @@
 		echo "\$Revision\$" >kwdelfile.c &&
 		p4 add -t ktext kwdelfile.c &&
 		p4 submit -d "Add file to be deleted" &&
-		grep 1 kwdelfile.c
+		test_grep 1 kwdelfile.c
 	) &&
 	git p4 clone --dest="$git" //depot &&
 	(
 		cd "$git" &&
-		grep Revision kwdelfile.c &&
+		test_grep Revision kwdelfile.c &&
 		git rm -f kwdelfile.c &&
 		git commit -m "Delete a file containing RCS keywords" &&
 		git config git-p4.skipSubmitEdit true &&
@@ -336,7 +336,7 @@
 		cd "$cli" &&
 		p4 sync &&
 		test -f kwfile1.c &&
-		grep "NewKW.*Revision.*[0-9]" kwfile1.c
+		test_grep "NewKW.*Revision.*[0-9]" kwfile1.c
 
 	)
 '
@@ -359,7 +359,7 @@
 	(
 		cd "$cli" &&
 		p4 sync &&
-		grep "NewKW2.*Revision.*[0-9]" kwfile1.c
+		test_grep "NewKW2.*Revision.*[0-9]" kwfile1.c
 
 	)
 '
diff --git a/t/t9811-git-p4-label-import.sh b/t/t9811-git-p4-label-import.sh
index 7614dfb..ee25701 100755
--- a/t/t9811-git-p4-label-import.sh
+++ b/t/t9811-git-p4-label-import.sh
@@ -62,9 +62,11 @@
 
 		cd main &&
 		git checkout TAG_F1_ONLY &&
-		! test -f f2 &&
+		test_path_is_missing f2 &&
 		git checkout TAG_WITH\$_SHELL_CHAR &&
-		test -f f1 && test -f f2 && test -f file_with_\$metachar &&
+		test_path_is_file f1 &&
+		test_path_is_file f2 &&
+		test_path_is_file file_with_\$metachar &&
 
 		git show TAG_LONG_LABEL | grep -q "A Label second line"
 	)
@@ -102,11 +104,11 @@
 
 		git checkout TAG_F1_1 &&
 		ls &&
-		test -f f1 &&
+		test_path_is_file f1 &&
 
 		git checkout TAG_F1_2 &&
 		ls &&
-		test -f f1
+		test_path_is_file f1
 	)
 '
 
@@ -135,9 +137,9 @@
 		p4 labels ... | grep LIGHTWEIGHT_TAG &&
 		p4 label -o GIT_TAG_1 | grep "tag created in git:xyzzy" &&
 		p4 sync ...@GIT_TAG_1 &&
-		! test -f main/f10 &&
+		test_path_is_missing main/f10 &&
 		p4 sync ...@GIT_TAG_2 &&
-		test -f main/f10
+		test_path_is_file main/f10
 	)
 '
 
@@ -168,9 +170,9 @@
 		cd "$cli" &&
 		p4 sync ... &&
 		p4 sync ...@GIT_TAG_ON_DELETED &&
-		test -f main/deleted_file &&
+		test_path_is_file main/deleted_file &&
 		p4 sync ...@GIT_TAG_AFTER_DELETION &&
-		! test -f main/deleted_file &&
+		test_path_is_missing main/deleted_file &&
 		echo "checking label contents" &&
 		p4 label -o GIT_TAG_ON_DELETED | grep "tag on deleted file"
 	)
@@ -231,17 +233,25 @@
 		P4CLIENT=missing-revision &&
 		client_view "//depot/missing-revision/... //missing-revision/..." &&
 		cd "$cli" &&
-		>f1 && p4 add f1 && p4 submit -d "start" &&
+		>f1 &&
+		p4 add f1 &&
+		p4 submit -d "start" &&
 
 		p4 tag -l TAG_S0 ... &&
 
-		>f2 && p4 add f2 && p4 submit -d "second" &&
+		>f2 &&
+		p4 add f2 &&
+		p4 submit -d "second" &&
 
 		startrev=$(p4_head_revision //depot/missing-revision/...) &&
 
-		>f3 && p4 add f3 && p4 submit -d "third" &&
+		>f3 &&
+		p4 add f3 &&
+		p4 submit -d "third" &&
 
-		p4 edit f2 && date >f2 && p4 submit -d "change" f2 &&
+		p4 edit f2 &&
+		date >f2 &&
+		p4 submit -d "change" f2 &&
 
 		endrev=$(p4_head_revision //depot/missing-revision/...) &&
 
diff --git a/t/t9813-git-p4-preserve-users.sh b/t/t9813-git-p4-preserve-users.sh
index fd018c8..838ddd3 100755
--- a/t/t9813-git-p4-preserve-users.sh
+++ b/t/t9813-git-p4-preserve-users.sh
@@ -119,20 +119,20 @@
 		P4EDITOR=cat P4USER=alice P4PASSWD=secret &&
 		export P4EDITOR P4USER P4PASSWD &&
 		git p4 commit >actual &&
-		grep "git author derek@example.com does not match" actual &&
+		test_grep "git author derek@example.com does not match" actual &&
 
 		make_change_by_user usernamefile3 Charlie charlie@example.com &&
 		git p4 commit >actual &&
-		grep "git author charlie@example.com does not match" actual &&
+		test_grep "git author charlie@example.com does not match" actual &&
 
 		make_change_by_user usernamefile3 alice alice@example.com &&
 		git p4 commit >actual &&
-		! grep "git author.*does not match" actual &&
+		test_grep ! "git author.*does not match" actual &&
 
 		git config git-p4.skipUserNameCheck true &&
 		make_change_by_user usernamefile3 Charlie charlie@example.com &&
 		git p4 commit >actual &&
-		! grep "git author.*does not match" actual &&
+		test_grep ! "git author.*does not match" actual &&
 
 		p4_check_commit_author usernamefile3 alice
 	)
diff --git a/t/t9814-git-p4-rename.sh b/t/t9814-git-p4-rename.sh
index 2a9838f..6c5ecb9 100755
--- a/t/t9814-git-p4-rename.sh
+++ b/t/t9814-git-p4-rename.sh
@@ -66,7 +66,7 @@
 		git diff-tree -r -M HEAD &&
 		git p4 submit &&
 		p4 filelog //depot/file4 >filelog &&
-		! grep " from //depot" filelog &&
+		test_grep ! " from //depot" filelog &&
 
 		git mv file4 file5 &&
 		git commit -a -m "Rename file4 to file5" &&
@@ -74,7 +74,7 @@
 		git config git-p4.detectRenames true &&
 		git p4 submit &&
 		p4 filelog //depot/file5 >filelog &&
-		grep " from //depot/file4" filelog &&
+		test_grep " from //depot/file4" filelog &&
 
 		git mv file5 file6 &&
 		echo update >>file6 &&
@@ -86,7 +86,7 @@
 		git config git-p4.detectRenames $(($level + 2)) &&
 		git p4 submit &&
 		p4 filelog //depot/file6 >filelog &&
-		! grep " from //depot" filelog &&
+		test_grep ! " from //depot" filelog &&
 
 		git mv file6 file7 &&
 		echo update >>file7 &&
@@ -98,7 +98,7 @@
 		git config git-p4.detectRenames $(($level - 2)) &&
 		git p4 submit &&
 		p4 filelog //depot/file7 >filelog &&
-		grep " from //depot/file6" filelog
+		test_grep " from //depot/file6" filelog
 	)
 '
 
diff --git a/t/t9827-git-p4-change-filetype.sh b/t/t9827-git-p4-change-filetype.sh
index d3670bd..9a8231a 100755
--- a/t/t9827-git-p4-change-filetype.sh
+++ b/t/t9827-git-p4-change-filetype.sh
@@ -36,7 +36,7 @@
 		git commit -m "symlink file1 to file2" &&
 		git p4 submit &&
 		p4 filelog -m 1 //depot/file2 >filelog &&
-		grep "(symlink)" filelog
+		test_grep "(symlink)" filelog
 	)
 '
 
@@ -55,7 +55,7 @@
 		git commit -m "re-write file2" &&
 		git p4 submit &&
 		p4 filelog -m 1 //depot/file2 >filelog &&
-		grep "(text)" filelog
+		test_grep "(text)" filelog
 	)
 '
 
diff --git a/t/t9832-unshelve.sh b/t/t9832-unshelve.sh
index 6b3cb04..f85a60a 100755
--- a/t/t9832-unshelve.sh
+++ b/t/t9832-unshelve.sh
@@ -69,7 +69,7 @@
 		change=$(last_shelved_change) &&
 		git p4 unshelve $change &&
 		git show refs/remotes/p4-unshelved/$change >actual &&
-		grep -q "Further description" actual &&
+		test_grep -q "Further description" actual &&
 		git cherry-pick refs/remotes/p4-unshelved/$change &&
 		test_path_is_file file2 &&
 		test_cmp file1 "$cli"/file1 &&
@@ -141,7 +141,7 @@
 		shelve_one_file //depot/file1 &&
 		change=$(last_shelved_change) &&
 		p4 describe -S $change >out.txt &&
-		grep -q "Change to be unshelved" out.txt
+		test_grep -q "Change to be unshelved" out.txt
 	)
 '
 
@@ -152,7 +152,7 @@
 		change=$(last_shelved_change) &&
 		cd "$git" &&
 		git p4 unshelve $change >out.txt &&
-		grep -q "unshelved changelist $change" out.txt
+		test_grep -q "unshelved changelist $change" out.txt
 	)
 '
 
diff --git a/t/t9833-errors.sh b/t/t9833-errors.sh
index e22369c..b312669 100755
--- a/t/t9833-errors.sh
+++ b/t/t9833-errors.sh
@@ -29,7 +29,7 @@
 		P4PASSWD=badpassword &&
 		export P4PASSWD &&
 		test_must_fail git p4 clone //depot/foo 2>errmsg &&
-		grep -q "failure accessing depot.*P4PASSWD" errmsg
+		test_grep -q "failure accessing depot.*P4PASSWD" errmsg
 	)
 '
 
@@ -41,7 +41,7 @@
 		test_commit "ticket-auth-check" &&
 		p4 logout &&
 		test_must_fail git p4 submit 2>errmsg &&
-		grep -q "failure accessing depot" errmsg
+		test_grep -q "failure accessing depot" errmsg
 	)
 '
 
diff --git a/t/t9835-git-p4-metadata-encoding-python2.sh b/t/t9835-git-p4-metadata-encoding-python2.sh
index b969c7e..7eb0c8e 100755
--- a/t/t9835-git-p4-metadata-encoding-python2.sh
+++ b/t/t9835-git-p4-metadata-encoding-python2.sh
@@ -84,7 +84,7 @@
 	test_when_finished cleanup_git &&
 	test_when_finished remove_user_cache &&
 	test_must_fail git -c git-p4.metadataDecodingStrategy=strict p4-python2 clone --dest="$git" //depot@all 2>err &&
-	grep "Decoding perforce metadata failed!" err
+	test_grep "Decoding perforce metadata failed!" err
 '
 
 test_expect_success 'check utf-8 contents with passthrough strategy' '
@@ -94,8 +94,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "some utf-8 tǣxt" actual &&
-		grep "ǣuthor" actual
+		test_grep "some utf-8 tǣxt" actual &&
+		test_grep "ǣuthor" actual
 	)
 '
 
@@ -107,9 +107,9 @@
 		cd "$git" &&
 		git log >actual &&
 		badly_encoded_in_git=$(echo "some latin-1 tæxt" | iconv -f utf8 -t latin1) &&
-		grep "$badly_encoded_in_git" actual &&
+		test_grep "$badly_encoded_in_git" actual &&
 		bad_author_in_git="$(echo æuthor | iconv -f utf8 -t latin1)" &&
-		grep "$bad_author_in_git" actual
+		test_grep "$bad_author_in_git" actual
 	)
 '
 
@@ -120,8 +120,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "some utf-8 tǣxt" actual &&
-		grep "ǣuthor" actual
+		test_grep "some utf-8 tǣxt" actual &&
+		test_grep "ǣuthor" actual
 	)
 '
 
@@ -132,8 +132,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "some latin-1 tæxt" actual &&
-		grep "æuthor" actual
+		test_grep "some latin-1 tæxt" actual &&
+		test_grep "æuthor" actual
 	)
 '
 
@@ -144,8 +144,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "sœme cp-1252 tæxt" actual &&
-		grep "æuthœr" actual
+		test_grep "sœme cp-1252 tæxt" actual &&
+		test_grep "æuthœr" actual
 	)
 '
 
@@ -156,8 +156,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "hÅs some cp850 text" actual &&
-		grep "Åuthor" actual
+		test_grep "hÅs some cp850 text" actual &&
+		test_grep "Åuthor" actual
 	)
 '
 
@@ -168,8 +168,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "h%8Fs some cp850 text" actual &&
-		grep "%8Futhor" actual
+		test_grep "h%8Fs some cp850 text" actual &&
+		test_grep "%8Futhor" actual
 	)
 '
 
@@ -191,8 +191,8 @@
 		git p4-python2 sync --branch=master &&
 
 		git log p4/master >actual &&
-		grep "sœme more cp-1252 tæxt" actual &&
-		grep "æuthœr" actual
+		test_grep "sœme more cp-1252 tæxt" actual &&
+		test_grep "æuthœr" actual
 	)
 '
 
@@ -208,7 +208,7 @@
 		cd "$git" &&
 		git log >actual &&
 		badly_encoded_in_git=$(echo "some latin-1 tæxt" | iconv -f utf8 -t latin1) &&
-		grep "$badly_encoded_in_git" actual
+		test_grep "$badly_encoded_in_git" actual
 	)
 '
 
diff --git a/t/t9836-git-p4-metadata-encoding-python3.sh b/t/t9836-git-p4-metadata-encoding-python3.sh
index da6669b..1611849 100755
--- a/t/t9836-git-p4-metadata-encoding-python3.sh
+++ b/t/t9836-git-p4-metadata-encoding-python3.sh
@@ -84,7 +84,7 @@
 	test_when_finished cleanup_git &&
 	test_when_finished remove_user_cache &&
 	test_must_fail git -c git-p4.metadataDecodingStrategy=strict p4-python3 clone --dest="$git" //depot@all 2>err &&
-	grep "Decoding perforce metadata failed!" err
+	test_grep "Decoding perforce metadata failed!" err
 '
 
 test_expect_success 'check utf-8 contents with passthrough strategy' '
@@ -94,8 +94,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "some utf-8 tǣxt" actual &&
-		grep "ǣuthor" actual
+		test_grep "some utf-8 tǣxt" actual &&
+		test_grep "ǣuthor" actual
 	)
 '
 
@@ -107,9 +107,9 @@
 		cd "$git" &&
 		git log >actual &&
 		badly_encoded_in_git=$(echo "some latin-1 tæxt" | iconv -f utf8 -t latin1) &&
-		grep "$badly_encoded_in_git" actual &&
+		test_grep "$badly_encoded_in_git" actual &&
 		bad_author_in_git="$(echo æuthor | iconv -f utf8 -t latin1)" &&
-		grep "$bad_author_in_git" actual
+		test_grep "$bad_author_in_git" actual
 	)
 '
 
@@ -120,8 +120,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "some utf-8 tǣxt" actual &&
-		grep "ǣuthor" actual
+		test_grep "some utf-8 tǣxt" actual &&
+		test_grep "ǣuthor" actual
 	)
 '
 
@@ -132,8 +132,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "some latin-1 tæxt" actual &&
-		grep "æuthor" actual
+		test_grep "some latin-1 tæxt" actual &&
+		test_grep "æuthor" actual
 	)
 '
 
@@ -144,8 +144,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "sœme cp-1252 tæxt" actual &&
-		grep "æuthœr" actual
+		test_grep "sœme cp-1252 tæxt" actual &&
+		test_grep "æuthœr" actual
 	)
 '
 
@@ -156,8 +156,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "hÅs some cp850 text" actual &&
-		grep "Åuthor" actual
+		test_grep "hÅs some cp850 text" actual &&
+		test_grep "Åuthor" actual
 	)
 '
 
@@ -168,8 +168,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "h%8Fs some cp850 text" actual &&
-		grep "%8Futhor" actual
+		test_grep "h%8Fs some cp850 text" actual &&
+		test_grep "%8Futhor" actual
 	)
 '
 
@@ -191,8 +191,8 @@
 		git p4-python3 sync --branch=master &&
 
 		git log p4/master >actual &&
-		grep "sœme more cp-1252 tæxt" actual &&
-		grep "æuthœr" actual
+		test_grep "sœme more cp-1252 tæxt" actual &&
+		test_grep "æuthœr" actual
 	)
 '
 
@@ -208,8 +208,8 @@
 	(
 		cd "$git" &&
 		git log >actual &&
-		grep "sœme cp-1252 tæxt" actual &&
-		grep "æuthœr" actual
+		test_grep "sœme cp-1252 tæxt" actual &&
+		test_grep "æuthœr" actual
 	)
 '
 
diff --git a/t/t9850-shell.sh b/t/t9850-shell.sh
index 21c3af4..27cc49b 100755
--- a/t/t9850-shell.sh
+++ b/t/t9850-shell.sh
@@ -32,7 +32,7 @@
 test_expect_success 'shell complains of overlong commands' '
 	test-tool genzeros | tr "\000" "a" |
 	test_must_fail git shell 2>err &&
-	grep "too long" err
+	test_grep "too long" err
 '
 
 test_done
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 55dc9ea..9ae3c48 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -1381,7 +1381,7 @@
 	test_when_finished "git worktree remove other_wt" &&
 	git worktree add --orphan other_wt &&
 	run_completion "git worktree remove " &&
-	grep other_wt out
+	test_grep other_wt out
 '
 
 test_expect_success '__git_complete_worktree_paths - not a git repository' '
@@ -1397,7 +1397,7 @@
 	test_when_finished "git -C otherrepo worktree remove otherrepo_wt" &&
 	git -C otherrepo worktree add --orphan otherrepo_wt &&
 	run_completion "git -C otherrepo worktree remove " &&
-	grep otherrepo_wt out
+	test_grep otherrepo_wt out
 '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
@@ -2554,14 +2554,14 @@
 test_expect_success 'basic' '
 	run_completion "git " &&
 	# built-in
-	grep -q "^add \$" out &&
+	test_grep -q "^add \$" out &&
 	# script
-	grep -q "^rebase \$" out &&
+	test_grep -q "^rebase \$" out &&
 	# plumbing
-	! grep -q "^ls-files \$" out &&
+	test_grep ! -q "^ls-files \$" out &&
 
 	run_completion "git r" &&
-	! grep -q -v "^r" out
+	test_grep ! -q -v "^r" out
 '
 
 test_expect_success 'double dash "git" itself' '
@@ -2656,7 +2656,7 @@
 test_expect_success 'completion.commands removes multiple commands' '
 	test_config completion.commands "-cherry -mergetool" &&
 	git --list-cmds=list-mainporcelain,list-complete,config >out &&
-	! grep -E "^(cherry|mergetool)$" out
+	test_grep ! -E "^(cherry|mergetool)$" out
 '
 
 test_expect_success 'setup for integration tests' '
@@ -3179,8 +3179,8 @@
 
 		# Just mainporcelain, not plumbing commands
 		run_completion "git c" &&
-		grep checkout out &&
-		! grep cat-file out
+		test_grep checkout out &&
+		test_grep ! cat-file out
 	)
 '
 
@@ -3193,13 +3193,13 @@
 
 		# Both mainporcelain and plumbing commands
 		run_completion "git c" &&
-		grep checkout out &&
-		grep cat-file out &&
+		test_grep checkout out &&
+		test_grep cat-file out &&
 
 		# Check "gitk", a "main" command, but not a built-in + more plumbing
 		run_completion "git g" &&
-		grep gitk out &&
-		grep get-tar-commit-id out
+		test_grep gitk out &&
+		test_grep get-tar-commit-id out
 	)
 '
 
diff --git a/t/test-lib.sh b/t/test-lib.sh
index ceefb99..1f0505e 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -210,7 +210,7 @@
 	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
 		immediate=t ;;
 	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
-		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
+		GIT_TEST_LONG=true; export GIT_TEST_LONG ;;
 	-r)
 		mark_option_requires_arg "$opt" run_list
 		;;
@@ -1217,14 +1217,14 @@
 	then
 		return
 	fi &&
-	say_color error "$(cat "$TEST_RESULTS_SAN_FILE".*)" &&
+	say_color >&4 error "$(cat "$TEST_RESULTS_SAN_FILE".*)" &&
 
 	if test "$test_failure" = 0
 	then
-		say "Our logs revealed a memory leak, exit non-zero!" &&
+		say >&4 "Our logs revealed a memory leak, exit non-zero!" &&
 		invert_exit_code=t
 	else
-		say "Our logs revealed a memory leak..."
+		say >&4 "Our logs revealed a memory leak..."
 	fi
 }
 
@@ -1849,7 +1849,7 @@
 '
 
 test_lazy_prereq EXPENSIVE '
-	test -n "$GIT_TEST_LONG"
+	test_bool_env GIT_TEST_LONG false
 '
 
 test_lazy_prereq EXPENSIVE_ON_WINDOWS '
diff --git a/t/unit-tests/u-hash.c b/t/unit-tests/u-hash.c
index bd4ac6a..19f4efd 100644
--- a/t/unit-tests/u-hash.c
+++ b/t/unit-tests/u-hash.c
@@ -12,7 +12,7 @@ static void check_hash_data(const void *data, size_t data_length,
 		unsigned char hash[GIT_MAX_HEXSZ];
 		const struct git_hash_algo *algop = &hash_algos[i];
 
-		algop->init_fn(&ctx);
+		git_hash_init(&ctx, algop);
 		git_hash_update(&ctx, data, data_length);
 		git_hash_final(hash, &ctx);
 
diff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c
index 6844bfc..ddf2db5 100644
--- a/t/unit-tests/u-odb-inmemory.c
+++ b/t/unit-tests/u-odb-inmemory.c
@@ -1,5 +1,6 @@
 #include "unit-test.h"
 #include "hex.h"
+#include "object-file.h"
 #include "odb/source-inmemory.h"
 #include "odb/streaming.h"
 #include "oidset.h"
@@ -36,6 +37,17 @@ static void cl_assert_object_info(struct odb_source_inmemory *source,
 	free(actual_content);
 }
 
+static void cl_assert_write_object(struct odb_source_inmemory *source,
+				   const char *content,
+				   enum object_type type,
+				   struct object_id *oid)
+{
+	size_t content_len = strlen(content);
+	hash_object_file(repo.hash_algo, content, content_len, type, oid);
+	cl_must_pass(odb_source_write_object(&source->base, content, content_len,
+					     type, oid, NULL, NULL, 0));
+}
+
 void test_odb_inmemory__initialize(void)
 {
 	odb = odb_new(&repo, "", "");
@@ -78,8 +90,7 @@ void test_odb_inmemory__read_written_object(void)
 	const char data[] = "foobar";
 	struct object_id written_oid;
 
-	cl_must_pass(odb_source_write_object(&source->base, data, strlen(data),
-					     OBJ_BLOB, &written_oid, NULL, 0));
+	cl_assert_write_object(source, data, OBJ_BLOB, &written_oid);
 	cl_assert_equal_s(oid_to_hex(&written_oid), FOOBAR_OID);
 	cl_assert_object_info(source, &written_oid, OBJ_BLOB, "foobar");
 
@@ -94,8 +105,7 @@ void test_odb_inmemory__read_stream_object(void)
 	const char data[] = "foobar";
 	char buf[3] = { 0 };
 
-	cl_must_pass(odb_source_write_object(&source->base, data, strlen(data),
-					     OBJ_BLOB, &written_oid, NULL, 0));
+	cl_assert_write_object(source, data, OBJ_BLOB, &written_oid);
 
 	cl_must_pass(odb_source_read_object_stream(&stream, &source->base,
 						   &written_oid));
@@ -141,8 +151,7 @@ void test_odb_inmemory__for_each_object(void)
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "%d", i);
 
-		cl_must_pass(odb_source_write_object(&source->base, buf.buf, buf.len,
-						     OBJ_BLOB, &written_oid, NULL, 0));
+		cl_assert_write_object(source, buf.buf, OBJ_BLOB, &written_oid);
 		cl_must_pass(oidset_insert(&expected_oids, &written_oid));
 	}
 
@@ -174,12 +183,9 @@ void test_odb_inmemory__for_each_object_can_abort_iteration(void)
 	struct object_id written_oid;
 	unsigned counter = 0;
 
-	cl_must_pass(odb_source_write_object(&source->base, "1", 1,
-					     OBJ_BLOB, &written_oid, NULL, 0));
-	cl_must_pass(odb_source_write_object(&source->base, "2", 1,
-					     OBJ_BLOB, &written_oid, NULL, 0));
-	cl_must_pass(odb_source_write_object(&source->base, "3", 1,
-					     OBJ_BLOB, &written_oid, NULL, 0));
+	cl_assert_write_object(source, "1", OBJ_BLOB, &written_oid);
+	cl_assert_write_object(source, "2", OBJ_BLOB, &written_oid);
+	cl_assert_write_object(source, "3", OBJ_BLOB, &written_oid);
 
 	cl_assert_equal_i(odb_source_for_each_object(&source->base, NULL,
 						     abort_after_two_objects,
@@ -199,12 +205,9 @@ void test_odb_inmemory__count_objects(void)
 	cl_must_pass(odb_source_count_objects(&source->base, 0, &count));
 	cl_assert_equal_u(count, 0);
 
-	cl_must_pass(odb_source_write_object(&source->base, "1", 1,
-					     OBJ_BLOB, &written_oid, NULL, 0));
-	cl_must_pass(odb_source_write_object(&source->base, "2", 1,
-					     OBJ_BLOB, &written_oid, NULL, 0));
-	cl_must_pass(odb_source_write_object(&source->base, "3", 1,
-					     OBJ_BLOB, &written_oid, NULL, 0));
+	cl_assert_write_object(source, "1", OBJ_BLOB, &written_oid);
+	cl_assert_write_object(source, "2", OBJ_BLOB, &written_oid);
+	cl_assert_write_object(source, "3", OBJ_BLOB, &written_oid);
 
 	cl_must_pass(odb_source_count_objects(&source->base, 0, &count));
 	cl_assert_equal_u(count, 3);
@@ -228,8 +231,7 @@ void test_odb_inmemory__find_abbrev_len(void)
 	 *
 	 * With only one blob written we expect a length of 4.
 	 */
-	cl_must_pass(odb_source_write_object(&source->base, "368317", strlen("368317"),
-					     OBJ_BLOB, &oid1, NULL, 0));
+	cl_assert_write_object(source, "368317", OBJ_BLOB, &oid1);
 	cl_must_pass(odb_source_find_abbrev_len(&source->base, &oid1, 4,
 						&abbrev_len));
 	cl_assert_equal_u(abbrev_len, 4);
@@ -238,8 +240,7 @@ void test_odb_inmemory__find_abbrev_len(void)
 	 * With both objects present, the shared 10-character prefix means we
 	 * need at least 11 characters to uniquely identify either object.
 	 */
-	cl_must_pass(odb_source_write_object(&source->base, "514796", strlen("514796"),
-					     OBJ_BLOB, &oid2, NULL, 0));
+	cl_assert_write_object(source, "514796", OBJ_BLOB, &oid2);
 	cl_must_pass(odb_source_find_abbrev_len(&source->base, &oid1, 4,
 						&abbrev_len));
 	cl_assert_equal_u(abbrev_len, 11);
@@ -255,13 +256,11 @@ void test_odb_inmemory__freshen_object(void)
 	const char *end;
 
 	cl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo));
-	cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0);
+	cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid, NULL), 0);
 
-	cl_must_pass(odb_source_write_object(&source->base, "foobar",
-					     strlen("foobar"), OBJ_BLOB,
-					     &written_oid, NULL, 0));
+	cl_assert_write_object(source, "foobar", OBJ_BLOB, &written_oid);
 	cl_assert_equal_i(odb_source_freshen_object(&source->base,
-						    &written_oid), 1);
+						    &written_oid, NULL), 1);
 
 	odb_source_free(&source->base);
 }
diff --git a/t/unit-tests/u-prio-queue.c b/t/unit-tests/u-prio-queue.c
index 63e5811..af3e0b8 100644
--- a/t/unit-tests/u-prio-queue.c
+++ b/t/unit-tests/u-prio-queue.c
@@ -53,13 +53,13 @@ static void test_prio_queue(int *input, size_t input_size,
 			prio_queue_reverse(&pq);
 			break;
 		case REPLACE:
-			peek = prio_queue_peek(&pq);
+			get = prio_queue_get(&pq);
 			cl_assert(i + 1 < input_size);
 			cl_assert(input[i + 1] >= 0);
 			cl_assert(j < result_size);
-			cl_assert_equal_i(result[j], show(peek));
+			cl_assert_equal_i(result[j], show(get));
 			j++;
-			prio_queue_replace(&pq, &input[++i]);
+			prio_queue_put(&pq, &input[++i]);
 			break;
 		default:
 			prio_queue_put(&pq, &input[i]);
diff --git a/t/unit-tests/u-reftable-basics.c b/t/unit-tests/u-reftable-basics.c
index 73566ed..c5d83b6 100644
--- a/t/unit-tests/u-reftable-basics.c
+++ b/t/unit-tests/u-reftable-basics.c
@@ -60,6 +60,17 @@ void test_reftable_basics__binsearch(void)
 	}
 }
 
+static int unreachable_lesseq(size_t i UNUSED, void *args UNUSED)
+{
+	cl_fail("comparison function called for empty range");
+	return 0;
+}
+
+void test_reftable_basics__binsearch_empty(void)
+{
+	cl_assert_equal_i(binsearch(0, &unreachable_lesseq, NULL), 0);
+}
+
 void test_reftable_basics__names_length(void)
 {
 	const char *a[] = { "a", "b", NULL };
diff --git a/t/unit-tests/u-reftable-block.c b/t/unit-tests/u-reftable-block.c
index f4bded7..99b155d 100644
--- a/t/unit-tests/u-reftable-block.c
+++ b/t/unit-tests/u-reftable-block.c
@@ -14,6 +14,31 @@ license that can be found in the LICENSE file or at
 #include "reftable/reftable-error.h"
 #include "strbuf.h"
 
+static int cl_reftable_write_block(struct reftable_buf *buf,
+				   uint8_t block_type,
+				   struct reftable_record *recs,
+				   size_t nrecs)
+{
+	struct block_writer writer = {
+		.last_key = REFTABLE_BUF_INIT,
+	};
+	uint8_t block[1024];
+	int block_end;
+
+	cl_must_pass(block_writer_init(&writer, block_type, block, 1024,
+				       0, hash_size(REFTABLE_HASH_SHA1)));
+	for (size_t i = 0; i < nrecs; i++)
+		cl_must_pass(block_writer_add(&writer, &recs[i]));
+
+	block_end = block_writer_finish(&writer);
+	cl_assert(block_end > 0);
+
+	cl_must_pass(reftable_buf_add(buf, block, block_end));
+
+	block_writer_release(&writer);
+	return block_end;
+}
+
 void test_reftable_block__read_write(void)
 {
 	const int header_off = 21; /* random */
@@ -381,25 +406,13 @@ void test_reftable_block__ref_read_write(void)
 void test_reftable_block__iterator(void)
 {
 	struct reftable_block_source source = { 0 };
-	struct block_writer writer = {
-		.last_key = REFTABLE_BUF_INIT,
-	};
 	struct reftable_record expected_refs[20];
 	struct reftable_ref_record ref = { 0 };
 	struct reftable_iterator it = { 0 };
 	struct reftable_block block = { 0 };
-	struct reftable_buf data;
+	struct reftable_buf data = REFTABLE_BUF_INIT;
 	int err;
 
-	data.len = 1024;
-	REFTABLE_CALLOC_ARRAY(data.buf, data.len);
-	cl_assert(data.buf != NULL);
-
-	err = block_writer_init(&writer, REFTABLE_BLOCK_TYPE_REF,
-				(uint8_t *) data.buf, data.len,
-				0, hash_size(REFTABLE_HASH_SHA1));
-	cl_assert(!err);
-
 	for (size_t i = 0; i < ARRAY_SIZE(expected_refs); i++) {
 		expected_refs[i] = (struct reftable_record) {
 			.type = REFTABLE_BLOCK_TYPE_REF,
@@ -409,13 +422,10 @@ void test_reftable_block__iterator(void)
 			},
 		};
 		memset(expected_refs[i].u.ref.value.val1, i, REFTABLE_HASH_SIZE_SHA1);
-
-		err = block_writer_add(&writer, &expected_refs[i]);
-		cl_assert_equal_i(err, 0);
 	}
 
-	err = block_writer_finish(&writer);
-	cl_assert(err > 0);
+	cl_reftable_write_block(&data, REFTABLE_BLOCK_TYPE_REF,
+				expected_refs, ARRAY_SIZE(expected_refs));
 
 	block_source_from_buf(&source, &data);
 	reftable_block_init(&block, &source, 0, 0, data.len,
@@ -453,6 +463,147 @@ void test_reftable_block__iterator(void)
 	reftable_ref_record_release(&ref);
 	reftable_iterator_destroy(&it);
 	reftable_block_release(&block);
-	block_writer_release(&writer);
+	reftable_buf_release(&data);
+}
+
+void test_reftable_block__corrupt_log_block_size(void)
+{
+	struct reftable_block_source source = { 0 };
+	struct reftable_record rec = {
+		.type = REFTABLE_BLOCK_TYPE_LOG,
+		.u.log = {
+			.refname = (char *) "refs/heads/main",
+			.update_index = 1,
+			.value_type = REFTABLE_LOG_UPDATE,
+		},
+	};
+	struct reftable_block block = { 0 };
+	struct reftable_buf data = REFTABLE_BUF_INIT;
+
+	cl_reftable_write_block(&data, REFTABLE_BLOCK_TYPE_LOG, &rec, 1);
+
+	/*
+	 * Log blocks store their inflated size as a big-endian 24-bit integer
+	 * right after the one-byte block type. Rewrite it to claim a size that
+	 * is smaller than the block header.
+	 */
+	reftable_put_be24((uint8_t *) data.buf + 1, 1);
+
+	block_source_from_buf(&source, &data);
+	cl_assert_equal_i(reftable_block_init(&block, &source, 0, 0, data.len,
+					      REFTABLE_HASH_SIZE_SHA1, REFTABLE_BLOCK_TYPE_LOG),
+			  REFTABLE_FORMAT_ERROR);
+
+	reftable_block_release(&block);
+	reftable_buf_release(&data);
+}
+
+void test_reftable_block__corrupt_block_size(void)
+{
+	struct reftable_block_source source = { 0 };
+	struct reftable_record rec = {
+		.type = REFTABLE_BLOCK_TYPE_REF,
+		.u.ref = {
+			.value_type = REFTABLE_REF_VAL1,
+			.refname = (char *) "refs/heads/main",
+		},
+	};
+	struct reftable_block block = { 0 };
+	struct reftable_buf data = REFTABLE_BUF_INIT;
+	uint32_t block_size;
+	unsigned char *p;
+
+	cl_reftable_write_block(&data, REFTABLE_BLOCK_TYPE_REF, &rec, 1);
+
+	/*
+	 * The block size is stored as a big-endian 24-bit integer right after
+	 * the one-byte block type at the start of the block. Corrupt it to
+	 * claim a size that is larger than the data we actually have. Reading
+	 * the restart count and restart table relative to such a bogus block
+	 * size must not access out-of-bounds memory.
+	 */
+	p = (unsigned char *) data.buf + 1;
+	block_size = reftable_get_be24(p);
+	cl_assert_equal_i(block_size, 47);
+	reftable_put_be24(p, block_size + 1);
+
+	block_source_from_buf(&source, &data);
+	cl_assert_equal_i(reftable_block_init(&block, &source, 0, 0, data.len,
+					      REFTABLE_HASH_SIZE_SHA1, REFTABLE_BLOCK_TYPE_REF),
+			  REFTABLE_FORMAT_ERROR);
+
+	reftable_block_release(&block);
+	reftable_buf_release(&data);
+}
+
+void test_reftable_block__corrupt_restart_count(void)
+{
+	struct reftable_block_source source = { 0 };
+	struct reftable_record rec = {
+		.type = REFTABLE_BLOCK_TYPE_REF,
+		.u.ref = {
+			.value_type = REFTABLE_REF_VAL1,
+			.refname = (char *) "refs/heads/main",
+		},
+	};
+	struct reftable_block block = { 0 };
+	struct reftable_buf data = REFTABLE_BUF_INIT;
+	int block_size;
+
+	block_size = cl_reftable_write_block(&data, REFTABLE_BLOCK_TYPE_REF, &rec, 1);
+
+	/*
+	 * Corrupt the restart count to claim a bogus number of restart points.
+	 * Note that this would only cause us to perform an out-of-bounds
+	 * access when seeking into the block, but we want to refuse such a
+	 * block outright.
+	 */
+	reftable_put_be16((uint8_t *) data.buf + block_size - 2, 0xffff);
+
+	block_source_from_buf(&source, &data);
+	cl_assert_equal_i(reftable_block_init(&block, &source, 0, 0, data.len,
+					      REFTABLE_HASH_SIZE_SHA1, REFTABLE_BLOCK_TYPE_REF),
+			  REFTABLE_FORMAT_ERROR);
+
+	reftable_block_release(&block);
+	reftable_buf_release(&data);
+}
+
+void test_reftable_block__corrupt_restart_offset(void)
+{
+	struct reftable_block_source source = { 0 };
+	struct reftable_record rec = {
+		.type = REFTABLE_BLOCK_TYPE_REF,
+		.u.ref = {
+			.value_type = REFTABLE_REF_VAL1,
+			.refname = (char *) "refs/heads/main",
+		},
+	};
+	struct reftable_block block = { 0 };
+	struct block_iter it = BLOCK_ITER_INIT;
+	struct reftable_buf want = REFTABLE_BUF_INIT;
+	struct reftable_buf data = REFTABLE_BUF_INIT;
+
+	cl_reftable_write_block(&data, REFTABLE_BLOCK_TYPE_REF, &rec, 1);
+
+	block_source_from_buf(&source, &data);
+	cl_must_pass(reftable_block_init(&block, &source, 0, 0, data.len,
+					 REFTABLE_HASH_SIZE_SHA1, REFTABLE_BLOCK_TYPE_REF));
+
+	/*
+	 * Corrupt the first restart offset, stored as a big-endian 24-bit
+	 * integer at the start of the restart table, to point past the end of
+	 * the records section. Seeking such a block must fail gracefully.
+	 */
+	reftable_put_be24((uint8_t *) block.block_data.data + block.restart_off,
+			  0xffffff);
+
+	block_iter_init(&it, &block);
+	cl_must_pass(reftable_buf_addstr(&want, "refs/heads/main"));
+	cl_assert_equal_i(block_iter_seek_key(&it, &want), REFTABLE_FORMAT_ERROR);
+
+	reftable_buf_release(&want);
+	block_iter_close(&it);
+	reftable_block_release(&block);
 	reftable_buf_release(&data);
 }
diff --git a/t/unit-tests/u-reftable-record.c b/t/unit-tests/u-reftable-record.c
index 1bf2e17..9c95083 100644
--- a/t/unit-tests/u-reftable-record.c
+++ b/t/unit-tests/u-reftable-record.c
@@ -11,6 +11,7 @@
 #include "reftable/basics.h"
 #include "reftable/constants.h"
 #include "reftable/record.h"
+#include "reftable/reftable-error.h"
 
 static void t_copy(struct reftable_record *rec)
 {
@@ -202,6 +203,29 @@ void test_reftable_record__ref_record_roundtrip(void)
 	reftable_buf_release(&scratch);
 }
 
+void test_reftable_record__ref_record_decode_invalid_value_type(void)
+{
+	struct reftable_buf scratch = REFTABLE_BUF_INIT;
+	struct reftable_record out = {
+		.type = REFTABLE_BLOCK_TYPE_REF,
+	};
+	struct reftable_buf key = REFTABLE_BUF_INIT;
+	uint8_t buffer[1024] = { 0 };
+	struct string_view dest = {
+		.buf = buffer,
+		.len = sizeof(buffer),
+	};
+
+	cl_must_pass(reftable_buf_addstr(&key, "refs/heads/master"));
+	cl_assert_equal_i(reftable_record_decode(&out, key, REFTABLE_NR_REF_VALUETYPES,
+						 dest, REFTABLE_HASH_SIZE_SHA1, &scratch),
+			  REFTABLE_FORMAT_ERROR);
+
+	reftable_record_release(&out);
+	reftable_buf_release(&key);
+	reftable_buf_release(&scratch);
+}
+
 void test_reftable_record__log_record_comparison(void)
 {
 	struct reftable_record in[3] = {
diff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c
index fae478e..d4251fa 100644
--- a/t/unit-tests/u-reftable-table.c
+++ b/t/unit-tests/u-reftable-table.c
@@ -1,8 +1,11 @@
 #include "unit-test.h"
 #include "lib-reftable.h"
+#include "reftable/basics.h"
+#include "reftable/block.h"
 #include "reftable/blocksource.h"
 #include "reftable/constants.h"
 #include "reftable/iter.h"
+#include "reftable/reftable-error.h"
 #include "reftable/table.h"
 #include "strbuf.h"
 
@@ -201,3 +204,92 @@ void test_reftable_table__block_iterator(void)
 	reftable_buf_release(&buf);
 	reftable_free(records);
 }
+
+void test_reftable_table__seek_invalid_log_offset(void)
+{
+	struct reftable_ref_record refs[] = {
+		{
+			.refname = (char *) "refs/heads/main",
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 42 },
+		},
+	};
+	struct reftable_log_record logs[] = {
+		{
+			.refname = (char *) "refs/heads/main",
+			.update_index = 1,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value.update = {
+				.name = (char *) "user",
+				.email = (char *) "user@example.com",
+				.message = (char *) "message\n",
+			},
+		},
+	};
+	struct reftable_block_source source = { 0 };
+	struct reftable_log_record log = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_table *table;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	size_t fsize = footer_size(1);
+	uint8_t *footer;
+
+	cl_reftable_write_to_buf(&buf, refs, ARRAY_SIZE(refs),
+				 logs, ARRAY_SIZE(logs), REFTABLE_HASH_SHA1, NULL);
+
+	/*
+	 * Corrupt the log section offset stored in the footer so that it points
+	 * past the end of the table. The footer is checksummed, so we also have
+	 * to recompute and rewrite the CRC.
+	 */
+	footer = (uint8_t *) buf.buf + buf.len - fsize;
+	reftable_put_be64(footer + header_size(1) + 24, UINT64_MAX);
+	reftable_put_be32(footer + fsize - 4, crc32(0, footer, fsize - 4));
+
+	block_source_from_buf(&source, &buf);
+	cl_must_pass(reftable_table_new(&table, &source, "name"));
+
+	/*
+	 * Seeking the log iterator must not crash even though the log section
+	 * offset is bogus. As the offset points past the end of the table we
+	 * know that the table is corrupt, so the seek must report a format
+	 * error instead of pretending that the section is empty.
+	 */
+	reftable_table_init_log_iterator(table, &it);
+	cl_assert_equal_i(reftable_iterator_seek_log(&it, ""),
+			  REFTABLE_FORMAT_ERROR);
+
+	reftable_log_record_release(&log);
+	reftable_iterator_destroy(&it);
+	reftable_table_decref(table);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_table__new_with_truncated_table(void)
+{
+	struct reftable_ref_record refs[] = {
+		{
+			.refname = (char *) "refs/heads/main",
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 42 },
+		},
+	};
+	struct reftable_block_source source = { 0 };
+	struct reftable_table *table;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+
+	cl_reftable_write_to_buf(&buf, refs, ARRAY_SIZE(refs), NULL, 0,
+				 REFTABLE_HASH_SHA1, NULL);
+
+	/*
+	 * Truncate the table so that it is large enough to read the header, but
+	 * too small to also contain the footer.
+	 */
+	buf.len = footer_size(1) - 1;
+	block_source_from_buf(&source, &buf);
+
+	cl_assert_equal_i(reftable_table_new(&table, &source, "name"),
+			  REFTABLE_FORMAT_ERROR);
+
+	reftable_buf_release(&buf);
+}
diff --git a/tempfile.c b/tempfile.c
index f0fdf58..dc9ca4e 100644
--- a/tempfile.c
+++ b/tempfile.c
@@ -42,8 +42,6 @@
  * file created by its parent.
  */
 
-#define USE_THE_REPOSITORY_VARIABLE
-
 #include "git-compat-util.h"
 #include "abspath.h"
 #include "path.h"
@@ -134,7 +132,8 @@ static void deactivate_tempfile(struct tempfile *tempfile)
 }
 
 /* Make sure errno contains a meaningful value on error */
-struct tempfile *create_tempfile_mode(const char *path, int mode)
+struct tempfile *repo_create_tempfile_mode(struct repository *r,
+					   const char *path, int mode)
 {
 	struct tempfile *tempfile = new_tempfile();
 
@@ -150,7 +149,7 @@ struct tempfile *create_tempfile_mode(const char *path, int mode)
 		return NULL;
 	}
 	activate_tempfile(tempfile);
-	if (adjust_shared_perm(the_repository, tempfile->filename.buf)) {
+	if (adjust_shared_perm(r, tempfile->filename.buf)) {
 		int save_errno = errno;
 		error("cannot fix permission bits on %s", tempfile->filename.buf);
 		delete_tempfile(&tempfile);
diff --git a/tempfile.h b/tempfile.h
index 2227a09..f571f3c 100644
--- a/tempfile.h
+++ b/tempfile.h
@@ -4,6 +4,8 @@
 #include "list.h"
 #include "strbuf.h"
 
+struct repository;
+
 /*
  * Handle temporary files.
  *
@@ -92,11 +94,13 @@ struct tempfile {
  * `core.sharedRepository`, so it is not guaranteed to have the given
  * mode.
  */
-struct tempfile *create_tempfile_mode(const char *path, int mode);
+struct tempfile *repo_create_tempfile_mode(struct repository *r,
+					   const char *path, int mode);
 
-static inline struct tempfile *create_tempfile(const char *path)
+static inline struct tempfile *repo_create_tempfile(struct repository *r,
+						    const char *path)
 {
-	return create_tempfile_mode(path, 0666);
+	return repo_create_tempfile_mode(r, path, 0666);
 }
 
 /*
diff --git a/tools/coccinelle/hash.cocci b/tools/coccinelle/hash.cocci
new file mode 100644
index 0000000..d0e2e5f
--- /dev/null
+++ b/tools/coccinelle/hash.cocci
@@ -0,0 +1,63 @@
+@@
+identifier f != git_hash_init;
+expression ALGO;
+struct git_hash_ctx *CTX;
+@@
+  f(...) {<...
+- ALGO->init_fn(CTX);
++ git_hash_init(CTX, ALGO);
+  ...>}
+
+@@
+identifier f != git_hash_clone;
+expression ALGO;
+struct git_hash_ctx *SRC;
+struct git_hash_ctx *DST;
+@@
+  f(...) {<...
+- ALGO->clone_fn(DST, SRC);
++ git_hash_clone(DST, SRC);
+  ...>}
+
+@@
+identifier f != git_hash_update;
+expression ALGO;
+struct git_hash_ctx *CTX;
+expression list ARGS;
+@@
+  f(...) {<...
+- ALGO->update_fn(CTX, ARGS);
++ git_hash_update(CTX, ARGS);
+  ...>}
+
+@@
+identifier f != git_hash_final;
+expression ALGO;
+struct git_hash_ctx *CTX;
+expression list ARGS;
+@@
+  f(...) {<...
+- ALGO->final_fn(ARGS, CTX);
++ git_hash_final(ARGS, CTX);
+  ...>}
+
+@@
+identifier f != git_hash_final_oid;
+expression ALGO;
+struct git_hash_ctx *CTX;
+expression list ARGS;
+@@
+  f(...) {<...
+- ALGO->final_oid_fn(ARGS, CTX);
++ git_hash_final_oid(ARGS, CTX);
+  ...>}
+
+@@
+identifier f != git_hash_discard;
+expression ALGO;
+struct git_hash_ctx *CTX;
+@@
+  f(...) {<...
+- ALGO->discard_fn(CTX);
++ git_hash_discard(CTX);
+  ...>}
diff --git a/trace.c b/trace.c
index 9b99460..515b99e 100644
--- a/trace.c
+++ b/trace.c
@@ -299,7 +299,7 @@ static const char *quote_crnl(const char *path)
 
 void trace_repo_setup(struct repository *r)
 {
-	const char *git_work_tree, *prefix = startup_info->prefix;
+	const char *git_work_tree, *prefix = r->prefix;
 	char *cwd;
 
 	if (!trace_want(&trace_setup_key))
@@ -310,7 +310,7 @@ void trace_repo_setup(struct repository *r)
 	if (!(git_work_tree = repo_get_work_tree(r)))
 		git_work_tree = "(null)";
 
-	if (!startup_info->prefix)
+	if (!r->prefix)
 		prefix = "(null)";
 
 	trace_printf_key(&trace_setup_key, "setup: git_dir: %s\n", quote_crnl(repo_get_git_dir(r)));
diff --git a/trace2/tr2_sid.c b/trace2/tr2_sid.c
index 1c1d27b..131b4f5 100644
--- a/trace2/tr2_sid.c
+++ b/trace2/tr2_sid.c
@@ -45,7 +45,7 @@ static void tr2_sid_append_my_sid_component(void)
 	if (xgethostname(hostname, sizeof(hostname)))
 		strbuf_add(&tr2sid_buf, "Localhost", 9);
 	else {
-		algo->init_fn(&ctx);
+		git_hash_init(&ctx, algo);
 		git_hash_update(&ctx, hostname, strlen(hostname));
 		git_hash_final(hash, &ctx);
 		hash_to_hex_algop_r(hex, hash, algo);
diff --git a/transport-helper.c b/transport-helper.c
index 80f90eb..b109fbd 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -162,7 +162,7 @@ static struct child_process *get_helper(struct transport *transport)
 
 	data->helper = helper;
 	data->no_disconnect_req = 0;
-	refspec_init_fetch(&data->rs);
+	refspec_init_fetch(&data->rs, the_hash_algo);
 
 	/*
 	 * Open the output as FILE* so strbuf_getline_*() family of
@@ -266,9 +266,9 @@ static int disconnect_helper(struct transport *transport)
 		close(data->helper->out);
 		fclose(data->out);
 		res = finish_command(data->helper);
-		FREE_AND_NULL(data->name);
 		FREE_AND_NULL(data->helper);
 	}
+	FREE_AND_NULL(data->name);
 	return res;
 }
 
@@ -784,6 +784,15 @@ static int fetch_refs(struct transport *transport,
 	return -1;
 }
 
+static int fetch_object_info_helper(struct transport *transport)
+{
+	get_helper(transport);
+	if (process_connect(transport, 0))
+		return transport->vtable->fetch_object_info(transport);
+
+	die(_("object-info requires protocol v2"));
+}
+
 struct push_update_ref_state {
 	struct ref *hint;
 	struct ref_push_report *report;
@@ -1330,6 +1339,7 @@ static struct transport_vtable vtable = {
 	.get_refs_list	= get_refs_list,
 	.get_bundle_uri = get_bundle_uri,
 	.fetch_refs	= fetch_refs,
+	.fetch_object_info = fetch_object_info_helper,
 	.push_refs	= push_refs,
 	.connect	= connect_helper,
 	.disconnect	= release_helper
diff --git a/transport-internal.h b/transport-internal.h
index 051f3ab..60db0be 100644
--- a/transport-internal.h
+++ b/transport-internal.h
@@ -45,6 +45,14 @@ struct transport_vtable {
 	 **/
 	int (*fetch_refs)(struct transport *transport, int refs_nr, struct ref **refs);
 
+	/*
+	 * Fetch object info (only size currently) from remote without
+	 * downloading the objects.
+	 *
+	 * Uses object-info capability of v2 protocol.
+	 */
+	int (*fetch_object_info)(struct transport *transport);
+
 	/**
 	 * Push the objects and refs. Send the necessary objects, and
 	 * then, for any refs where peer_ref is set and
diff --git a/transport.c b/transport.c
index fc144f0..42b0a2d 100644
--- a/transport.c
+++ b/transport.c
@@ -9,6 +9,7 @@
 #include "hook.h"
 #include "pkt-line.h"
 #include "fetch-pack.h"
+#include "fetch-object-info.h"
 #include "remote.h"
 #include "connect.h"
 #include "send-pack.h"
@@ -432,6 +433,47 @@ static int get_bundle_uri(struct transport *transport)
 				     transport->bundles, stateless_rpc);
 }
 
+static int fetch_object_info_via_pack(struct transport *transport)
+{
+	int ret = 0;
+	struct git_transport_data *data = transport->data;
+	struct packet_reader reader;
+	struct object_info_args args = { 0 };
+
+	args.server_options = transport->server_options;
+	args.oids = transport->smart_options->object_info_oids;
+	args.object_info_options = transport->smart_options->object_info_options;
+
+	connect_setup(transport, 0);
+	packet_reader_init(&reader, data->fd[0], NULL, 0,
+			   PACKET_READ_CHOMP_NEWLINE |
+			   PACKET_READ_GENTLE_ON_EOF |
+			   PACKET_READ_DIE_ON_ERR_PACKET);
+
+	data->version = discover_version(&reader);
+	transport->hash_algo = reader.hash_algo;
+
+	ret = fetch_object_info(data->version, &args, &reader,
+				data->options.object_info_data,
+				transport->stateless_rpc, data->fd[1]);
+
+	close(data->fd[0]);
+	if (data->fd[1] >= 0)
+		close(data->fd[1]);
+	if (finish_connect(data->conn))
+		ret = -1;
+	data->conn = NULL;
+
+	return ret;
+}
+
+int transport_fetch_object_info(struct transport *transport)
+{
+	if (!transport->vtable->fetch_object_info)
+		die(_("remote does not support object-info"));
+	return transport->vtable->fetch_object_info(transport);
+}
+
 static int fetch_refs_via_pack(struct transport *transport,
 			       int nr_heads, struct ref **to_fetch)
 {
@@ -1004,6 +1046,7 @@ static struct transport_vtable taken_over_vtable = {
 	.get_refs_list	= get_refs_via_connect,
 	.get_bundle_uri = get_bundle_uri,
 	.fetch_refs	= fetch_refs_via_pack,
+	.fetch_object_info = fetch_object_info_via_pack,
 	.push_refs	= git_transport_push,
 	.disconnect	= disconnect_git
 };
@@ -1169,6 +1212,7 @@ static struct transport_vtable builtin_smart_vtable = {
 	.get_refs_list	= get_refs_via_connect,
 	.get_bundle_uri = get_bundle_uri,
 	.fetch_refs	= fetch_refs_via_pack,
+	.fetch_object_info = fetch_object_info_via_pack,
 	.push_refs	= git_transport_push,
 	.connect	= connect_git,
 	.disconnect	= disconnect_git
@@ -1553,8 +1597,11 @@ int transport_push(struct repository *r,
 	if (!(flags & (TRANSPORT_PUSH_DRY_RUN |
 		       TRANSPORT_RECURSE_SUBMODULES_ONLY))) {
 		struct ref *ref;
+		struct remote *tracking_remote = repo_remote_for_push_tracking(
+			r, transport->remote);
+
 		for (ref = remote_refs; ref; ref = ref->next)
-			transport_update_tracking_ref(transport->remote, ref, verbose);
+			transport_update_tracking_ref(tracking_remote, ref, verbose);
 	}
 
 	if (porcelain && !push_ret)
diff --git a/transport.h b/transport.h
index 7e5867c..a7869d1 100644
--- a/transport.h
+++ b/transport.h
@@ -55,6 +55,10 @@ struct git_transport_options {
 	 * common commits to this oidset instead of fetching any packfiles.
 	 */
 	struct oidset *acked_commits;
+
+	struct oid_array *object_info_oids;
+	struct object_info *object_info_data;
+	struct string_list *object_info_options;
 };
 
 enum transport_family {
@@ -310,6 +314,11 @@ const struct git_hash_algo *transport_get_hash_algo(struct transport *transport)
 int transport_fetch_refs(struct transport *transport, struct ref *refs);
 
 /*
+ * Fetch the object info from remote
+ */
+int transport_fetch_object_info(struct transport *transport);
+
+/*
  * If this flag is set, unlocking will avoid to call non-async-signal-safe
  * functions. This will necessarily leave behind some data structures which
  * cannot be cleaned up.
diff --git a/unpack-trees.c b/unpack-trees.c
index b42020f..154d6d4 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -671,8 +671,10 @@ static struct cache_entry *next_cache_entry(struct unpack_trees_options *o)
 
 	while (pos < index->cache_nr) {
 		struct cache_entry *ce = index->cache[pos];
-		if (!(ce->ce_flags & CE_UNPACKED))
+		if (!(ce->ce_flags & CE_UNPACKED)) {
+			o->internal.cache_bottom = pos;
 			return ce;
+		}
 		pos++;
 	}
 	return NULL;
@@ -2428,7 +2430,7 @@ static int check_ok_to_remove(const char *name, int len, int dtype,
 	 *
 	 * Ignore that lstat() if it matches.
 	 */
-	if (ignore_case && icase_exists(o, name, len, st))
+	if (repo_ignore_case(the_repository) && icase_exists(o, name, len, st))
 		return 0;
 
 	if (o->internal.dir &&
diff --git a/usage.c b/usage.c
index 527edb1..3f0118a 100644
--- a/usage.c
+++ b/usage.c
@@ -188,7 +188,7 @@ static void show_usage_if_asked_helper(const char *err, ...)
 	va_start(params, err);
 	vfreportf(stdout, _("usage: "), err, params);
 	va_end(params);
-	exit(129);
+	exit(0);
 }
 
 void show_usage_if_asked(int ac, const char **av, const char *err)
diff --git a/userdiff.c b/userdiff.c
index b5412e6..7129bf1 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -362,6 +362,16 @@ PATTERNS("scheme",
 	 "\\|([^|\\\\]|\\\\.)*\\|"
 	 /* All other words should be delimited by spaces or parentheses. */
 	 "|([^][)(}{ \t])+"),
+PATTERNS("swift",
+	 "^[ \t]*((@[A-Za-z_][A-Za-z0-9_]*(\\([^()]*\\))?[ \t]+)*([a-z]+[ \t]+)*(func|init|deinit|subscript|class|struct|enum|protocol|extension|actor)[ \t(?!<].*)$",
+	 /* -- */
+	 "[a-zA-Z_][a-zA-Z0-9_]*"
+	 /* hexadecimal, octal, and binary literals */
+	 "|0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+"
+	 /* integers and floating-point numbers */
+	 "|[0-9][0-9_]*([.][0-9_]+)?([eE][-+]?[0-9]+)?"
+	 /* unary and binary operators */
+	 "|[-+*/%<>=!&|^~?]=|&&|\\|\\||<<=?|>>=?|\\?\\?|\\.\\.[.<]|->"),
 PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
 	 "\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
 { .name = "default", .binary = -1 },
diff --git a/walker.c b/walker.c
index e98eb6d..e3de77f 100644
--- a/walker.c
+++ b/walker.c
@@ -84,12 +84,12 @@ static struct prio_queue complete = { compare_commits_by_commit_date };
 static int process_commit(struct walker *walker, struct commit *commit)
 {
 	struct commit_list *parents;
+	struct commit *item;
 
 	if (repo_parse_commit(the_repository, commit))
 		return -1;
 
-	while (complete.nr) {
-		struct commit *item = prio_queue_peek(&complete);
+	while ((item = prio_queue_peek(&complete))) {
 		if (item->date < commit->date)
 			break;
 		pop_most_recent_commit(&complete, COMPLETE);
diff --git a/worktree.c b/worktree.c
index 3012582..cbf9532 100644
--- a/worktree.c
+++ b/worktree.c
@@ -1,4 +1,3 @@
-#define USE_THE_REPOSITORY_VARIABLE
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "git-compat-util.h"
@@ -111,34 +110,36 @@ static int is_main_worktree_bare(struct repository *repo)
 /**
  * get the main worktree
  */
-static struct worktree *get_main_worktree(int skip_reading_head)
+static struct worktree *get_main_worktree(struct repository *repo,
+					  int skip_reading_head)
 {
 	struct worktree *worktree = NULL;
 	struct strbuf worktree_path = STRBUF_INIT;
 
-	strbuf_add_real_path(&worktree_path, repo_get_common_dir(the_repository));
+	strbuf_add_real_path(&worktree_path, repo_get_common_dir(repo));
 	strbuf_strip_suffix(&worktree_path, "/.git");
 
 	CALLOC_ARRAY(worktree, 1);
-	worktree->repo = the_repository;
+	worktree->repo = repo;
 	worktree->path = strbuf_detach(&worktree_path, NULL);
 	worktree->is_current = is_current_worktree(worktree);
-	worktree->is_bare = (the_repository->bare_cfg == 1) ||
-		is_bare_repository(the_repository) ||
+	worktree->is_bare = (repo->bare_cfg == 1) ||
+		is_bare_repository(repo) ||
 		/*
 		 * When in a secondary worktree we have to also verify if the main
 		 * worktree is bare in $commondir/config.worktree.
 		 * This check is unnecessary if we're currently in the main worktree,
 		 * as prior checks already consulted all configs of the current worktree.
 		 */
-		(!worktree->is_current && is_main_worktree_bare(the_repository));
+		(!worktree->is_current && is_main_worktree_bare(repo));
 
 	if (!skip_reading_head)
 		add_head_info(worktree);
 	return worktree;
 }
 
-struct worktree *get_linked_worktree(const char *id,
+struct worktree *get_linked_worktree(struct repository *repo,
+				     const char *id,
 				     int skip_reading_head)
 {
 	struct worktree *worktree = NULL;
@@ -148,7 +149,7 @@ struct worktree *get_linked_worktree(const char *id,
 	if (!id)
 		die("Missing linked worktree name");
 
-	repo_common_path_append(the_repository, &path, "worktrees/%s/gitdir", id);
+	repo_common_path_append(repo, &path, "worktrees/%s/gitdir", id);
 	if (strbuf_read_file(&worktree_path, path.buf, 0) <= 0)
 		/* invalid gitdir file */
 		goto done;
@@ -162,7 +163,7 @@ struct worktree *get_linked_worktree(const char *id,
 	}
 
 	CALLOC_ARRAY(worktree, 1);
-	worktree->repo = the_repository;
+	worktree->repo = repo;
 	worktree->path = strbuf_detach(&worktree_path, NULL);
 	worktree->id = xstrdup(id);
 	worktree->is_current = is_current_worktree(worktree);
@@ -182,7 +183,8 @@ struct worktree *get_linked_worktree(const char *id,
  * retrieving worktree metadata that could be used when the worktree is known
  * to not be in a healthy state, e.g. when creating or repairing it.
  */
-static struct worktree **get_worktrees_internal(int skip_reading_head)
+static struct worktree **get_worktrees_internal(struct repository *repo,
+						int skip_reading_head)
 {
 	struct worktree **list = NULL;
 	struct strbuf path = STRBUF_INIT;
@@ -192,16 +194,16 @@ static struct worktree **get_worktrees_internal(int skip_reading_head)
 
 	ALLOC_ARRAY(list, alloc);
 
-	list[counter++] = get_main_worktree(skip_reading_head);
+	list[counter++] = get_main_worktree(repo, skip_reading_head);
 
-	strbuf_addf(&path, "%s/worktrees", repo_get_common_dir(the_repository));
+	strbuf_addf(&path, "%s/worktrees", repo_get_common_dir(repo));
 	dir = opendir(path.buf);
 	strbuf_release(&path);
 	if (dir) {
 		while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {
 			struct worktree *linked = NULL;
 
-			if ((linked = get_linked_worktree(d->d_name, skip_reading_head))) {
+			if ((linked = get_linked_worktree(repo, d->d_name, skip_reading_head))) {
 				ALLOC_GROW(list, counter + 1, alloc);
 				list[counter++] = linked;
 			}
@@ -214,14 +216,14 @@ static struct worktree **get_worktrees_internal(int skip_reading_head)
 	return list;
 }
 
-struct worktree **get_worktrees(void)
+struct worktree **get_worktrees(struct repository *repo)
 {
-	return get_worktrees_internal(0);
+	return get_worktrees_internal(repo, 0);
 }
 
-struct worktree **get_worktrees_without_reading_head(void)
+struct worktree **get_worktrees_without_reading_head(struct repository *repo)
 {
-	return get_worktrees_internal(1);
+	return get_worktrees_internal(repo, 1);
 }
 
 char *get_worktree_git_dir(const struct worktree *wt)
@@ -334,7 +336,7 @@ const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire)
 	if (wt->prune_reason_valid)
 		return wt->prune_reason;
 
-	if (should_prune_worktree(wt->id, &reason, &path, expire))
+	if (should_prune_worktree(wt->repo, wt->id, &reason, &path, expire))
 		wt->prune_reason = strbuf_detach(&reason, NULL);
 	wt->prune_reason_valid = 1;
 
@@ -392,7 +394,7 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,
 	if (!is_absolute_path(wt->path)) {
 		strbuf_addf_gently(errmsg,
 				   _("'%s' file does not contain absolute path to the working tree location"),
-				   repo_common_path_replace(the_repository, &buf, "worktrees/%s/gitdir", wt->id));
+				   repo_common_path_replace(wt->repo, &buf, "worktrees/%s/gitdir", wt->id));
 		goto done;
 	}
 
@@ -414,12 +416,12 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,
 		goto done;
 	}
 
-	strbuf_realpath(&realpath, repo_common_path_replace(the_repository, &buf, "worktrees/%s", wt->id), 1);
+	strbuf_realpath(&realpath, repo_common_path_replace(wt->repo, &buf, "worktrees/%s", wt->id), 1);
 	ret = fspathcmp(path, realpath.buf);
 
 	if (ret)
 		strbuf_addf_gently(errmsg, _("'%s' does not point back to '%s'"),
-				   wt->path, repo_common_path_replace(the_repository, &buf,
+				   wt->path, repo_common_path_replace(wt->repo, &buf,
 								      "worktrees/%s", wt->id));
 done:
 	free(path);
@@ -440,12 +442,13 @@ void update_worktree_location(struct worktree *wt, const char *path_,
 	if (is_main_worktree(wt))
 		BUG("can't relocate main worktree");
 
-	wt_gitdir = repo_common_path(the_repository, "worktrees/%s/gitdir", wt->id);
+	wt_gitdir = repo_common_path(wt->repo, "worktrees/%s/gitdir", wt->id);
 	strbuf_realpath(&gitdir, wt_gitdir, 1);
 	strbuf_realpath(&path, path_, 1);
 	strbuf_addf(&dotgit, "%s/.git", path.buf);
 	if (fspathcmp(wt->path, path.buf)) {
-		write_worktree_linking_files(dotgit.buf, gitdir.buf, use_relative_paths);
+		write_worktree_linking_files(wt->repo, dotgit.buf,
+					     gitdir.buf, use_relative_paths);
 
 		free(wt->path);
 		wt->path = strbuf_detach(&path, NULL);
@@ -533,7 +536,8 @@ const struct worktree *find_shared_symref(struct worktree **worktrees,
 	return NULL;
 }
 
-int submodule_uses_worktrees(const char *path)
+int submodule_uses_worktrees(struct repository *repo,
+			     const char *path)
 {
 	char *submodule_gitdir;
 	struct strbuf sb = STRBUF_INIT, err = STRBUF_INIT;
@@ -542,7 +546,7 @@ int submodule_uses_worktrees(const char *path)
 	int ret = 0;
 	struct repository_format format = REPOSITORY_FORMAT_INIT;
 
-	submodule_gitdir = repo_submodule_path(the_repository,
+	submodule_gitdir = repo_submodule_path(repo,
 					       path, "%s", "");
 	if (!submodule_gitdir)
 		return 0;
@@ -595,13 +599,14 @@ void strbuf_worktree_ref(const struct worktree *wt,
 	strbuf_addstr(sb, refname);
 }
 
-int other_head_refs(refs_for_each_cb fn, void *cb_data)
+int other_head_refs(struct repository *repo,
+		    refs_for_each_cb fn, void *cb_data)
 {
 	struct worktree **worktrees, **p;
 	struct strbuf refname = STRBUF_INIT;
 	int ret = 0;
 
-	worktrees = get_worktrees();
+	worktrees = get_worktrees(repo);
 	for (p = worktrees; *p; p++) {
 		struct worktree *wt = *p;
 		struct object_id oid;
@@ -612,7 +617,7 @@ int other_head_refs(refs_for_each_cb fn, void *cb_data)
 
 		strbuf_reset(&refname);
 		strbuf_worktree_ref(wt, &refname, "HEAD");
-		if (refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
+		if (refs_resolve_ref_unsafe(get_main_ref_store(repo),
 					    refname.buf,
 					    RESOLVE_REF_READING,
 					    &oid, &flag)) {
@@ -658,7 +663,7 @@ static void repair_gitfile(struct worktree *wt,
 		goto done;
 	}
 
-	path = repo_common_path(the_repository, "worktrees/%s", wt->id);
+	path = repo_common_path(wt->repo, "worktrees/%s", wt->id);
 	strbuf_realpath(&repo, path, 1);
 	strbuf_addf(&dotgit, "%s/.git", wt->path);
 	strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
@@ -685,7 +690,8 @@ static void repair_gitfile(struct worktree *wt,
 
 	if (repair) {
 		fn(0, wt->path, repair, cb_data);
-		write_worktree_linking_files(dotgit.buf, gitdir.buf, use_relative_paths);
+		write_worktree_linking_files(wt->repo, dotgit.buf,
+					     gitdir.buf, use_relative_paths);
 	}
 
 done:
@@ -705,9 +711,10 @@ static void repair_noop(int iserr UNUSED,
 	/* nothing */
 }
 
-void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_paths)
+void repair_worktrees(struct repository *repo, worktree_repair_fn fn,
+		      void *cb_data, int use_relative_paths)
 {
-	struct worktree **worktrees = get_worktrees_internal(1);
+	struct worktree **worktrees = get_worktrees_internal(repo, 1);
 	struct worktree **wt = worktrees + 1; /* +1 skips main worktree */
 
 	if (!fn)
@@ -727,7 +734,7 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path
 	if (is_main_worktree(wt))
 		goto done;
 
-	path = repo_common_path(the_repository, "worktrees/%s/gitdir", wt->id);
+	path = repo_common_path(wt->repo, "worktrees/%s/gitdir", wt->id);
 	strbuf_realpath(&gitdir, path, 1);
 
 	if (strbuf_read_file(&dotgit, gitdir.buf, 0) < 0)
@@ -743,16 +750,17 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path
 	if (!file_exists(dotgit.buf))
 		goto done;
 
-	write_worktree_linking_files(dotgit.buf, gitdir.buf, is_relative_path);
+	write_worktree_linking_files(wt->repo, dotgit.buf,
+				     gitdir.buf, is_relative_path);
 done:
 	strbuf_release(&gitdir);
 	strbuf_release(&dotgit);
 	free(path);
 }
 
-void repair_worktrees_after_gitdir_move(const char *old_path)
+void repair_worktrees_after_gitdir_move(struct repository *repo, const char *old_path)
 {
-	struct worktree **worktrees = get_worktrees_internal(1);
+	struct worktree **worktrees = get_worktrees_internal(repo, 1);
 	struct worktree **wt = worktrees + 1; /* +1 skips main worktree */
 
 	for (; *wt; wt++)
@@ -760,7 +768,7 @@ void repair_worktrees_after_gitdir_move(const char *old_path)
 	free_worktrees(worktrees);
 }
 
-static int is_main_worktree_path(const char *path)
+static int is_main_worktree_path(struct repository *repo, const char *path)
 {
 	struct strbuf target = STRBUF_INIT;
 	struct strbuf maindir = STRBUF_INIT;
@@ -768,7 +776,7 @@ static int is_main_worktree_path(const char *path)
 
 	strbuf_add_real_path(&target, path);
 	strbuf_strip_suffix(&target, "/.git");
-	strbuf_add_real_path(&maindir, repo_get_common_dir(the_repository));
+	strbuf_add_real_path(&maindir, repo_get_common_dir(repo));
 	strbuf_strip_suffix(&maindir, "/.git");
 	cmp = fspathcmp(maindir.buf, target.buf);
 
@@ -786,7 +794,9 @@ static int is_main_worktree_path(const char *path)
  *
  * Returns -1 on failure and strbuf.len on success.
  */
-static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred)
+static ssize_t infer_backlink(struct repository *repo,
+			      const char *gitfile,
+			      struct strbuf *inferred)
 {
 	struct strbuf actual = STRBUF_INIT;
 	const char *id;
@@ -801,7 +811,7 @@ static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred)
 	id++; /* advance past '/' to point at <id> */
 	if (!*id)
 		goto error;
-	repo_common_path_replace(the_repository, inferred, "worktrees/%s", id);
+	repo_common_path_replace(repo, inferred, "worktrees/%s", id);
 	if (!is_directory(inferred->buf))
 		goto error;
 
@@ -817,7 +827,8 @@ static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred)
  * Repair <repo>/worktrees/<id>/gitdir if missing, corrupt, or not pointing at
  * the worktree's path.
  */
-void repair_worktree_at_path(const char *path,
+void repair_worktree_at_path(struct repository *repo,
+			     const char *path,
 			     worktree_repair_fn fn, void *cb_data,
 			     int use_relative_paths)
 {
@@ -833,7 +844,7 @@ void repair_worktree_at_path(const char *path,
 	if (!fn)
 		fn = repair_noop;
 
-	if (is_main_worktree_path(path))
+	if (is_main_worktree_path(repo, path))
 		goto done;
 
 	strbuf_addf(&dotgit, "%s/.git", path);
@@ -842,7 +853,7 @@ void repair_worktree_at_path(const char *path,
 		goto done;
 	}
 
-	infer_backlink(dotgit.buf, &inferred_backlink);
+	infer_backlink(repo, dotgit.buf, &inferred_backlink);
 	strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);
 	dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
 	if (dotgit_contents) {
@@ -915,7 +926,8 @@ void repair_worktree_at_path(const char *path,
 
 	if (repair) {
 		fn(0, gitdir.buf, repair, cb_data);
-		write_worktree_linking_files(dotgit.buf, gitdir.buf, use_relative_paths);
+		write_worktree_linking_files(repo, dotgit.buf,
+					     gitdir.buf, use_relative_paths);
 	}
 done:
 	free(dotgit_contents);
@@ -926,12 +938,16 @@ void repair_worktree_at_path(const char *path,
 	strbuf_release(&dotgit);
 }
 
-int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
+int should_prune_worktree(struct repository *repo,
+			  const char *id,
+			  struct strbuf *reason,
+			  char **wtpath,
+			  timestamp_t expire)
 {
 	struct stat st;
 	struct strbuf dotgit = STRBUF_INIT;
 	struct strbuf gitdir = STRBUF_INIT;
-	struct strbuf repo = STRBUF_INIT;
+	struct strbuf repo_path = STRBUF_INIT;
 	struct strbuf file = STRBUF_INIT;
 	char *path = NULL;
 	int rc = 0;
@@ -941,17 +957,17 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
 
 	*wtpath = NULL;
 
-	path = repo_common_path(the_repository, "worktrees/%s", id);
-	strbuf_realpath(&repo, path, 1);
+	path = repo_common_path(repo, "worktrees/%s", id);
+	strbuf_realpath(&repo_path, path, 1);
 	FREE_AND_NULL(path);
 
-	strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
-	if (!is_directory(repo.buf)) {
+	strbuf_addf(&gitdir, "%s/gitdir", repo_path.buf);
+	if (!is_directory(repo_path.buf)) {
 		strbuf_addstr(reason, _("not a valid directory"));
 		rc = 1;
 		goto done;
 	}
-	strbuf_addf(&file, "%s/locked", repo.buf);
+	strbuf_addf(&file, "%s/locked", repo_path.buf);
 	if (file_exists(file.buf)) {
 		goto done;
 	}
@@ -995,12 +1011,12 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
 	if (is_absolute_path(path)) {
 		strbuf_addstr(&dotgit, path);
 	} else {
-		strbuf_addf(&dotgit, "%s/%s", repo.buf, path);
+		strbuf_addf(&dotgit, "%s/%s", repo_path.buf, path);
 		strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
 	}
 	if (!file_exists(dotgit.buf)) {
 		strbuf_reset(&file);
-		strbuf_addf(&file, "%s/index", repo.buf);
+		strbuf_addf(&file, "%s/index", repo_path.buf);
 		if (stat(file.buf, &st) || st.st_mtime <= expire) {
 			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
 			rc = 1;
@@ -1012,17 +1028,18 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
 	free(path);
 	strbuf_release(&dotgit);
 	strbuf_release(&gitdir);
-	strbuf_release(&repo);
+	strbuf_release(&repo_path);
 	strbuf_release(&file);
 	return rc;
 }
 
-static int move_config_setting(const char *key, const char *value,
+static int move_config_setting(struct repository *repo,
+			       const char *key, const char *value,
 			       const char *from_file, const char *to_file)
 {
-	if (repo_config_set_in_file_gently(the_repository, to_file, key, NULL, value))
+	if (repo_config_set_in_file_gently(repo, to_file, key, NULL, value))
 		return error(_("unable to set %s in '%s'"), key, to_file);
-	if (repo_config_set_in_file_gently(the_repository, from_file, key, NULL, NULL))
+	if (repo_config_set_in_file_gently(repo, from_file, key, NULL, NULL))
 		return error(_("unable to unset %s in '%s'"), key, from_file);
 	return 0;
 }
@@ -1042,7 +1059,7 @@ int init_worktree_config(struct repository *r)
 	 */
 	if (r->repository_format_worktree_config)
 		return 0;
-	if ((res = repo_config_set_gently(the_repository, "extensions.worktreeConfig", "true")))
+	if ((res = repo_config_set_gently(r, "extensions.worktreeConfig", "true")))
 		return error(_("failed to set extensions.worktreeConfig setting"));
 
 	common_config_file = xstrfmt("%s/config", r->commondir);
@@ -1058,7 +1075,7 @@ int init_worktree_config(struct repository *r)
 	 * _could_ be negating a global core.bare=true.
 	 */
 	if (!git_configset_get_bool(&cs, "core.bare", &bare) && bare) {
-		if ((res = move_config_setting("core.bare", "true",
+		if ((res = move_config_setting(r, "core.bare", "true",
 					       common_config_file,
 					       main_worktree_file)))
 			goto cleanup;
@@ -1070,7 +1087,7 @@ int init_worktree_config(struct repository *r)
 	 * upgrade to worktree config.
 	 */
 	if (!git_configset_get_value(&cs, "core.worktree", &core_worktree, NULL)) {
-		if ((res = move_config_setting("core.worktree", core_worktree,
+		if ((res = move_config_setting(r, "core.worktree", core_worktree,
 					       common_config_file,
 					       main_worktree_file)))
 			goto cleanup;
@@ -1089,37 +1106,38 @@ int init_worktree_config(struct repository *r)
 	return res;
 }
 
-void write_worktree_linking_files(const char *dotgit, const char *gitdir,
+void write_worktree_linking_files(struct repository *repo,
+				  const char *dotgit, const char *gitdir,
 				  int use_relative_paths)
 {
 	struct strbuf path = STRBUF_INIT;
-	struct strbuf repo = STRBUF_INIT;
+	struct strbuf repo_path = STRBUF_INIT;
 	struct strbuf tmp = STRBUF_INIT;
 
 	strbuf_addstr(&path, dotgit);
 	strbuf_strip_suffix(&path, "/.git");
 	strbuf_realpath(&path, path.buf, 1);
-	strbuf_addstr(&repo, gitdir);
-	strbuf_strip_suffix(&repo, "/gitdir");
-	strbuf_realpath(&repo, repo.buf, 1);
+	strbuf_addstr(&repo_path, gitdir);
+	strbuf_strip_suffix(&repo_path, "/gitdir");
+	strbuf_realpath(&repo_path, repo_path.buf, 1);
 
-	if (use_relative_paths && !the_repository->repository_format_relative_worktrees) {
-		if (upgrade_repository_format(the_repository, 1) < 0)
+	if (use_relative_paths && !repo->repository_format_relative_worktrees) {
+		if (upgrade_repository_format(repo, 1) < 0)
 			die(_("unable to upgrade repository format to support relative worktrees"));
-		if (repo_config_set_gently(the_repository, "extensions.relativeWorktrees", "true"))
+		if (repo_config_set_gently(repo, "extensions.relativeWorktrees", "true"))
 			die(_("unable to set extensions.relativeWorktrees setting"));
-		the_repository->repository_format_relative_worktrees = 1;
+		repo->repository_format_relative_worktrees = 1;
 	}
 
 	if (use_relative_paths) {
-		write_file(gitdir, "%s/.git", relative_path(path.buf, repo.buf, &tmp));
-		write_file(dotgit, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
+		write_file(gitdir, "%s/.git", relative_path(path.buf, repo_path.buf, &tmp));
+		write_file(dotgit, "gitdir: %s", relative_path(repo_path.buf, path.buf, &tmp));
 	} else {
 		write_file(gitdir, "%s/.git", path.buf);
-		write_file(dotgit, "gitdir: %s", repo.buf);
+		write_file(dotgit, "gitdir: %s", repo_path.buf);
 	}
 
 	strbuf_release(&path);
-	strbuf_release(&repo);
+	strbuf_release(&repo_path);
 	strbuf_release(&tmp);
 }
diff --git a/worktree.h b/worktree.h
index 1075409..fbb2757 100644
--- a/worktree.h
+++ b/worktree.h
@@ -28,7 +28,7 @@ struct worktree {
  * The caller is responsible for freeing the memory from the returned
  * worktrees by calling free_worktrees().
  */
-struct worktree **get_worktrees(void);
+struct worktree **get_worktrees(struct repository *repo);
 
 /*
  * Like `get_worktrees`, but does not read HEAD. Skip reading HEAD allows to
@@ -36,7 +36,7 @@ struct worktree **get_worktrees(void);
  * the HEAD ref. This is useful in contexts where it is assumed that the
  * refdb may not be in a consistent state.
  */
-struct worktree **get_worktrees_without_reading_head(void);
+struct worktree **get_worktrees_without_reading_head(struct repository *repo);
 
 /*
  * Construct a struct worktree corresponding to repo->gitdir and
@@ -47,7 +47,7 @@ struct worktree *get_current_worktree(struct repository *repo);
 /*
  * Returns 1 if linked worktrees exist, 0 otherwise.
  */
-int submodule_uses_worktrees(const char *path);
+int submodule_uses_worktrees(struct repository *repo, const char *path);
 
 /*
  * Return git dir of the worktree. Note that the path may be relative.
@@ -76,7 +76,8 @@ struct worktree *find_worktree(struct worktree **list,
  * Look up the worktree corresponding to `id`, or NULL of no such worktree
  * exists.
  */
-struct worktree *get_linked_worktree(const char *id,
+struct worktree *get_linked_worktree(struct repository *repo,
+				     const char *id,
 				     int skip_reading_head);
 
 /*
@@ -112,7 +113,8 @@ const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
  * `expire` defines a grace period to prune the worktree when its path
  * does not exist.
  */
-int should_prune_worktree(const char *id,
+int should_prune_worktree(struct repository *repo,
+			  const char *id,
 			  struct strbuf *reason,
 			  char **wtpath,
 			  timestamp_t expire);
@@ -142,12 +144,14 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path,
  * function, if non-NULL, is called with the path of the worktree and a
  * description of the repair or error, along with the callback user-data.
  */
-void repair_worktrees(worktree_repair_fn, void *cb_data, int use_relative_paths);
+void repair_worktrees(struct repository *repo, worktree_repair_fn,
+		      void *cb_data, int use_relative_paths);
 
 /*
  * Repair the linked worktrees after the gitdir has been moved.
  */
-void repair_worktrees_after_gitdir_move(const char *old_path);
+void repair_worktrees_after_gitdir_move(struct repository *repo,
+					const char *old_path);
 
 /*
  * Repair the linked worktree after the gitdir has been moved.
@@ -164,7 +168,9 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path
  * worktree and a description of the repair or error, along with the callback
  * user-data.
  */
-void repair_worktree_at_path(const char *, worktree_repair_fn,
+void repair_worktree_at_path(struct repository *repo,
+			     const char *path,
+			     worktree_repair_fn fn,
 			     void *cb_data, int use_relative_paths);
 
 /*
@@ -196,7 +202,7 @@ int is_shared_symref(const struct worktree *wt,
  * Similar to head_ref() for all HEADs _except_ one from the current
  * worktree, which is covered by head_ref().
  */
-int other_head_refs(refs_for_each_cb fn, void *cb_data);
+int other_head_refs(struct repository *repo, refs_for_each_cb fn, void *cb_data);
 
 int is_worktree_being_rebased(const struct worktree *wt, const char *target);
 int is_worktree_being_bisected(const struct worktree *wt, const char *target);
@@ -239,7 +245,8 @@ int init_worktree_config(struct repository *r);
  *  dotgit: "/path/to/foo/.git"
  *  gitdir: "/path/to/repo/worktrees/foo/gitdir"
  */
-void write_worktree_linking_files(const char *dotgit, const char *gitdir,
+void write_worktree_linking_files(struct repository *repo,
+				  const char *dotgit, const char *gitdir,
 				  int use_relative_paths);
 
 #endif
diff --git a/wt-status.c b/wt-status.c
index 58461e0..57772c7 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -832,14 +832,16 @@ static void wt_status_collect_untracked(struct wt_status *s)
 	for (i = 0; i < dir.nr; i++) {
 		struct dir_entry *ent = dir.entries[i];
 		if (index_name_is_other(istate, ent->name, ent->len))
-			string_list_insert(&s->untracked, ent->name);
+			string_list_append(&s->untracked, ent->name);
 	}
+	string_list_sort_u(&s->untracked, 0);
 
 	for (i = 0; i < dir.ignored_nr; i++) {
 		struct dir_entry *ent = dir.ignored[i];
 		if (index_name_is_other(istate, ent->name, ent->len))
-			string_list_insert(&s->ignored, ent->name);
+			string_list_append(&s->ignored, ent->name);
 	}
+	string_list_sort_u(&s->ignored, 0);
 
 	dir_clear(&dir);