Skip to contentSkip to conversation

t: add lint-style.pl and convert grep to test_grep

PATCH Superseded Michael Montalbo via GitGitGadget 5 participants 40 messages Jun 4 – Jul 6 Page 1 of 2

lore
Share & tools
lore
#0 lore raw

cover t: add lint-style.pl and convert grep to test_grep

The test suite has a test_grep wrapper that prints file contents on assertion failure, making debugging easier. Many tests still use bare 'grep' for assertions, which silently swallows context on failure.

This series adds a lint tool (lint-style.pl) to mechanically detect and convert these, then applies it across the test suite.

The tool reuses the shared shell parser (t/lib-shell-parser.pl) rather than reimplementing shell parsing. This gives us proper handling of heredocs, $(...), pipes, quoting, case patterns, and control flow structures for free. The only new parsing logic is the grep assertion classifier, which works on the already-parsed token stream.

The classifier distinguishes grep used as an assertion (checking exit status against PATTERN + FILE) from grep used as a filter (pipes, redirects, $(), control flow conditions). Only assertions are converted. Greps at assertion level with no file argument are flagged as likely bugs; three pre-existing instances were found and fixed in patch 5.

The --fix mode converts mechanically: 'grep' becomes 'test_grep', '! grep' becomes 'test_grep !'.

Structure:

1/6 t/README: document test_grep helper 2/6 t: extract chainlint's parser into shared module 3/6 t: fix Lexer line count for $() inside double-quoted strings 4/6 t: add lint-style.pl with test_grep negation rule 5/6 t: fix grep assertions missing file arguments 6/6 t: lint and convert grep assertions to test_grep

Patch 2 extracts chainlint.pl's Lexer, ShellParser, and ScriptParser into t/lib-shell-parser.pl. ScriptParser's check_test() becomes a no-op in the module; chainlint.pl defines ChainlintParser (a ScriptParser subclass that runs TestParser for &&-chain detection), and lint-style.pl defines LintParser (a ScriptParser subclass that runs grep lint rules).

Patch 3 fixes a pre-existing bug in scan_dqstring where the post-loop newline counter re-counted newlines that were already counted during recursive $() parsing. The fix counts newlines inline as non-special characters are consumed, removing the catch-all counter entirely. chainlint is unaffected (it uses byte offsets), but lint-style.pl needs accurate token line numbers to locate and fix specific lines in the original file.

Patch 4 introduces the lint-style.pl framework (LintParser subclass, Makefile targets, fixture infrastructure) with a small, complete rule (! test_grep -> test_grep !) so reviewers can see the machinery in action before the bulk conversion.

Patch 5 fixes three test bugs where grep assertions were missing their file arguments, causing them to pass vacuously (all three pass with the corrected arguments). These were independently discovered by the missing-file detection rule introduced in patch 6.

Patch 6 adds the main rule, its fixtures, and the mechanical conversion of ~2800 assertions across ~340 files, including sourced test fragments in t/t5411/ and heredoc test bodies in t/t5564/.

To verify the conversion (patch 6 adds both the rule and the mechanical conversion in the same commit, so apply the full series and re-run --fix to confirm it produces no further changes):

git checkout && perl t/lint-style.pl --fix t/t*.sh t/test-lib*.sh t/lib-.sh t/-tests.sh t/perf/.sh t/t5411/.sh

As an independent completeness check:

grep -rn '^\s*!\sgrep\s' t/t*.sh t/lib-.sh t/-tests.sh t/test-lib*.sh t/perf/*.sh | grep -v test_grep | grep -v lint-ok | grep -v '#.*grep'

Future rules:

The framework is designed to make adding new rules cheap. Each rule is a function that receives parsed commands and the token stream. The harness handles tokenization, line mapping, --fix, and fixture testing. Three natural follow-ups:

* 'test_must_fail grep': test_must_fail distinguishes expected failures from crashes (signals), which only makes sense for git commands. Using it with grep or test_grep should be '! grep' or 'test_grep !' instead.

* '! git cmd': should use test_must_fail, which distinguishes controlled failures from crashes. The README explicitly documents this as a "don't".

* 'test -f' / 'test -d' / 'test -e': should use test_path_is_file, test_path_is_dir, test_path_exists. The helpers print the actual directory listing on failure; bare 'test' just says "failed".

Known limitations:

* One grep in t/t1400 asserts against .git/packed-refs which does not exist on the reftable backend; suppressed with '# lint-ok'.

* One grep in t/t7450 checks a path inside a clone that may have failed (MINGW-only test); the file may not exist, so test_grep's existence check would trip; suppressed with '# lint-ok'.

* Two greps in t/t3901 inside case branches that inherit piped stdin from two lines above are suppressed with '# lint-ok'.

* One grep in t/t6437 uses glob expansion (grep -q content *) which breaks test_grep's file check; suppressed with '# lint-ok'.

* One grep in t/t7527 captures $? for later use rather than asserting inline; suppressed with '# lint-ok'.

Michael Montalbo (6):
t/README: document test_grep helper
t: extract chainlint's parser into shared module
t: fix Lexer line count for $() inside double-quoted strings
t: add lint-style.pl with test_grep negation rule
t: fix grep assertions missing file arguments
t: lint and convert grep assertions to test_grep

 t/.gitattributes                             |   2 +
 t/Makefile                                   |  37 +-
 t/README                                     |  21 +
 t/chainlint.pl                               | 521 +-----------------
 t/check-shell-parser.pl                      |  58 +++
 t/for-each-ref-tests.sh                      |  12 +-
 t/lib-bitmap.sh                              |  12 +-
 t/lib-bundle-uri-protocol.sh                 |  26 +-
 t/lib-httpd.sh                               |   2 +-
 t/lib-shell-parser.pl                        | 522 +++++++++++++++++++
 t/lint-style.pl                              | 425 +++++++++++++++
 t/lint-style/grep-assert.expect              |  13 +
 t/lint-style/grep-assert.test                |  24 +
 t/lint-style/grep-fix.expect                 |  16 +
 t/lint-style/grep-fix.test                   |  16 +
 t/lint-style/grep-missing-file.expect        |   4 +
 t/lint-style/grep-missing-file.test          |   6 +
 t/lint-style/grep-negated.expect             |   5 +
 t/lint-style/grep-negated.test               |   9 +
 t/lint-style/grep-not-assert.expect          |   0
 t/lint-style/grep-not-assert.test            |  43 ++
 t/lint-style/heredoc.expect                  |   3 +
 t/lint-style/heredoc.test                    |  14 +
 t/lint-style/test-grep-negation-fix.expect   |   4 +
 t/lint-style/test-grep-negation-fix.test     |   4 +
 t/lint-style/test-grep-negation.expect       |   3 +
 t/lint-style/test-grep-negation.test         |   4 +
 t/pack-refs-tests.sh                         |   2 +-
 t/show-ref-exists-tests.sh                   |   2 +-
 t/t0000-basic.sh                             |  16 +-
 t/t0001-init.sh                              |  18 +-
 t/t0008-ignores.sh                           |   8 +-
 t/t0009-git-dir-validation.sh                |   6 +-
 t/t0012-help.sh                              |   4 +-
 t/t0013-sha1dc.sh                            |   2 +-
 t/t0017-env-helper.sh                        |   4 +-
 t/t0021-conversion.sh                        |  18 +-
 t/t0029-core-unsetenvvars.sh                 |   4 +-
 t/t0030-stripspace.sh                        |   4 +-
 t/t0031-lockfile-pid.sh                      |   2 +-
 t/t0040-parse-options.sh                     |  52 +-
 t/t0041-usage.sh                             |   2 +-
 t/t0052-simple-ipc.sh                        |  10 +-
 t/t0061-run-command.sh                       |   2 +-
 t/t0066-dir-iterator.sh                      |   2 +-
 t/t0068-for-each-repo.sh                     |  16 +-
 t/t0070-fundamental.sh                       |   6 +-
 t/t0081-find-pack.sh                         |  12 +-
 t/t0091-bugreport.sh                         |  18 +-
 t/t0092-diagnose.sh                          |  12 +-
 t/t0100-previous.sh                          |   2 +-
 t/t0200-gettext-basic.sh                     |  14 +-
 t/t0203-gettext-setlocale-sanity.sh          |   4 +-
 t/t0204-gettext-reencode-sanity.sh           |   8 +-
 t/t0210-trace2-normal.sh                     |   6 +-
 t/t0211-trace2-perf.sh                       |  80 +--
 t/t0212-trace2-event.sh                      |   8 +-
 t/t0300-credentials.sh                       |   4 +-
 t/t0410-partial-clone.sh                     |  82 +--
 t/t0450-txt-doc-vs-help.sh                   |   2 +-
 t/t0500-progress-display.sh                  |  18 +-
 t/t0610-reftable-basics.sh                   |   8 +-
 t/t1004-read-tree-m-u-wf.sh                  |   8 +-
 t/t1006-cat-file.sh                          |  18 +-
 t/t1007-hash-object.sh                       |   8 +-
 t/t1011-read-tree-sparse-checkout.sh         |  10 +-
 t/t1050-large.sh                             |   6 +-
 t/t1091-sparse-checkout-builtin.sh           |  24 +-
 t/t1092-sparse-checkout-compatibility.sh     |  44 +-
 t/t1300-config.sh                            |  16 +-
 t/t1305-config-include.sh                    |   2 +-
 t/t1308-config-set.sh                        |   6 +-
 t/t1400-update-ref.sh                        | 170 +++---
 t/t1403-show-ref.sh                          |  18 +-
 t/t1410-reflog.sh                            |   4 +-
 t/t1415-worktree-refs.sh                     |   4 +-
 t/t1430-bad-ref-name.sh                      |  56 +-
 t/t1450-fsck.sh                              |  12 +-
 t/t1451-fsck-buffer.sh                       |   6 +-
 t/t1460-refs-migrate.sh                      |   2 +-
 t/t1500-rev-parse.sh                         |   6 +-
 t/t1502-rev-parse-parseopt.sh                |   2 +-
 t/t1503-rev-parse-verify.sh                  |  10 +-
 t/t1510-repo-setup.sh                        |  10 +-
 t/t1512-rev-parse-disambiguation.sh          |   4 +-
 t/t1515-rev-parse-outside-repo.sh            |   2 +-
 t/t1800-hook.sh                              |  18 +-
 t/t2004-checkout-cache-temp.sh               |   4 +-
 t/t2019-checkout-ambiguous-ref.sh            |   4 +-
 t/t2024-checkout-dwim.sh                     |   8 +-
 t/t2030-unresolve-info.sh                    |   6 +-
 t/t2060-switch.sh                            |   6 +-
 t/t2070-restore.sh                           |   2 +-
 t/t2080-parallel-checkout-basics.sh          |  14 +-
 t/t2081-parallel-checkout-collisions.sh      |  24 +-
 t/t2082-parallel-checkout-attributes.sh      |  12 +-
 t/t2103-update-index-ignore-missing.sh       |   6 +-
 t/t2200-add-update.sh                        |   2 +-
 t/t2203-add-intent.sh                        |   6 +-
 t/t2400-worktree-add.sh                      |  24 +-
 t/t2402-worktree-list.sh                     |  16 +-
 t/t2403-worktree-move.sh                     |   6 +-
 t/t2405-worktree-submodule.sh                |   6 +-
 t/t2407-worktree-heads.sh                    |  26 +-
 t/t2500-untracked-overwriting.sh             |   8 +-
 t/t2501-cwd-empty.sh                         |   4 +-
 t/t3001-ls-files-others-exclude.sh           |   6 +-
 t/t3007-ls-files-recurse-submodules.sh       |   6 +-
 t/t3200-branch.sh                            |  12 +-
 t/t3202-show-branch.sh                       |  10 +-
 t/t3203-branch-output.sh                     |   4 +-
 t/t3206-range-diff.sh                        |  78 +--
 t/t3207-branch-submodule.sh                  |   4 +-
 t/t3301-notes.sh                             |  32 +-
 t/t3310-notes-merge-manual-resolve.sh        |  16 +-
 t/t3320-notes-merge-worktrees.sh             |   2 +-
 t/t3400-rebase.sh                            |  16 +-
 t/t3402-rebase-merge.sh                      |  16 +-
 t/t3404-rebase-interactive.sh                |  72 +--
 t/t3406-rebase-message.sh                    |   6 +-
 t/t3415-rebase-autosquash.sh                 |  10 +-
 t/t3416-rebase-onto-threedots.sh             |   4 +-
 t/t3418-rebase-continue.sh                   |  10 +-
 t/t3420-rebase-autostash.sh                  |  25 +-
 t/t3422-rebase-incompatible-options.sh       |   4 +-
 t/t3429-rebase-edit-todo.sh                  |   2 +-
 t/t3430-rebase-merges.sh                     |  32 +-
 t/t3500-cherry.sh                            |   4 +-
 t/t3501-revert-cherry-pick.sh                |   6 +-
 t/t3504-cherry-pick-rerere.sh                |   6 +-
 t/t3510-cherry-pick-sequence.sh              |  24 +-
 t/t3602-rm-sparse-checkout.sh                |   4 +-
 t/t3705-add-sparse-checkout.sh               |  10 +-
 t/t3800-mktag.sh                             |   4 +-
 t/t3901-i18n-patch.sh                        |  20 +-
 t/t3903-stash.sh                             |  28 +-
 t/t3904-stash-patch.sh                       |   4 +-
 t/t3908-stash-in-worktree.sh                 |   2 +-
 t/t4000-diff-format.sh                       |   2 +-
 t/t4001-diff-rename.sh                       |   4 +-
 t/t4011-diff-symlink.sh                      |   2 +-
 t/t4013-diff-various.sh                      |   2 +-
 t/t4014-format-patch.sh                      | 344 ++++++------
 t/t4015-diff-whitespace.sh                   |  16 +-
 t/t4017-diff-retval.sh                       |   2 +-
 t/t4018-diff-funcname.sh                     |   2 +-
 t/t4019-diff-wserror.sh                      |   8 +-
 t/t4020-diff-external.sh                     |  18 +-
 t/t4021-format-patch-numbered.sh             |   4 +-
 t/t4022-diff-rewrite.sh                      |  14 +-
 t/t4028-format-patch-mime-headers.sh         |   6 +-
 t/t4031-diff-rewrite-binary.sh               |  18 +-
 t/t4033-diff-patience.sh                     |   2 +-
 t/t4036-format-patch-signer-mime.sh          |   6 +-
 t/t4038-diff-combined.sh                     |   6 +-
 t/t4051-diff-function-context.sh             |  38 +-
 t/t4053-diff-no-index.sh                     |   4 +-
 t/t4063-diff-blobs.sh                        |   2 +-
 t/t4065-diff-anchored.sh                     |  26 +-
 t/t4067-diff-partial-clone.sh                |  12 +-
 t/t4073-diff-stat-name-width.sh              |  24 +-
 t/t4103-apply-binary.sh                      |   2 +-
 t/t4120-apply-popt.sh                        |   2 +-
 t/t4124-apply-ws-rule.sh                     |  10 +-
 t/t4128-apply-root.sh                        |   2 +-
 t/t4140-apply-ita.sh                         |   4 +-
 t/t4141-apply-too-large.sh                   |   2 +-
 t/t4150-am.sh                                |  48 +-
 t/t4200-rerere.sh                            |   6 +-
 t/t4201-shortlog.sh                          |   2 +-
 t/t4202-log.sh                               |  84 +--
 t/t4204-patch-id.sh                          |   2 +-
 t/t4205-log-pretty-formats.sh                |   2 +-
 t/t4209-log-pickaxe.sh                       |  10 +-
 t/t4211-line-log.sh                          |  72 +--
 t/t4216-log-bloom.sh                         |  18 +-
 t/t4252-am-options.sh                        |  22 +-
 t/t4254-am-corrupt.sh                        |   6 +-
 t/t4258-am-quoted-cr.sh                      |   2 +-
 t/t4301-merge-tree-write-tree.sh             |  18 +-
 t/t5000-tar-tree.sh                          |  10 +-
 t/t5004-archive-corner-cases.sh              |   2 +-
 t/t5100-mailinfo.sh                          |   2 +-
 t/t5150-request-pull.sh                      |  18 +-
 t/t5300-pack-object.sh                       |  22 +-
 t/t5302-pack-index.sh                        |   6 +-
 t/t5304-prune.sh                             |   8 +-
 t/t5310-pack-bitmaps.sh                      |  14 +-
 t/t5317-pack-objects-filter-objects.sh       |  12 +-
 t/t5318-commit-graph.sh                      |   8 +-
 t/t5319-multi-pack-index.sh                  |  16 +-
 t/t5324-split-commit-graph.sh                |  10 +-
 t/t5325-reverse-index.sh                     |   2 +-
 t/t5326-multi-pack-bitmaps.sh                |  28 +-
 t/t5328-commit-graph-64bit-time.sh           |   2 +-
 t/t5329-pack-objects-cruft.sh                |   8 +-
 t/t5334-incremental-multi-pack-index.sh      |   2 +-
 t/t5335-compact-multi-pack-index.sh          |   4 +-
 t/t5351-unpack-large-objects.sh              |   2 +-
 t/t5402-post-merge-hook.sh                   |   4 +-
 t/t5403-post-checkout-hook.sh                |   2 +-
 t/t5404-tracking-branches.sh                 |   2 +-
 t/t5406-remote-rejects.sh                    |   2 +-
 t/t5407-post-rewrite-hook.sh                 |   8 +-
 t/t5409-colorize-remote-messages.sh          |  36 +-
 t/t5411/test-0013-bad-protocol.sh            |  14 +-
 t/t5411/test-0014-bad-protocol--porcelain.sh |  14 +-
 t/t5500-fetch-pack.sh                        |  38 +-
 t/t5504-fetch-receive-strict.sh              |  14 +-
 t/t5505-remote.sh                            |  20 +-
 t/t5510-fetch.sh                             |  10 +-
 t/t5512-ls-remote.sh                         |   8 +-
 t/t5514-fetch-multiple.sh                    |   2 +-
 t/t5516-fetch-push.sh                        |  20 +-
 t/t5520-pull.sh                              |   4 +-
 t/t5524-pull-msg.sh                          |   6 +-
 t/t5526-fetch-submodules.sh                  |  16 +-
 t/t5529-push-errors.sh                       |   4 +-
 t/t5530-upload-pack-error.sh                 |  18 +-
 t/t5531-deep-submodule-push.sh               |   2 +-
 t/t5532-fetch-proxy.sh                       |   2 +-
 t/t5533-push-cas.sh                          |  12 +-
 t/t5534-push-signed.sh                       |  22 +-
 t/t5537-fetch-shallow.sh                     |   2 +-
 t/t5538-push-shallow.sh                      |   2 +-
 t/t5539-fetch-http-shallow.sh                |   4 +-
 t/t5541-http-push-smart.sh                   |  32 +-
 t/t5544-pack-objects-hook.sh                 |  12 +-
 t/t5550-http-fetch-dumb.sh                   |   4 +-
 t/t5551-http-fetch-smart.sh                  |  46 +-
 t/t5552-skipping-fetch-negotiator.sh         |   6 +-
 t/t5554-noop-fetch-negotiator.sh             |   4 +-
 t/t5557-http-get.sh                          |   2 +-
 t/t5558-clone-bundle-uri.sh                  |  38 +-
 t/t5562-http-backend-content-length.sh       |   2 +-
 t/t5564-http-proxy.sh                        |  10 +-
 t/t5581-http-curl-verbose.sh                 |   2 +-
 t/t5583-push-branches.sh                     |   8 +-
 t/t5601-clone.sh                             |  28 +-
 t/t5604-clone-reference.sh                   |   8 +-
 t/t5605-clone-local.sh                       |   2 +-
 t/t5606-clone-options.sh                     |   6 +-
 t/t5612-clone-refspec.sh                     |   2 +-
 t/t5616-partial-clone.sh                     |  60 +--
 t/t5619-clone-local-ambiguous-transport.sh   |   2 +-
 t/t5620-backfill.sh                          |  12 +-
 t/t5700-protocol-v1.sh                       |  46 +-
 t/t5701-git-serve.sh                         |  14 +-
 t/t5702-protocol-v2.sh                       | 152 +++---
 t/t5703-upload-pack-ref-in-want.sh           |  22 +-
 t/t5705-session-id-in-capabilities.sh        |  12 +-
 t/t5750-bundle-uri-parse.sh                  |   8 +-
 t/t5801-remote-helpers.sh                    |   4 +-
 t/t5810-proto-disable-local.sh               |   2 +-
 t/t5813-proto-disable-ssh.sh                 |   4 +-
 t/t6000-rev-list-misc.sh                     |  26 +-
 t/t6005-rev-list-count.sh                    |   8 +-
 t/t6006-rev-list-format.sh                   |   4 +-
 t/t6009-rev-list-parent.sh                   |   4 +-
 t/t6020-bundle-misc.sh                       |  12 +-
 t/t6022-rev-list-missing.sh                  |   4 +-
 t/t6030-bisect-porcelain.sh                  | 150 +++---
 t/t6040-tracking-info.sh                     |   2 +-
 t/t6112-rev-list-filters-objects.sh          |  24 +-
 t/t6115-rev-list-du.sh                       |   4 +-
 t/t6120-describe.sh                          |  14 +-
 t/t6200-fmt-merge-msg.sh                     |  82 +--
 t/t6402-merge-rename.sh                      |   4 +-
 t/t6403-merge-file.sh                        |   6 +-
 t/t6404-recursive-merge.sh                   |   2 +-
 t/t6406-merge-attr.sh                        |  20 +-
 t/t6417-merge-ours-theirs.sh                 |  30 +-
 t/t6418-merge-text-auto.sh                   |   2 +-
 t/t6422-merge-rename-corner-cases.sh         |   8 +-
 t/t6423-merge-rename-directories.sh          |  72 +--
 t/t6424-merge-unrelated-index-changes.sh     |   6 +-
 t/t6427-diff3-conflict-markers.sh            |  10 +-
 t/t6432-merge-recursive-space-options.sh     |   4 +-
 t/t6436-merge-overwrite.sh                   |   6 +-
 t/t6437-submodule-merge.sh                   |  12 +-
 t/t6500-gc.sh                                |   8 +-
 t/t6600-test-reach.sh                        |   4 +-
 t/t7001-mv.sh                                |  16 +-
 t/t7002-mv-sparse-checkout.sh                |  38 +-
 t/t7003-filter-branch.sh                     |  16 +-
 t/t7004-tag.sh                               |   2 +-
 t/t7006-pager.sh                             |  16 +-
 t/t7012-skip-worktree-writing.sh             |   6 +-
 t/t7030-verify-tag.sh                        |  52 +-
 t/t7031-verify-tag-signed-ssh.sh             |  46 +-
 t/t7102-reset.sh                             |   2 +-
 t/t7110-reset-merge.sh                       |  40 +-
 t/t7201-co.sh                                |   6 +-
 t/t7300-clean.sh                             |   2 +-
 t/t7301-clean-interactive.sh                 |   2 +-
 t/t7400-submodule-basic.sh                   |  32 +-
 t/t7402-submodule-rebase.sh                  |   2 +-
 t/t7406-submodule-update.sh                  |  26 +-
 t/t7416-submodule-dash-url.sh                |  20 +-
 t/t7417-submodule-path-url.sh                |   2 +-
 t/t7450-bad-git-dotfiles.sh                  |  14 +-
 t/t7501-commit-basic-functionality.sh        |  14 +-
 t/t7502-commit-porcelain.sh                  |   2 +-
 t/t7507-commit-verbose.sh                    |   6 +-
 t/t7508-status.sh                            |   6 +-
 t/t7510-signed-commit.sh                     |  68 +--
 t/t7516-commit-races.sh                      |   4 +-
 t/t7519-status-fsmonitor.sh                  |  14 +-
 t/t7527-builtin-fsmonitor.sh                 |  84 +--
 t/t7528-signed-commit-ssh.sh                 |  68 +--
 t/t7600-merge.sh                             |  10 +-
 t/t7603-merge-reduce-heads.sh                |  20 +-
 t/t7606-merge-custom.sh                      |   2 +-
 t/t7607-merge-state.sh                       |   4 +-
 t/t7610-mergetool.sh                         |  18 +-
 t/t7700-repack.sh                            |  14 +-
 t/t7703-repack-geometric.sh                  |   4 +-
 t/t7704-repack-cruft.sh                      |  12 +-
 t/t7800-difftool.sh                          |  26 +-
 t/t7810-grep.sh                              |  22 +-
 t/t7814-grep-recurse-submodules.sh           |   2 +-
 t/t7900-maintenance.sh                       |  36 +-
 t/t8008-blame-formats.sh                     |   2 +-
 t/t8010-cat-file-filters.sh                  |   2 +-
 t/t8012-blame-colors.sh                      |   2 +-
 t/t9001-send-email.sh                        | 190 +++----
 t/t9003-help-autocorrect.sh                  |   6 +-
 t/t9106-git-svn-commit-diff-clobber.sh       |   2 +-
 t/t9107-git-svn-migrate.sh                   |  30 +-
 t/t9110-git-svn-use-svm-props.sh             |  20 +-
 t/t9111-git-svn-use-svnsync-props.sh         |  18 +-
 t/t9114-git-svn-dcommit-merge.sh             |   6 +-
 t/t9116-git-svn-log.sh                       |   8 +-
 t/t9117-git-svn-init-clone.sh                |  12 +-
 t/t9119-git-svn-info.sh                      |  16 +-
 t/t9122-git-svn-author.sh                    |   8 +-
 t/t9130-git-svn-authors-file.sh              |   8 +-
 t/t9138-git-svn-authors-prog.sh              |  14 +-
 t/t9140-git-svn-reset.sh                     |   4 +-
 t/t9153-git-svn-rewrite-uuid.sh              |   4 +-
 t/t9200-git-cvsexportcommit.sh               |   2 +-
 t/t9210-scalar.sh                            |  34 +-
 t/t9211-scalar-clone.sh                      |  16 +-
 t/t9300-fast-import.sh                       |  10 +-
 t/t9350-fast-export.sh                       |  54 +-
 t/t9351-fast-export-anonymize.sh             |  36 +-
 t/t9400-git-cvsserver-server.sh              |   4 +-
 t/t9501-gitweb-standalone-http-status.sh     |  58 +--
 t/t9502-gitweb-standalone-parse-output.sh    |  38 +-
 t/t9800-git-p4-basic.sh                      |  10 +-
 t/t9801-git-p4-branch.sh                     |  48 +-
 t/t9806-git-p4-options.sh                    |  10 +-
 t/t9807-git-p4-submit.sh                     |   2 +-
 t/t9810-git-p4-rcs.sh                        |   8 +-
 t/t9813-git-p4-preserve-users.sh             |   8 +-
 t/t9814-git-p4-rename.sh                     |   8 +-
 t/t9827-git-p4-change-filetype.sh            |   4 +-
 t/t9832-unshelve.sh                          |   6 +-
 t/t9833-errors.sh                            |   4 +-
 t/t9835-git-p4-metadata-encoding-python2.sh  |  36 +-
 t/t9836-git-p4-metadata-encoding-python3.sh  |  38 +-
 t/t9850-shell.sh                             |   2 +-
 t/t9902-completion.sh                        |  26 +-
 363 files changed, 4067 insertions(+), 3334 deletions(-)
 create mode 100644 t/check-shell-parser.pl
 create mode 100644 t/lib-shell-parser.pl
 create mode 100755 t/lint-style.pl
 create mode 100644 t/lint-style/grep-assert.expect
 create mode 100644 t/lint-style/grep-assert.test
 create mode 100644 t/lint-style/grep-fix.expect
 create mode 100644 t/lint-style/grep-fix.test
 create mode 100644 t/lint-style/grep-missing-file.expect
 create mode 100644 t/lint-style/grep-missing-file.test
 create mode 100644 t/lint-style/grep-negated.expect
 create mode 100644 t/lint-style/grep-negated.test
 create mode 100644 t/lint-style/grep-not-assert.expect
 create mode 100644 t/lint-style/grep-not-assert.test
 create mode 100644 t/lint-style/heredoc.expect
 create mode 100644 t/lint-style/heredoc.test
 create mode 100644 t/lint-style/test-grep-negation-fix.expect
 create mode 100644 t/lint-style/test-grep-negation-fix.test
 create mode 100644 t/lint-style/test-grep-negation.expect
 create mode 100644 t/lint-style/test-grep-negation.test

base-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2135

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/6 t/README: document test_grep helper

From: Michael Montalbo <[email protected]>

test_grep is a wrapper around grep for test assertions that prints the file contents on failure for easier debugging. It also accepts '!' as its first argument for negation, which preserves the diagnostic output that '! test_grep' would suppress.

Despite being widely used (and the preferred replacement for bare grep in assertions), test_grep has no entry in t/README alongside the other documented helpers like test_cmp and test_line_count. Add one.

---
 t/README | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
t/README +21 -0
index adbbd9acf4..c12a1c317a 100644
@@ -1039,6 +1039,27 @@ see test-lib-functions.sh for the full list and their options.
 
    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_path_is_file <path>
    test_path_is_dir <path>
    test_path_is_missing <path>
Signature · 1 line
gitgitgadget
#2 lore raw

PATCH 2/6 t: extract chainlint's parser into shared module

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Move the Lexer, ShellParser, and ScriptParser packages from chainlint.pl into t/lib-shell-parser.pl so they can be reused by other tools. ScriptParser's check_test() is a no-op in the shared module; callers subclass ScriptParser and override it.

chainlint.pl defines TestParser (&&-chain detection) and ChainlintParser (a ScriptParser subclass whose check_test runs TestParser and formats the results). The shared module is loaded via do() for portability with minimal Perl installations.

A subsequent commit introduces lint-style.pl which needs the same shell parser to properly tokenize test scripts. Sharing the parser avoids reimplementing heredoc handling, $(...) nesting, pipe tracking, quoting, and test body extraction.

---
 t/chainlint.pl        | 521 ++----------------------------------------
 t/lib-shell-parser.pl | 517 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 530 insertions(+), 508 deletions(-)
 create mode 100644 t/lib-shell-parser.pl
t/chainlint.pl +13 -508
index f0598e3934..49b7cc6cb8 100755
@@ -23,458 +23,10 @@ my $jobs = -1;
 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 whitepace, 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;
+my $_lib = dirname($0) . "/lib-shell-parser.pl";
+$_lib = "./$_lib" unless $_lib =~ m{^/};
+do $_lib or die "failed to load $_lib: $@$!\n";
 
 # TestParser is a subclass of ShellParser which, beyond parsing shell script
 # code, is also imbued with semantic knowledge of test construction, and checks
@@ -484,7 +36,7 @@ DONE:
 # scripts in which the tests are defined.
 package TestParser;
 
-use base 'ShellParser';
+our @ISA = ('ShellParser');
 
 sub new {
 	my $class = shift @_;
@@ -578,14 +130,12 @@ DONE:
 	$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 is a subclass of ScriptParser which checks each test
+# body for broken &&-chains via TestParser, then formats and collects
+# the results.
+package ChainlintParser;
 
-use base 'ShellParser';
+our @ISA = ('ScriptParser');
 
 sub new {
 	my $class = shift @_;
@@ -595,35 +145,6 @@ sub new {
 	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 format_problem {
 	local $_ = shift;
 	/^AMP$/ && return "missing '&&'";
@@ -635,10 +156,10 @@ sub format_problem {
 
 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,22 +194,6 @@ sub check_test {
 	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.
 package main;
@@ -803,7 +308,7 @@ sub check_script {
 		}
 		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);
t/lib-shell-parser.pl new file +517 -0

517 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 3/6 t: fix Lexer line count for $() inside double-quoted strings

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

scan_dqstring's post-loop newline counter re-counts newlines that were already counted during recursive parsing of $() bodies. This happens because scan_dollar's returned text can contain newlines (from token text of multi-line strings and from \n command separator tokens), and the catch-all counter at the end of scan_dqstring counts all of them again.

Fix this by counting newlines inline as non-special characters are consumed, and removing the post-loop catch-all. Each newline is now counted exactly once: literal newlines at the inline match, line splices at the \<newline> handler, and $() newlines by scan_token during the recursive parse.

This does not affect chainlint's output because chainlint annotates the original body text using byte offsets, not token line numbers. It does matter for tools like lint-style.pl (introduced in a subsequent commit) that use token line numbers to locate and fix specific lines in the original file.

Add check-shell-parser.pl to verify that the Lexer reports correct line numbers after multi-line $() in double-quoted strings.

---
 t/Makefile              |  7 +++--
 t/check-shell-parser.pl | 58 +++++++++++++++++++++++++++++++++++++++++
 t/lib-shell-parser.pl   | 11 +++++---
 3 files changed, 71 insertions(+), 5 deletions(-)
 create mode 100644 t/check-shell-parser.pl
t/Makefile +5 -2
index ab8a5b54aa..25f923fed9 100644
@@ -139,7 +139,7 @@ check-meson:
 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 check-shell-parser
 else
 GIT_TEST_CHAIN_LINT = 0
 endif
@@ -160,6 +160,8 @@ test-lint-executable:
 test-lint-shell-syntax:
 	@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)
 
+check-shell-parser:
+	@'$(PERL_PATH_SQ)' check-shell-parser.pl
 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 +187,8 @@ perf:
 	$(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-shell-parser $(UNIT_TESTS)
 
 .PHONY: libgit-sys-test libgit-rs-test
 libgit-sys-test:
t/check-shell-parser.pl new file +58 -0

58 lines not on this page · raw message

t/lib-shell-parser.pl +8 -3
index 1e521a94f8..fa9b44d6ec 100644
@@ -89,8 +89,14 @@ sub scan_dqstring {
 	my $b = $self->{buff};
 	my $s = '"';
 	while (1) {
-		# slurp up non-special characters
-		$s .= $1 if $$b =~ /\G([^"\$\\]+)/gc;
+		# slurp up non-special characters; count newlines
+		# inline so we don't need a catch-all counter that
+		# would miscount newlines from recursive $() parsing
+		if ($$b =~ /\G([^"\$\\]+)/gc) {
+			my $chunk = $1;
+			$self->{lineno} += () = $chunk =~ /\n/sg;
+			$s .= $chunk;
+		}
 		# handle special characters
 		last unless $$b =~ /\G(.)/sgc;
 		my $c = $1;
@@ -107,7 +113,6 @@ sub scan_dqstring {
 		}
 		die("internal error scanning dq-string '$c'\n");
 	}
-	$self->{lineno} += () = $s =~ /\n/sg;
 	return $s;
 }
 
Signature · 1 line
gitgitgadget
#4 lore raw

PATCH 4/6 t: add lint-style.pl with test_grep negation rule

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Add a mechanical lint checker for test scripts, similar in spirit to check-non-portable-shell.pl but focused on test conventions rather than portability.

The tool defines LintParser, a subclass of ScriptParser (from the shared lib-shell-parser.pl module). ScriptParser's parse_cmd() finds test_expect_success blocks and calls check_test() for each body; LintParser overrides check_test() to run lint rules on the parsed commands. A "# lint-ok" comment suppresses all checks for intentional style violations.

The first rule detects '! test_grep' and replaces it with 'test_grep !'. Shell-level negation suppresses the diagnostic output that test_grep prints on failure; the built-in negation preserves it.

Three violations inside test bodies are converted via --fix. One additional violation in a helper function outside test_expect_success (t7900's test_geometric_repack_needed) is converted manually, since the parser only processes test bodies.

---
 t/.gitattributes                           |   2 +
 t/Makefile                                 |  32 +++-
 t/lint-style.pl                            | 200 +++++++++++++++++++++
 t/lint-style/heredoc.expect                |   3 +
 t/lint-style/heredoc.test                  |  14 ++
 t/lint-style/test-grep-negation-fix.expect |   4 +
 t/lint-style/test-grep-negation-fix.test   |   4 +
 t/lint-style/test-grep-negation.expect     |   3 +
 t/lint-style/test-grep-negation.test       |   4 +
 t/t0031-lockfile-pid.sh                    |   2 +-
 t/t5300-pack-object.sh                     |   2 +-
 t/t5319-multi-pack-index.sh                |   2 +-
 t/t7900-maintenance.sh                     |   2 +-
 13 files changed, 268 insertions(+), 6 deletions(-)
 create mode 100755 t/lint-style.pl
 create mode 100644 t/lint-style/heredoc.expect
 create mode 100644 t/lint-style/heredoc.test
 create mode 100644 t/lint-style/test-grep-negation-fix.expect
 create mode 100644 t/lint-style/test-grep-negation-fix.test
 create mode 100644 t/lint-style/test-grep-negation.expect
 create mode 100644 t/lint-style/test-grep-negation.test
t/.gitattributes +2 -0
index 7664c6e027..aea6889d03 100644
@@ -1,5 +1,7 @@
 t[0-9][0-9][0-9][0-9]/* -whitespace
 /chainlint/*.expect eol=lf -whitespace
+/lint-style/*.expect eol=lf -whitespace
+/lint-style/*.test eol=lf -whitespace
 /t0110/url-* binary
 /t3206/* eol=lf
 /t3900/*.txt eol=lf
t/Makefile +30 -2
index 25f923fed9..3a5fa4ce37 100644
@@ -46,6 +46,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)))
 CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl
+LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))
 UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)
 UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))
 UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)
@@ -139,7 +140,7 @@ check-meson:
 test-lint: test-lint-duplicates test-lint-executable \
 	test-lint-filenames
 ifneq ($(PERL_PATH),)
-test-lint: test-lint-shell-syntax check-shell-parser
+test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser
 else
 GIT_TEST_CHAIN_LINT = 0
 endif
@@ -162,6 +163,32 @@ test-lint-shell-syntax:
 
 check-shell-parser:
 	@'$(PERL_PATH_SQ)' check-shell-parser.pl
+
+test-lint-style:
+	@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)
+
+check-lint-style:
+	@rc=0; for t in $(LINT_STYLE_TESTS); do \
+		base=$${t%.test}; \
+		case $$base in \
+		*-fix) \
+			cp "$$t" "$$t.tmp" && \
+			'$(PERL_PATH_SQ)' lint-style.pl --fix "$$t.tmp" >/dev/null 2>&1; \
+			fix_rc=$$?; \
+			if test $$fix_rc != 0; then \
+				echo "FAIL: $$t (--fix exit code $$fix_rc)"; rc=1; \
+			elif ! diff -u "$$base.expect" "$$t.tmp"; then \
+				echo "FAIL: $$t (--fix output)"; rc=1; \
+			fi; \
+			rm -f "$$t.tmp" ;; \
+		*) \
+			if ! '$(PERL_PATH_SQ)' lint-style.pl "$$t" 2>&1 | \
+				diff -u "$$base.expect" -; then \
+				echo "FAIL: $$t"; rc=1; \
+			fi ;; \
+		esac; \
+	done; test $$rc = 0
+
 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)
@@ -188,7 +215,8 @@ perf:
 
 .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
 	check-chainlint clean-chainlint test-chainlint \
-	check-shell-parser $(UNIT_TESTS)
+	check-shell-parser \
+	check-lint-style test-lint-style $(UNIT_TESTS)
 
 .PHONY: libgit-sys-test libgit-rs-test
 libgit-sys-test:
t/lint-style.pl new file +200 -0

200 lines not on this page · raw message

t/lint-style/heredoc.expect new file +3 -0
new file mode 100644
index 0000000000..7ff6d4a52d
@@ -0,0 +1,3 @@
+lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep "after-heredoc-is-caught" actual
+lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep "not-inside-sed-heredoc" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/heredoc.test new file +14 -0
new file mode 100644
index 0000000000..4c05831cfb
@@ -0,0 +1,14 @@
+test_expect_success 'greps inside heredocs are skipped' '
+	cat <<-EOF &&
+	grep "inside-strip-tabs" file
+	EOF
+	cat <<-\EOF &&
+	grep "inside-no-expand" file
+	EOF
+	! test_grep "after-heredoc-is-caught" actual
+'
+
+test_expect_success 'sed with << does not start a heredoc' '
+	sed "s/<< foo/bar/" file &&
+	! test_grep "not-inside-sed-heredoc" actual
+'
t/lint-style/test-grep-negation-fix.expect new file +4 -0
new file mode 100644
index 0000000000..28ecde1073
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+	test_grep ! "pattern" actual &&
+	test_grep ! -i "insensitive" actual
+'
t/lint-style/test-grep-negation-fix.test new file +4 -0
new file mode 100644
index 0000000000..571c150031
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+	! test_grep "pattern" actual &&
+	! test_grep -i "insensitive" actual
+'
t/lint-style/test-grep-negation.expect new file +3 -0
new file mode 100644
index 0000000000..1fa9e124aa
@@ -0,0 +1,3 @@
+lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep "pattern" actual
+lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i "insensitive" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/test-grep-negation.test new file +4 -0
new file mode 100644
index 0000000000..571c150031
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+	! test_grep "pattern" actual &&
+	! test_grep -i "insensitive" actual
+'
t/t0031-lockfile-pid.sh +1 -1
index 8ef87addf5..e9e2f04049 100755
@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '
 		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
 	)
 '
 
t/t5300-pack-object.sh +1 -1
index 73445782e7..3179b4963e 100755
@@ -720,7 +720,7 @@ test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat
 
 	# --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' '
t/t5319-multi-pack-index.sh +1 -1
index fa0d4046f7..9154d9795f 100755
@@ -1175,7 +1175,7 @@ test_expect_success 'load reverse index when missing .idx, .pack' '
 
 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' '
t/t7900-maintenance.sh +1 -1
index d7f82e1bec..9db4a76f67 100755
@@ -664,7 +664,7 @@ test_geometric_repack_needed () {
 	true)
 		test_grep "\[\"git\",\"repack\"," trace2.txt;;
 	false)
-		! test_grep "\[\"git\",\"repack\"," trace2.txt;;
+		test_grep ! "\[\"git\",\"repack\"," trace2.txt;;
 	*)
 		BUG "invalid parameter: $NEEDED";;
 	esac
Signature · 1 line
gitgitgadget
#5 lore raw

Re: [PATCH 4/6] t: add lint-style.pl with test_grep negation rule

Hi Michael,

This sounds like a neat effort!

One drive-by comment…

Quoted 101 lines · Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Add a mechanical lint checker for test scripts, similar in spirit to check-non-portable-shell.pl but focused on test conventions rather than portability.

The tool defines LintParser, a subclass of ScriptParser (from the shared lib-shell-parser.pl module). ScriptParser's parse_cmd() finds test_expect_success blocks and calls check_test() for each body; LintParser overrides check_test() to run lint rules on the parsed commands. A "# lint-ok" comment suppresses all checks for intentional style violations.

The first rule detects '! test_grep' and replaces it with 'test_grep !'. Shell-level negation suppresses the diagnostic output that test_grep prints on failure; the built-in negation preserves it.

Three violations inside test bodies are converted via --fix. One additional violation in a helper function outside test_expect_success (t7900's test_geometric_repack_needed) is converted manually, since the parser only processes test bodies.

---
 t/.gitattributes                           |   2 +
 t/Makefile                                 |  32 +++-
 t/lint-style.pl                            | 200 +++++++++++++++++++++
 t/lint-style/heredoc.expect                |   3 +
 t/lint-style/heredoc.test                  |  14 ++
 t/lint-style/test-grep-negation-fix.expect |   4 +
 t/lint-style/test-grep-negation-fix.test   |   4 +
 t/lint-style/test-grep-negation.expect     |   3 +
 t/lint-style/test-grep-negation.test       |   4 +
 t/t0031-lockfile-pid.sh                    |   2 +-
 t/t5300-pack-object.sh                     |   2 +-
 t/t5319-multi-pack-index.sh                |   2 +-
 t/t7900-maintenance.sh                     |   2 +-
 13 files changed, 268 insertions(+), 6 deletions(-)
 create mode 100755 t/lint-style.pl
 create mode 100644 t/lint-style/heredoc.expect
 create mode 100644 t/lint-style/heredoc.test
 create mode 100644 t/lint-style/test-grep-negation-fix.expect
 create mode 100644 t/lint-style/test-grep-negation-fix.test
 create mode 100644 t/lint-style/test-grep-negation.expect
 create mode 100644 t/lint-style/test-grep-negation.test
t/.gitattributes +2 -0
index 7664c6e027..aea6889d03 100644
@@ -1,5 +1,7 @@
 t[0-9][0-9][0-9][0-9]/* -whitespace
 /chainlint/*.expect eol=lf -whitespace
+/lint-style/*.expect eol=lf -whitespace
+/lint-style/*.test eol=lf -whitespace
 /t0110/url-* binary
 /t3206/* eol=lf
 /t3900/*.txt eol=lf
t/Makefile +18 -1
index 25f923fed9..3a5fa4ce37 100644
@@ -46,6 +46,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)))
 CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl
+LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))
 UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)
 UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))
 UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)
@@ -139,7 +140,7 @@ check-meson:
 test-lint: test-lint-duplicates test-lint-executable \
        test-lint-filenames
 ifneq ($(PERL_PATH),)
-test-lint: test-lint-shell-syntax check-shell-parser
+test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser
 else
 GIT_TEST_CHAIN_LINT = 0
 endif
@@ -162,6 +163,32 @@ test-lint-shell-syntax:
 
 check-shell-parser:
        @'$(PERL_PATH_SQ)' check-shell-parser.pl
+
+test-lint-style:
+       @'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)
+
+check-lint-style:
+       @rc=0; for t in $(LINT_STYLE_TESTS); do \
+               base=$${t%.test}; \
+               case $$base in \
+               *-fix) \
+                       cp "$$t" "$$t.tmp" && \
+                       '$(PERL_PATH_SQ)' lint-style.pl --fix "$$t.tmp" >/dev/null 2>&1; \
+                       fix_rc=$$?; \
+                       if test $$fix_rc != 0; then \
+                               echo "FAIL: $$t (--fix exit code $$fix_rc)"; rc=1; \
+                       elif ! diff -u "$$base.expect" "$$t.tmp"; then \
+                               echo "FAIL: $$t (--fix output)"; rc=1; \

t/Makefile test-lint-shell-syntax: · line 191

Quoted 10 lines
t/Makefile +10 -0
+                       fi; \
+                       rm -f "$$t.tmp" ;; \
+               *) \
+                       if ! '$(PERL_PATH_SQ)' lint-style.pl "$$t" 2>&1 | \
+                               diff -u "$$base.expect" -; then \
+                               echo "FAIL: $$t"; rc=1; \
+                       fi ;; \
+               esac; \
+       done; test $$rc = 0
+

…I wonder if it would be easier to maintain this recipe as a separate shell script and have make give LINT_STYLE_TESTS and PERL_PATH (w/o SQ? idk) to the script. That's a lot of inline code otherwise!

Quoted 342 lines
t/Makefile +2 -1
 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)
@@ -188,7 +215,8 @@ perf:
 
 .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
        check-chainlint clean-chainlint test-chainlint \
-       check-shell-parser $(UNIT_TESTS)
+       check-shell-parser \
+       check-lint-style test-lint-style $(UNIT_TESTS)
 
 .PHONY: libgit-sys-test libgit-rs-test
 libgit-sys-test:
t/lint-style.pl new file +200 -0
new file mode 100755
index 0000000000..9268577f9b
@@ -0,0 +1,200 @@
+#!/usr/bin/perl
+
+# Check test scripts for style violations that can be detected
+# mechanically, such as using bare 'grep' where test_grep should
+# be used.  Use --fix to automatically apply suggested replacements.
+#
+# Detection uses parsed tokens from the shared shell parser for
+# correct handling of heredocs, $(...), pipes, and quoting.
+# Fixes modify the original file text to preserve formatting.
+
+use strict;
+use warnings;
+use File::Basename;
+# Force LF output so check-lint-style's diff against the
+# pre-committed .expect files works on Windows.
+binmode(STDOUT, ':unix');
+binmode(STDERR, ':unix');
+
+my $fix_mode = 0;
+if (@ARGV && $ARGV[0] eq '--fix') {
+       $fix_mode = 1;
+       shift @ARGV;
+}
+
+# Load the shared shell parser (Lexer, ShellParser, ScriptParser).
+my $_lib = dirname($0) . "/lib-shell-parser.pl";
+$_lib = "./$_lib" unless $_lib =~ m{^/};
+do $_lib or die "$0: failed to load $_lib: $@$!\n";
+
+# LintParser is a subclass of ScriptParser which runs lint rules
+# on each test body.  Per-file state (file name, raw lines, dirty
+# flag) is stored on the instance before calling parse().
+#
+# Subroutines defined below (parse_commands, check_test_grep_negation,
+# etc.) are in package main and called with the main:: prefix.
+# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible
+# across packages since 'package' does not introduce a new scope.
+package LintParser;
+our @ISA = ('ScriptParser');
+
+package main;
+
+my $exit_code = 0;
+my $has_fixable = 0;
+
+sub err {
+       my ($file, $lineno, $line, $msg, %opts) = @_;
+       $line =~ s/^\s+//;
+       $line =~ s/\s+$//;
+       $line =~ s/\s+/ /g;
+       my $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';
+       print "$file:$lineno: $prefix: $msg: $line\n";
+       $exit_code = 1 unless $fix_mode && $opts{fixable};
+}
+
+# Report a lint violation found by a rule.  In --fix mode, apply
+# the regex substitution on the raw line and report success.
+# Otherwise just report.  Returns 1 if the line was modified.
+sub report_violation {
+       my ($file, $cmd, $line_ref, $match, $fix, $from) = @_;
+       my $lineno = $cmd->{lineno};
+       my $display = join(' ', @{$cmd->{tokens}});
+       $has_fixable++;  # count for the "--fix" hint
+       if ($fix_mode) {
+               if ($$line_ref =~ s/$match/$fix/) {
+                       err $file, $lineno, $display,
+                               "replace '$from' with '$fix'",
+                               fixable => 1;
+                       return 1;
+               }
+               err $file, $lineno, $display,
+                       "replace '$from' with '$fix' (could not auto-fix)";
+       } else {
+               err $file, $lineno, $display,
+                       "replace '$from' with '$fix'";
+       }
+       return 0;
+}
+
+# Split a token stream into commands at &&, ||, ;;, and \n.
+sub parse_commands {
+       my ($content) = @_;
+       my $parser = ShellParser->new(\$content);
+       my @all_tokens = $parser->parse();
+
+       my @commands;
+       my @current;
+       my $lineno = 1;
+
+       for (my $ti = 0; $ti < @all_tokens; $ti++) {
+               my $text = $all_tokens[$ti]->[0];
+               if ($text =~ /^(?:&&|\|\||;;|\n)$/) {
+                       if (@current) {
+                               push @commands, {
+                                       tokens => [@current],
+                                       lineno => $lineno,
+                               };
+                               @current = ();
+                       }
+               } else {
+                       $lineno = $all_tokens[$ti]->[3]
+                               if !@current && defined $all_tokens[$ti]->[3];
+                       push @current, $text;
+               }
+       }
+       if (@current) {
+               push @commands, {
+                       tokens => [@current],
+                       lineno => $lineno,
+               };
+       }
+       return @commands;
+}
+
+# --- Rule: '! test_grep' should be 'test_grep !' ---
+# Shell-level negation suppresses test_grep's diagnostic output
+# on failure.  Built-in negation preserves it.
+sub check_test_grep_negation {
+       my ($cmd, $file, $line_ref) = @_;
+       my @tokens = @{$cmd->{tokens}};
+       return unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';
+
+       return report_violation($file, $cmd, $line_ref,
+               qr/!\s*test_grep/, 'test_grep !', '! test_grep');
+}
+
+# Map parsed commands back to raw file lines for --fix.
+# Detection uses parsed tokens (correct handling of quoting,
+# heredocs, pipes) but fixes must modify the original text
+# to preserve formatting.
+package LintParser;
+
+sub check_test {
+       # Called by ScriptParser::parse_cmd for each test_expect_success
+       # or test_expect_failure block.
+       my $self = shift @_;
+       my $title = ScriptParser::unwrap(shift @_);
+
+       # Two test body formats:
+       #   Quoted:  test_expect_success 'title' '..body..'
+       #   Heredoc: test_expect_success 'title' - <<\EOF
+       #              ..body..
+       #            EOF
+       # For quoted, the body token is the quoted string.
+       # For heredoc, the body token is '-' and the actual
+       # code arrives as the next argument from the Lexer.
+       my $body_token = shift @_;
+       my $lineno_base = $body_token->[3] || 1;
+       my $body = ScriptParser::unwrap($body_token);
+
+       if ($body eq '-') {
+               my $herebody = shift @_;
+               if ($herebody) {
+                       $body = $herebody->{content};
+                       $lineno_base = $herebody->{start_line} || 1;
+               }
+       }
+       return unless $body;
+
+       # Map each command back to its file line number.
+       # $lineno_base is where the body starts in the file;
+       # $cmd->{lineno} is relative to the body (starting at 1).
+       my $raw_lines = $self->{raw_lines};
+       for my $cmd (main::parse_commands($body)) {
+               my $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;
+               $cmd->{lineno} = $ln;
+               next unless $ln >= 1 && $ln <= @$raw_lines;
+               next if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;
+
+               if (main::check_test_grep_negation($cmd, $self->{file}, \$raw_lines->[$ln - 1])) {
+                       $self->{dirty} = 1;
+               }
+       }
+}
+
+package main;
+
+for my $file (@ARGV) {
+       # :unix:crlf strips \r on Windows (same as chainlint.pl)
+       open(my $fh, '<:unix:crlf', $file) or die "$0: $file: $!\n";
+       my @raw_lines = <$fh>;
+       close $fh;
+
+       my $parser = LintParser->new(\join('', @raw_lines));
+       $parser->{file} = $file;
+       $parser->{raw_lines} = \@raw_lines;
+       $parser->{dirty} = 0;
+       $parser->parse();
+
+       if ($fix_mode && $parser->{dirty}) {
+               open(my $out, '>', $file) or die "$0: $file: $!\n";
+               print $out @{$parser->{raw_lines}};
+               close $out;
+       }
+}
+
+if ($has_fixable && !$fix_mode) {
+       print "hint: run with --fix to apply the suggested replacements.\n";
+}
+exit $exit_code;
t/lint-style/heredoc.expect new file +3 -0
new file mode 100644
index 0000000000..7ff6d4a52d
@@ -0,0 +1,3 @@
+lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep "after-heredoc-is-caught" actual
+lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep "not-inside-sed-heredoc" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/heredoc.test new file +14 -0
new file mode 100644
index 0000000000..4c05831cfb
@@ -0,0 +1,14 @@
+test_expect_success 'greps inside heredocs are skipped' '
+       cat <<-EOF &&
+       grep "inside-strip-tabs" file
+       EOF
+       cat <<-\EOF &&
+       grep "inside-no-expand" file
+       EOF
+       ! test_grep "after-heredoc-is-caught" actual
+'
+
+test_expect_success 'sed with << does not start a heredoc' '
+       sed "s/<< foo/bar/" file &&
+       ! test_grep "not-inside-sed-heredoc" actual
+'
t/lint-style/test-grep-negation-fix.expect new file +4 -0
new file mode 100644
index 0000000000..28ecde1073
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+       test_grep ! "pattern" actual &&
+       test_grep ! -i "insensitive" actual
+'
t/lint-style/test-grep-negation-fix.test new file +4 -0
new file mode 100644
index 0000000000..571c150031
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+       ! test_grep "pattern" actual &&
+       ! test_grep -i "insensitive" actual
+'
t/lint-style/test-grep-negation.expect new file +3 -0
new file mode 100644
index 0000000000..1fa9e124aa
@@ -0,0 +1,3 @@
+lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep "pattern" actual
+lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i "insensitive" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/test-grep-negation.test new file +4 -0
new file mode 100644
index 0000000000..571c150031
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+       ! test_grep "pattern" actual &&
+       ! test_grep -i "insensitive" actual
+'
t/t0031-lockfile-pid.sh +1 -1
index 8ef87addf5..e9e2f04049 100755
@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '
                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
        )
 '
 
t/t5300-pack-object.sh +1 -1
index 73445782e7..3179b4963e 100755
@@ -720,7 +720,7 @@ test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat
 
        # --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' '
t/t5319-multi-pack-index.sh +1 -1
index fa0d4046f7..9154d9795f 100755
@@ -1175,7 +1175,7 @@ test_expect_success 'load reverse index when missing .idx, .pack' '
 
 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' '
t/t7900-maintenance.sh +1 -2
index d7f82e1bec..9db4a76f67 100755
@@ -664,7 +664,7 @@ test_geometric_repack_needed () {
        true)
                test_grep "\[\"git\",\"repack\"," trace2.txt;;
        false)
-               ! test_grep "\[\"git\",\"repack\"," trace2.txt;;
+               test_grep ! "\[\"git\",\"repack\"," trace2.txt;;
        *)
                BUG "invalid parameter: $NEEDED";;
        esac
--

gitgitgadget

Signature · 1 line
D. Ben Knoble
#6 lore raw

Re: [PATCH 4/6] t: add lint-style.pl with test_grep negation rule

Quoted 116 lines · D. Ben Knoble

Hi Michael,

This sounds like a neat effort!

One drive-by comment…

Quoted 107 lines · Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Add a mechanical lint checker for test scripts, similar in spirit to check-non-portable-shell.pl but focused on test conventions rather than portability.

The tool defines LintParser, a subclass of ScriptParser (from the shared lib-shell-parser.pl module). ScriptParser's parse_cmd() finds test_expect_success blocks and calls check_test() for each body; LintParser overrides check_test() to run lint rules on the parsed commands. A "# lint-ok" comment suppresses all checks for intentional style violations.

The first rule detects '! test_grep' and replaces it with 'test_grep !'. Shell-level negation suppresses the diagnostic output that test_grep prints on failure; the built-in negation preserves it.

Three violations inside test bodies are converted via --fix. One additional violation in a helper function outside test_expect_success (t7900's test_geometric_repack_needed) is converted manually, since the parser only processes test bodies.

---
 t/.gitattributes                           |   2 +
 t/Makefile                                 |  32 +++-
 t/lint-style.pl                            | 200 +++++++++++++++++++++
 t/lint-style/heredoc.expect                |   3 +
 t/lint-style/heredoc.test                  |  14 ++
 t/lint-style/test-grep-negation-fix.expect |   4 +
 t/lint-style/test-grep-negation-fix.test   |   4 +
 t/lint-style/test-grep-negation.expect     |   3 +
 t/lint-style/test-grep-negation.test       |   4 +
 t/t0031-lockfile-pid.sh                    |   2 +-
 t/t5300-pack-object.sh                     |   2 +-
 t/t5319-multi-pack-index.sh                |   2 +-
 t/t7900-maintenance.sh                     |   2 +-
 13 files changed, 268 insertions(+), 6 deletions(-)
 create mode 100755 t/lint-style.pl
 create mode 100644 t/lint-style/heredoc.expect
 create mode 100644 t/lint-style/heredoc.test
 create mode 100644 t/lint-style/test-grep-negation-fix.expect
 create mode 100644 t/lint-style/test-grep-negation-fix.test
 create mode 100644 t/lint-style/test-grep-negation.expect
 create mode 100644 t/lint-style/test-grep-negation.test
t/.gitattributes +2 -0
index 7664c6e027..aea6889d03 100644
@@ -1,5 +1,7 @@
 t[0-9][0-9][0-9][0-9]/* -whitespace
 /chainlint/*.expect eol=lf -whitespace
+/lint-style/*.expect eol=lf -whitespace
+/lint-style/*.test eol=lf -whitespace
 /t0110/url-* binary
 /t3206/* eol=lf
 /t3900/*.txt eol=lf
t/Makefile +24 -1
index 25f923fed9..3a5fa4ce37 100644
@@ -46,6 +46,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)))
 CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl
+LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))
 UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)
 UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))
 UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)
@@ -139,7 +140,7 @@ check-meson:
 test-lint: test-lint-duplicates test-lint-executable \
        test-lint-filenames
 ifneq ($(PERL_PATH),)
-test-lint: test-lint-shell-syntax check-shell-parser
+test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser
 else
 GIT_TEST_CHAIN_LINT = 0
 endif
@@ -162,6 +163,32 @@ test-lint-shell-syntax:
 
 check-shell-parser:
        @'$(PERL_PATH_SQ)' check-shell-parser.pl
+
+test-lint-style:
+       @'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)
+
+check-lint-style:
+       @rc=0; for t in $(LINT_STYLE_TESTS); do \
+               base=$${t%.test}; \
+               case $$base in \
+               *-fix) \
+                       cp "$$t" "$$t.tmp" && \
+                       '$(PERL_PATH_SQ)' lint-style.pl --fix "$$t.tmp" >/dev/null 2>&1; \
+                       fix_rc=$$?; \
+                       if test $$fix_rc != 0; then \
+                               echo "FAIL: $$t (--fix exit code $$fix_rc)"; rc=1; \
+                       elif ! diff -u "$$base.expect" "$$t.tmp"; then \
+                               echo "FAIL: $$t (--fix output)"; rc=1; \
+                       fi; \
+                       rm -f "$$t.tmp" ;; \
+               *) \
+                       if ! '$(PERL_PATH_SQ)' lint-style.pl "$$t" 2>&1 | \
+                               diff -u "$$base.expect" -; then \
+                               echo "FAIL: $$t"; rc=1; \
Quoted 9 lines

t/Makefile test-lint-shell-syntax: · line 191

Quoted 4 lines
t/Makefile +4 -0
+                       fi ;; \
+               esac; \
+       done; test $$rc = 0
+

…I wonder if it would be easier to maintain this recipe as a separate shell script and have make give LINT_STYLE_TESTS and PERL_PATH (w/o SQ? idk) to the script. That's a lot of inline code otherwise!

Yeah that's a good call out, will fix in a follow-up. Thank you for taking a look!

Quoted 346 lines
Quoted 340 lines
t/Makefile +2 -1
 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)
@@ -188,7 +215,8 @@ perf:
 
 .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
        check-chainlint clean-chainlint test-chainlint \
-       check-shell-parser $(UNIT_TESTS)
+       check-shell-parser \
+       check-lint-style test-lint-style $(UNIT_TESTS)
 
 .PHONY: libgit-sys-test libgit-rs-test
 libgit-sys-test:
t/lint-style.pl new file +200 -0
new file mode 100755
index 0000000000..9268577f9b
@@ -0,0 +1,200 @@
+#!/usr/bin/perl
+
+# Check test scripts for style violations that can be detected
+# mechanically, such as using bare 'grep' where test_grep should
+# be used.  Use --fix to automatically apply suggested replacements.
+#
+# Detection uses parsed tokens from the shared shell parser for
+# correct handling of heredocs, $(...), pipes, and quoting.
+# Fixes modify the original file text to preserve formatting.
+
+use strict;
+use warnings;
+use File::Basename;
+# Force LF output so check-lint-style's diff against the
+# pre-committed .expect files works on Windows.
+binmode(STDOUT, ':unix');
+binmode(STDERR, ':unix');
+
+my $fix_mode = 0;
+if (@ARGV && $ARGV[0] eq '--fix') {
+       $fix_mode = 1;
+       shift @ARGV;
+}
+
+# Load the shared shell parser (Lexer, ShellParser, ScriptParser).
+my $_lib = dirname($0) . "/lib-shell-parser.pl";
+$_lib = "./$_lib" unless $_lib =~ m{^/};
+do $_lib or die "$0: failed to load $_lib: $@$!\n";
+
+# LintParser is a subclass of ScriptParser which runs lint rules
+# on each test body.  Per-file state (file name, raw lines, dirty
+# flag) is stored on the instance before calling parse().
+#
+# Subroutines defined below (parse_commands, check_test_grep_negation,
+# etc.) are in package main and called with the main:: prefix.
+# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible
+# across packages since 'package' does not introduce a new scope.
+package LintParser;
+our @ISA = ('ScriptParser');
+
+package main;
+
+my $exit_code = 0;
+my $has_fixable = 0;
+
+sub err {
+       my ($file, $lineno, $line, $msg, %opts) = @_;
+       $line =~ s/^\s+//;
+       $line =~ s/\s+$//;
+       $line =~ s/\s+/ /g;
+       my $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';
+       print "$file:$lineno: $prefix: $msg: $line\n";
+       $exit_code = 1 unless $fix_mode && $opts{fixable};
+}
+
+# Report a lint violation found by a rule.  In --fix mode, apply
+# the regex substitution on the raw line and report success.
+# Otherwise just report.  Returns 1 if the line was modified.
+sub report_violation {
+       my ($file, $cmd, $line_ref, $match, $fix, $from) = @_;
+       my $lineno = $cmd->{lineno};
+       my $display = join(' ', @{$cmd->{tokens}});
+       $has_fixable++;  # count for the "--fix" hint
+       if ($fix_mode) {
+               if ($$line_ref =~ s/$match/$fix/) {
+                       err $file, $lineno, $display,
+                               "replace '$from' with '$fix'",
+                               fixable => 1;
+                       return 1;
+               }
+               err $file, $lineno, $display,
+                       "replace '$from' with '$fix' (could not auto-fix)";
+       } else {
+               err $file, $lineno, $display,
+                       "replace '$from' with '$fix'";
+       }
+       return 0;
+}
+
+# Split a token stream into commands at &&, ||, ;;, and \n.
+sub parse_commands {
+       my ($content) = @_;
+       my $parser = ShellParser->new(\$content);
+       my @all_tokens = $parser->parse();
+
+       my @commands;
+       my @current;
+       my $lineno = 1;
+
+       for (my $ti = 0; $ti < @all_tokens; $ti++) {
+               my $text = $all_tokens[$ti]->[0];
+               if ($text =~ /^(?:&&|\|\||;;|\n)$/) {
+                       if (@current) {
+                               push @commands, {
+                                       tokens => [@current],
+                                       lineno => $lineno,
+                               };
+                               @current = ();
+                       }
+               } else {
+                       $lineno = $all_tokens[$ti]->[3]
+                               if !@current && defined $all_tokens[$ti]->[3];
+                       push @current, $text;
+               }
+       }
+       if (@current) {
+               push @commands, {
+                       tokens => [@current],
+                       lineno => $lineno,
+               };
+       }
+       return @commands;
+}
+
+# --- Rule: '! test_grep' should be 'test_grep !' ---
+# Shell-level negation suppresses test_grep's diagnostic output
+# on failure.  Built-in negation preserves it.
+sub check_test_grep_negation {
+       my ($cmd, $file, $line_ref) = @_;
+       my @tokens = @{$cmd->{tokens}};
+       return unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';
+
+       return report_violation($file, $cmd, $line_ref,
+               qr/!\s*test_grep/, 'test_grep !', '! test_grep');
+}
+
+# Map parsed commands back to raw file lines for --fix.
+# Detection uses parsed tokens (correct handling of quoting,
+# heredocs, pipes) but fixes must modify the original text
+# to preserve formatting.
+package LintParser;
+
+sub check_test {
+       # Called by ScriptParser::parse_cmd for each test_expect_success
+       # or test_expect_failure block.
+       my $self = shift @_;
+       my $title = ScriptParser::unwrap(shift @_);
+
+       # Two test body formats:
+       #   Quoted:  test_expect_success 'title' '..body..'
+       #   Heredoc: test_expect_success 'title' - <<\EOF
+       #              ..body..
+       #            EOF
+       # For quoted, the body token is the quoted string.
+       # For heredoc, the body token is '-' and the actual
+       # code arrives as the next argument from the Lexer.
+       my $body_token = shift @_;
+       my $lineno_base = $body_token->[3] || 1;
+       my $body = ScriptParser::unwrap($body_token);
+
+       if ($body eq '-') {
+               my $herebody = shift @_;
+               if ($herebody) {
+                       $body = $herebody->{content};
+                       $lineno_base = $herebody->{start_line} || 1;
+               }
+       }
+       return unless $body;
+
+       # Map each command back to its file line number.
+       # $lineno_base is where the body starts in the file;
+       # $cmd->{lineno} is relative to the body (starting at 1).
+       my $raw_lines = $self->{raw_lines};
+       for my $cmd (main::parse_commands($body)) {
+               my $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;
+               $cmd->{lineno} = $ln;
+               next unless $ln >= 1 && $ln <= @$raw_lines;
+               next if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;
+
+               if (main::check_test_grep_negation($cmd, $self->{file}, \$raw_lines->[$ln - 1])) {
+                       $self->{dirty} = 1;
+               }
+       }
+}
+
+package main;
+
+for my $file (@ARGV) {
+       # :unix:crlf strips \r on Windows (same as chainlint.pl)
+       open(my $fh, '<:unix:crlf', $file) or die "$0: $file: $!\n";
+       my @raw_lines = <$fh>;
+       close $fh;
+
+       my $parser = LintParser->new(\join('', @raw_lines));
+       $parser->{file} = $file;
+       $parser->{raw_lines} = \@raw_lines;
+       $parser->{dirty} = 0;
+       $parser->parse();
+
+       if ($fix_mode && $parser->{dirty}) {
+               open(my $out, '>', $file) or die "$0: $file: $!\n";
+               print $out @{$parser->{raw_lines}};
+               close $out;
+       }
+}
+
+if ($has_fixable && !$fix_mode) {
+       print "hint: run with --fix to apply the suggested replacements.\n";
+}
+exit $exit_code;
t/lint-style/heredoc.expect new file +3 -0
new file mode 100644
index 0000000000..7ff6d4a52d
@@ -0,0 +1,3 @@
+lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep "after-heredoc-is-caught" actual
+lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep "not-inside-sed-heredoc" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/heredoc.test new file +14 -0
new file mode 100644
index 0000000000..4c05831cfb
@@ -0,0 +1,14 @@
+test_expect_success 'greps inside heredocs are skipped' '
+       cat <<-EOF &&
+       grep "inside-strip-tabs" file
+       EOF
+       cat <<-\EOF &&
+       grep "inside-no-expand" file
+       EOF
+       ! test_grep "after-heredoc-is-caught" actual
+'
+
+test_expect_success 'sed with << does not start a heredoc' '
+       sed "s/<< foo/bar/" file &&
+       ! test_grep "not-inside-sed-heredoc" actual
+'
t/lint-style/test-grep-negation-fix.expect new file +4 -0
new file mode 100644
index 0000000000..28ecde1073
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+       test_grep ! "pattern" actual &&
+       test_grep ! -i "insensitive" actual
+'
t/lint-style/test-grep-negation-fix.test new file +4 -0
new file mode 100644
index 0000000000..571c150031
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+       ! test_grep "pattern" actual &&
+       ! test_grep -i "insensitive" actual
+'
t/lint-style/test-grep-negation.expect new file +3 -0
new file mode 100644
index 0000000000..1fa9e124aa
@@ -0,0 +1,3 @@
+lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep "pattern" actual
+lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i "insensitive" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/test-grep-negation.test new file +4 -0
new file mode 100644
index 0000000000..571c150031
@@ -0,0 +1,4 @@
+test_expect_success 'negated test_grep' '
+       ! test_grep "pattern" actual &&
+       ! test_grep -i "insensitive" actual
+'
t/t0031-lockfile-pid.sh +1 -1
index 8ef87addf5..e9e2f04049 100755
@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '
                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
        )
 '
 
t/t5300-pack-object.sh +1 -1
index 73445782e7..3179b4963e 100755
@@ -720,7 +720,7 @@ test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat
 
        # --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' '
t/t5319-multi-pack-index.sh +1 -1
index fa0d4046f7..9154d9795f 100755
@@ -1175,7 +1175,7 @@ test_expect_success 'load reverse index when missing .idx, .pack' '
 
 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' '
t/t7900-maintenance.sh +1 -2
index d7f82e1bec..9db4a76f67 100755
@@ -664,7 +664,7 @@ test_geometric_repack_needed () {
        true)
                test_grep "\[\"git\",\"repack\"," trace2.txt;;
        false)
-               ! test_grep "\[\"git\",\"repack\"," trace2.txt;;
+               test_grep ! "\[\"git\",\"repack\"," trace2.txt;;
        *)
                BUG "invalid parameter: $NEEDED";;
        esac
--
Quoted 2 lines

gitgitgadget

--
D. Ben Knoble

#7 lore raw

PATCH 5/6 t: fix grep assertions missing file arguments

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Three grep assertions were missing their file arguments, causing them to read from empty stdin instead of the intended file:

- t2402: '! grep ...' should read from 'out', matching the grep on the preceding line.
- t7507: the closing quote is in the wrong place, making the entire 'diff --git actual' a single pattern with no file argument instead of pattern 'diff --git' and file 'actual'.
- t7700: '! grep ...' should read from 'packlist', matching the redirect on the preceding line.

Without file arguments these greps always succeed (empty stdin matches nothing), so the assertions were not actually checking anything. All three tests pass with the corrected file arguments, confirming the intended behavior is sound.

---
 t/t2402-worktree-list.sh  | 2 +-
 t/t7507-commit-verbose.sh | 2 +-
 t/t7700-repack.sh         | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)
t/t2402-worktree-list.sh +1 -1
index e0c6abd2f5..93f92e854a 100755
@@ -144,7 +144,7 @@ test_expect_success '"list" all worktrees with prunable annotation' '
 	rm -rf prunable &&
 	git worktree list >out &&
 	grep "/prunable  *[0-9a-f].* prunable$" out &&
-	! grep "/unprunable  *[0-9a-f].* prunable$"
+	! grep "/unprunable  *[0-9a-f].* prunable$" out
 '
 
 test_expect_success '"list" all worktrees --porcelain with prunable' '
t/t7507-commit-verbose.sh +1 -1
index b53d71c086..acdb6b1455 100755
@@ -163,7 +163,7 @@ done
 
 test_expect_success "status ignores commit.verbose=true" '
 	git -c commit.verbose=true status >actual &&
-	! grep "^diff --git actual"
+	! grep "^diff --git" actual
 '
 
 test_done
t/t7700-repack.sh +1 -1
index 63ef63fc50..c6ff3aed30 100755
@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar
 	rm -f .git/objects/pack/* &&
 	mv pack-* .git/objects/pack/ &&
 	git verify-pack -v -- .git/objects/pack/*.idx >packlist &&
-	! grep "^$coid " &&
+	! grep "^$coid " packlist &&
 	echo >.git/objects/info/alternates &&
 	test_must_fail git show $coid
 '
Signature · 1 line
gitgitgadget
#8 lore raw

PATCH 6/6 t: lint and convert grep assertions to test_grep

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Extend lint-style.pl with a rule that detects bare 'grep' used as a test assertion and converts it to test_grep. test_grep prints the file contents on failure, making test debugging significantly easier.

parse_commands() is extended to split at shell structural tokens ({, }, (, ), |) and keywords (if, then, for, etc.), and each command gains a token_pos index so that rules can scan backward and forward in the token stream for context.

Three new functions implement the grep-assertion rule:

- is_filter_context() scans the surrounding tokens for pipes, control-flow keywords (if/elif/while/until), for-in value lists, and brace groups with output redirects.

- is_grep_assertion() classifies a grep command: convertible assertion (pattern and file present), filter (not an assertion), or missing file argument (flagged as a likely bug).

- check_bare_grep() ties them together and calls
report_violation() with the appropriate fix.

The --fix mode handles:
- Replacing 'grep' with 'test_grep'
- Moving negation from '! grep' to 'test_grep !'
- Stripping the -q flag (test_grep inherently checks match status)

Five files require '# lint-ok' annotations for intentional grep usage that cannot be mechanically converted: t1400 (packed-refs may not exist on reftable), t3901 (piped stdin via case block), t6437 (glob argument breaks test_grep's test -f check), t7450 (file may not exist after failed MINGW clone), and t7527 ($? capture on the next line).

The test-lint-style scope is extended to include sourced test fragments in subdirectories (t5411/*.sh and similar) via a new TSOURCED variable.

Run '--fix' to convert all ~2800 grep assertions across ~340 files in the test suite. test-lib-functions.sh and lib-rebase.sh are excluded from linting since they implement test infrastructure rather than test assertions.

---
 t/Makefile                                   |   4 +-
 t/for-each-ref-tests.sh                      |  12 +-
 t/lib-bitmap.sh                              |  12 +-
 t/lib-bundle-uri-protocol.sh                 |  26 +-
 t/lib-httpd.sh                               |   2 +-
 t/lint-style.pl                              | 289 ++++++++++++++--
 t/lint-style/grep-assert.expect              |  13 +
 t/lint-style/grep-assert.test                |  24 ++
 t/lint-style/grep-fix.expect                 |  16 +
 t/lint-style/grep-fix.test                   |  16 +
 t/lint-style/grep-missing-file.expect        |   4 +
 t/lint-style/grep-missing-file.test          |   6 +
 t/lint-style/grep-negated.expect             |   5 +
 t/lint-style/grep-negated.test               |   9 +
 t/lint-style/grep-not-assert.expect          |   0
 t/lint-style/grep-not-assert.test            |  43 +++
 t/pack-refs-tests.sh                         |   2 +-
 t/show-ref-exists-tests.sh                   |   2 +-
 t/t0000-basic.sh                             |  16 +-
 t/t0001-init.sh                              |  18 +-
 t/t0008-ignores.sh                           |   8 +-
 t/t0009-git-dir-validation.sh                |   6 +-
 t/t0012-help.sh                              |   4 +-
 t/t0013-sha1dc.sh                            |   2 +-
 t/t0017-env-helper.sh                        |   4 +-
 t/t0021-conversion.sh                        |  18 +-
 t/t0029-core-unsetenvvars.sh                 |   4 +-
 t/t0030-stripspace.sh                        |   4 +-
 t/t0040-parse-options.sh                     |  52 +--
 t/t0041-usage.sh                             |   2 +-
 t/t0052-simple-ipc.sh                        |  10 +-
 t/t0061-run-command.sh                       |   2 +-
 t/t0066-dir-iterator.sh                      |   2 +-
 t/t0068-for-each-repo.sh                     |  16 +-
 t/t0070-fundamental.sh                       |   6 +-
 t/t0081-find-pack.sh                         |  12 +-
 t/t0091-bugreport.sh                         |  18 +-
 t/t0092-diagnose.sh                          |  12 +-
 t/t0100-previous.sh                          |   2 +-
 t/t0200-gettext-basic.sh                     |  14 +-
 t/t0203-gettext-setlocale-sanity.sh          |   4 +-
 t/t0204-gettext-reencode-sanity.sh           |   8 +-
 t/t0210-trace2-normal.sh                     |   6 +-
 t/t0211-trace2-perf.sh                       |  80 ++---
 t/t0212-trace2-event.sh                      |   8 +-
 t/t0300-credentials.sh                       |   4 +-
 t/t0410-partial-clone.sh                     |  82 ++---
 t/t0450-txt-doc-vs-help.sh                   |   2 +-
 t/t0500-progress-display.sh                  |  18 +-
 t/t0610-reftable-basics.sh                   |   8 +-
 t/t1004-read-tree-m-u-wf.sh                  |   8 +-
 t/t1006-cat-file.sh                          |  18 +-
 t/t1007-hash-object.sh                       |   8 +-
 t/t1011-read-tree-sparse-checkout.sh         |  10 +-
 t/t1050-large.sh                             |   6 +-
 t/t1091-sparse-checkout-builtin.sh           |  24 +-
 t/t1092-sparse-checkout-compatibility.sh     |  44 +--
 t/t1300-config.sh                            |  16 +-
 t/t1305-config-include.sh                    |   2 +-
 t/t1308-config-set.sh                        |   6 +-
 t/t1400-update-ref.sh                        | 170 ++++-----
 t/t1403-show-ref.sh                          |  18 +-
 t/t1410-reflog.sh                            |   4 +-
 t/t1415-worktree-refs.sh                     |   4 +-
 t/t1430-bad-ref-name.sh                      |  56 +--
 t/t1450-fsck.sh                              |  12 +-
 t/t1451-fsck-buffer.sh                       |   6 +-
 t/t1460-refs-migrate.sh                      |   2 +-
 t/t1500-rev-parse.sh                         |   6 +-
 t/t1502-rev-parse-parseopt.sh                |   2 +-
 t/t1503-rev-parse-verify.sh                  |  10 +-
 t/t1510-repo-setup.sh                        |  10 +-
 t/t1512-rev-parse-disambiguation.sh          |   4 +-
 t/t1515-rev-parse-outside-repo.sh            |   2 +-
 t/t1800-hook.sh                              |  18 +-
 t/t2004-checkout-cache-temp.sh               |   4 +-
 t/t2019-checkout-ambiguous-ref.sh            |   4 +-
 t/t2024-checkout-dwim.sh                     |   8 +-
 t/t2030-unresolve-info.sh                    |   6 +-
 t/t2060-switch.sh                            |   6 +-
 t/t2070-restore.sh                           |   2 +-
 t/t2080-parallel-checkout-basics.sh          |  14 +-
 t/t2081-parallel-checkout-collisions.sh      |  24 +-
 t/t2082-parallel-checkout-attributes.sh      |  12 +-
 t/t2103-update-index-ignore-missing.sh       |   6 +-
 t/t2200-add-update.sh                        |   2 +-
 t/t2203-add-intent.sh                        |   6 +-
 t/t2400-worktree-add.sh                      |  24 +-
 t/t2402-worktree-list.sh                     |  16 +-
 t/t2403-worktree-move.sh                     |   6 +-
 t/t2405-worktree-submodule.sh                |   6 +-
 t/t2407-worktree-heads.sh                    |  26 +-
 t/t2500-untracked-overwriting.sh             |   8 +-
 t/t2501-cwd-empty.sh                         |   4 +-
 t/t3001-ls-files-others-exclude.sh           |   6 +-
 t/t3007-ls-files-recurse-submodules.sh       |   6 +-
 t/t3200-branch.sh                            |  12 +-
 t/t3202-show-branch.sh                       |  10 +-
 t/t3203-branch-output.sh                     |   4 +-
 t/t3206-range-diff.sh                        |  78 ++---
 t/t3207-branch-submodule.sh                  |   4 +-
 t/t3301-notes.sh                             |  32 +-
 t/t3310-notes-merge-manual-resolve.sh        |  16 +-
 t/t3320-notes-merge-worktrees.sh             |   2 +-
 t/t3400-rebase.sh                            |  16 +-
 t/t3402-rebase-merge.sh                      |  16 +-
 t/t3404-rebase-interactive.sh                |  72 ++--
 t/t3406-rebase-message.sh                    |   6 +-
 t/t3415-rebase-autosquash.sh                 |  10 +-
 t/t3416-rebase-onto-threedots.sh             |   4 +-
 t/t3418-rebase-continue.sh                   |  10 +-
 t/t3420-rebase-autostash.sh                  |  25 +-
 t/t3422-rebase-incompatible-options.sh       |   4 +-
 t/t3429-rebase-edit-todo.sh                  |   2 +-
 t/t3430-rebase-merges.sh                     |  32 +-
 t/t3500-cherry.sh                            |   4 +-
 t/t3501-revert-cherry-pick.sh                |   6 +-
 t/t3504-cherry-pick-rerere.sh                |   6 +-
 t/t3510-cherry-pick-sequence.sh              |  24 +-
 t/t3602-rm-sparse-checkout.sh                |   4 +-
 t/t3705-add-sparse-checkout.sh               |  10 +-
 t/t3800-mktag.sh                             |   4 +-
 t/t3901-i18n-patch.sh                        |  20 +-
 t/t3903-stash.sh                             |  28 +-
 t/t3904-stash-patch.sh                       |   4 +-
 t/t3908-stash-in-worktree.sh                 |   2 +-
 t/t4000-diff-format.sh                       |   2 +-
 t/t4001-diff-rename.sh                       |   4 +-
 t/t4011-diff-symlink.sh                      |   2 +-
 t/t4013-diff-various.sh                      |   2 +-
 t/t4014-format-patch.sh                      | 344 +++++++++----------
 t/t4015-diff-whitespace.sh                   |  16 +-
 t/t4017-diff-retval.sh                       |   2 +-
 t/t4018-diff-funcname.sh                     |   2 +-
 t/t4019-diff-wserror.sh                      |   8 +-
 t/t4020-diff-external.sh                     |  18 +-
 t/t4021-format-patch-numbered.sh             |   4 +-
 t/t4022-diff-rewrite.sh                      |  14 +-
 t/t4028-format-patch-mime-headers.sh         |   6 +-
 t/t4031-diff-rewrite-binary.sh               |  18 +-
 t/t4033-diff-patience.sh                     |   2 +-
 t/t4036-format-patch-signer-mime.sh          |   6 +-
 t/t4038-diff-combined.sh                     |   6 +-
 t/t4051-diff-function-context.sh             |  38 +-
 t/t4053-diff-no-index.sh                     |   4 +-
 t/t4063-diff-blobs.sh                        |   2 +-
 t/t4065-diff-anchored.sh                     |  26 +-
 t/t4067-diff-partial-clone.sh                |  12 +-
 t/t4073-diff-stat-name-width.sh              |  24 +-
 t/t4103-apply-binary.sh                      |   2 +-
 t/t4120-apply-popt.sh                        |   2 +-
 t/t4124-apply-ws-rule.sh                     |  10 +-
 t/t4128-apply-root.sh                        |   2 +-
 t/t4140-apply-ita.sh                         |   4 +-
 t/t4141-apply-too-large.sh                   |   2 +-
 t/t4150-am.sh                                |  48 +--
 t/t4200-rerere.sh                            |   6 +-
 t/t4201-shortlog.sh                          |   2 +-
 t/t4202-log.sh                               |  84 ++---
 t/t4204-patch-id.sh                          |   2 +-
 t/t4205-log-pretty-formats.sh                |   2 +-
 t/t4209-log-pickaxe.sh                       |  10 +-
 t/t4211-line-log.sh                          |  72 ++--
 t/t4216-log-bloom.sh                         |  18 +-
 t/t4252-am-options.sh                        |  22 +-
 t/t4254-am-corrupt.sh                        |   6 +-
 t/t4258-am-quoted-cr.sh                      |   2 +-
 t/t4301-merge-tree-write-tree.sh             |  18 +-
 t/t5000-tar-tree.sh                          |  10 +-
 t/t5004-archive-corner-cases.sh              |   2 +-
 t/t5100-mailinfo.sh                          |   2 +-
 t/t5150-request-pull.sh                      |  18 +-
 t/t5300-pack-object.sh                       |  20 +-
 t/t5302-pack-index.sh                        |   6 +-
 t/t5304-prune.sh                             |   8 +-
 t/t5310-pack-bitmaps.sh                      |  14 +-
 t/t5317-pack-objects-filter-objects.sh       |  12 +-
 t/t5318-commit-graph.sh                      |   8 +-
 t/t5319-multi-pack-index.sh                  |  14 +-
 t/t5324-split-commit-graph.sh                |  10 +-
 t/t5325-reverse-index.sh                     |   2 +-
 t/t5326-multi-pack-bitmaps.sh                |  28 +-
 t/t5328-commit-graph-64bit-time.sh           |   2 +-
 t/t5329-pack-objects-cruft.sh                |   8 +-
 t/t5334-incremental-multi-pack-index.sh      |   2 +-
 t/t5335-compact-multi-pack-index.sh          |   4 +-
 t/t5351-unpack-large-objects.sh              |   2 +-
 t/t5402-post-merge-hook.sh                   |   4 +-
 t/t5403-post-checkout-hook.sh                |   2 +-
 t/t5404-tracking-branches.sh                 |   2 +-
 t/t5406-remote-rejects.sh                    |   2 +-
 t/t5407-post-rewrite-hook.sh                 |   8 +-
 t/t5409-colorize-remote-messages.sh          |  36 +-
 t/t5411/test-0013-bad-protocol.sh            |  14 +-
 t/t5411/test-0014-bad-protocol--porcelain.sh |  14 +-
 t/t5500-fetch-pack.sh                        |  38 +-
 t/t5504-fetch-receive-strict.sh              |  14 +-
 t/t5505-remote.sh                            |  20 +-
 t/t5510-fetch.sh                             |  10 +-
 t/t5512-ls-remote.sh                         |   8 +-
 t/t5514-fetch-multiple.sh                    |   2 +-
 t/t5516-fetch-push.sh                        |  20 +-
 t/t5520-pull.sh                              |   4 +-
 t/t5524-pull-msg.sh                          |   6 +-
 t/t5526-fetch-submodules.sh                  |  16 +-
 t/t5529-push-errors.sh                       |   4 +-
 t/t5530-upload-pack-error.sh                 |  18 +-
 t/t5531-deep-submodule-push.sh               |   2 +-
 t/t5532-fetch-proxy.sh                       |   2 +-
 t/t5533-push-cas.sh                          |  12 +-
 t/t5534-push-signed.sh                       |  22 +-
 t/t5537-fetch-shallow.sh                     |   2 +-
 t/t5538-push-shallow.sh                      |   2 +-
 t/t5539-fetch-http-shallow.sh                |   4 +-
 t/t5541-http-push-smart.sh                   |  32 +-
 t/t5544-pack-objects-hook.sh                 |  12 +-
 t/t5550-http-fetch-dumb.sh                   |   4 +-
 t/t5551-http-fetch-smart.sh                  |  46 +--
 t/t5552-skipping-fetch-negotiator.sh         |   6 +-
 t/t5554-noop-fetch-negotiator.sh             |   4 +-
 t/t5557-http-get.sh                          |   2 +-
 t/t5558-clone-bundle-uri.sh                  |  38 +-
 t/t5562-http-backend-content-length.sh       |   2 +-
 t/t5564-http-proxy.sh                        |  10 +-
 t/t5581-http-curl-verbose.sh                 |   2 +-
 t/t5583-push-branches.sh                     |   8 +-
 t/t5601-clone.sh                             |  28 +-
 t/t5604-clone-reference.sh                   |   8 +-
 t/t5605-clone-local.sh                       |   2 +-
 t/t5606-clone-options.sh                     |   6 +-
 t/t5612-clone-refspec.sh                     |   2 +-
 t/t5616-partial-clone.sh                     |  60 ++--
 t/t5619-clone-local-ambiguous-transport.sh   |   2 +-
 t/t5620-backfill.sh                          |  12 +-
 t/t5700-protocol-v1.sh                       |  46 +--
 t/t5701-git-serve.sh                         |  14 +-
 t/t5702-protocol-v2.sh                       | 152 ++++----
 t/t5703-upload-pack-ref-in-want.sh           |  22 +-
 t/t5705-session-id-in-capabilities.sh        |  12 +-
 t/t5750-bundle-uri-parse.sh                  |   8 +-
 t/t5801-remote-helpers.sh                    |   4 +-
 t/t5810-proto-disable-local.sh               |   2 +-
 t/t5813-proto-disable-ssh.sh                 |   4 +-
 t/t6000-rev-list-misc.sh                     |  26 +-
 t/t6005-rev-list-count.sh                    |   8 +-
 t/t6006-rev-list-format.sh                   |   4 +-
 t/t6009-rev-list-parent.sh                   |   4 +-
 t/t6020-bundle-misc.sh                       |  12 +-
 t/t6022-rev-list-missing.sh                  |   4 +-
 t/t6030-bisect-porcelain.sh                  | 150 ++++----
 t/t6040-tracking-info.sh                     |   2 +-
 t/t6112-rev-list-filters-objects.sh          |  24 +-
 t/t6115-rev-list-du.sh                       |   4 +-
 t/t6120-describe.sh                          |  14 +-
 t/t6200-fmt-merge-msg.sh                     |  82 ++---
 t/t6402-merge-rename.sh                      |   4 +-
 t/t6403-merge-file.sh                        |   6 +-
 t/t6404-recursive-merge.sh                   |   2 +-
 t/t6406-merge-attr.sh                        |  20 +-
 t/t6417-merge-ours-theirs.sh                 |  30 +-
 t/t6418-merge-text-auto.sh                   |   2 +-
 t/t6422-merge-rename-corner-cases.sh         |   8 +-
 t/t6423-merge-rename-directories.sh          |  72 ++--
 t/t6424-merge-unrelated-index-changes.sh     |   6 +-
 t/t6427-diff3-conflict-markers.sh            |  10 +-
 t/t6432-merge-recursive-space-options.sh     |   4 +-
 t/t6436-merge-overwrite.sh                   |   6 +-
 t/t6437-submodule-merge.sh                   |  12 +-
 t/t6500-gc.sh                                |   8 +-
 t/t6600-test-reach.sh                        |   4 +-
 t/t7001-mv.sh                                |  16 +-
 t/t7002-mv-sparse-checkout.sh                |  38 +-
 t/t7003-filter-branch.sh                     |  16 +-
 t/t7004-tag.sh                               |   2 +-
 t/t7006-pager.sh                             |  16 +-
 t/t7012-skip-worktree-writing.sh             |   6 +-
 t/t7030-verify-tag.sh                        |  52 +--
 t/t7031-verify-tag-signed-ssh.sh             |  46 +--
 t/t7102-reset.sh                             |   2 +-
 t/t7110-reset-merge.sh                       |  40 +--
 t/t7201-co.sh                                |   6 +-
 t/t7300-clean.sh                             |   2 +-
 t/t7301-clean-interactive.sh                 |   2 +-
 t/t7400-submodule-basic.sh                   |  32 +-
 t/t7402-submodule-rebase.sh                  |   2 +-
 t/t7406-submodule-update.sh                  |  26 +-
 t/t7416-submodule-dash-url.sh                |  20 +-
 t/t7417-submodule-path-url.sh                |   2 +-
 t/t7450-bad-git-dotfiles.sh                  |  14 +-
 t/t7501-commit-basic-functionality.sh        |  14 +-
 t/t7502-commit-porcelain.sh                  |   2 +-
 t/t7507-commit-verbose.sh                    |   6 +-
 t/t7508-status.sh                            |   6 +-
 t/t7510-signed-commit.sh                     |  68 ++--
 t/t7516-commit-races.sh                      |   4 +-
 t/t7519-status-fsmonitor.sh                  |  14 +-
 t/t7527-builtin-fsmonitor.sh                 |  84 ++---
 t/t7528-signed-commit-ssh.sh                 |  68 ++--
 t/t7600-merge.sh                             |  10 +-
 t/t7603-merge-reduce-heads.sh                |  20 +-
 t/t7606-merge-custom.sh                      |   2 +-
 t/t7607-merge-state.sh                       |   4 +-
 t/t7610-mergetool.sh                         |  18 +-
 t/t7700-repack.sh                            |  14 +-
 t/t7703-repack-geometric.sh                  |   4 +-
 t/t7704-repack-cruft.sh                      |  12 +-
 t/t7800-difftool.sh                          |  26 +-
 t/t7810-grep.sh                              |  22 +-
 t/t7814-grep-recurse-submodules.sh           |   2 +-
 t/t7900-maintenance.sh                       |  34 +-
 t/t8008-blame-formats.sh                     |   2 +-
 t/t8010-cat-file-filters.sh                  |   2 +-
 t/t8012-blame-colors.sh                      |   2 +-
 t/t9001-send-email.sh                        | 190 +++++-----
 t/t9003-help-autocorrect.sh                  |   6 +-
 t/t9106-git-svn-commit-diff-clobber.sh       |   2 +-
 t/t9107-git-svn-migrate.sh                   |  30 +-
 t/t9110-git-svn-use-svm-props.sh             |  20 +-
 t/t9111-git-svn-use-svnsync-props.sh         |  18 +-
 t/t9114-git-svn-dcommit-merge.sh             |   6 +-
 t/t9116-git-svn-log.sh                       |   8 +-
 t/t9117-git-svn-init-clone.sh                |  12 +-
 t/t9119-git-svn-info.sh                      |  16 +-
 t/t9122-git-svn-author.sh                    |   8 +-
 t/t9130-git-svn-authors-file.sh              |   8 +-
 t/t9138-git-svn-authors-prog.sh              |  14 +-
 t/t9140-git-svn-reset.sh                     |   4 +-
 t/t9153-git-svn-rewrite-uuid.sh              |   4 +-
 t/t9200-git-cvsexportcommit.sh               |   2 +-
 t/t9210-scalar.sh                            |  34 +-
 t/t9211-scalar-clone.sh                      |  16 +-
 t/t9300-fast-import.sh                       |  10 +-
 t/t9350-fast-export.sh                       |  54 +--
 t/t9351-fast-export-anonymize.sh             |  36 +-
 t/t9400-git-cvsserver-server.sh              |   4 +-
 t/t9501-gitweb-standalone-http-status.sh     |  58 ++--
 t/t9502-gitweb-standalone-parse-output.sh    |  38 +-
 t/t9800-git-p4-basic.sh                      |  10 +-
 t/t9801-git-p4-branch.sh                     |  48 +--
 t/t9806-git-p4-options.sh                    |  10 +-
 t/t9807-git-p4-submit.sh                     |   2 +-
 t/t9810-git-p4-rcs.sh                        |   8 +-
 t/t9813-git-p4-preserve-users.sh             |   8 +-
 t/t9814-git-p4-rename.sh                     |   8 +-
 t/t9827-git-p4-change-filetype.sh            |   4 +-
 t/t9832-unshelve.sh                          |   6 +-
 t/t9833-errors.sh                            |   4 +-
 t/t9835-git-p4-metadata-encoding-python2.sh  |  36 +-
 t/t9836-git-p4-metadata-encoding-python3.sh  |  38 +-
 t/t9850-shell.sh                             |   2 +-
 t/t9902-completion.sh                        |  26 +-
 351 files changed, 3215 insertions(+), 2853 deletions(-)
 create mode 100644 t/lint-style/grep-assert.expect
 create mode 100644 t/lint-style/grep-assert.test
 create mode 100644 t/lint-style/grep-fix.expect
 create mode 100644 t/lint-style/grep-fix.test
 create mode 100644 t/lint-style/grep-missing-file.expect
 create mode 100644 t/lint-style/grep-missing-file.test
 create mode 100644 t/lint-style/grep-negated.expect
 create mode 100644 t/lint-style/grep-negated.test
 create mode 100644 t/lint-style/grep-not-assert.expect
 create mode 100644 t/lint-style/grep-not-assert.test
t/Makefile +3 -1
index 3a5fa4ce37..9492195511 100644
@@ -164,8 +164,10 @@ test-lint-shell-syntax:
 check-shell-parser:
 	@'$(PERL_PATH_SQ)' check-shell-parser.pl
 
+TSOURCED = $(sort $(wildcard t[0-9]*/*.sh))
+
 test-lint-style:
-	@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)
+	@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF) $(TSOURCED)
 
 check-lint-style:
 	@rc=0; for t in $(LINT_STYLE_TESTS); do \
t/for-each-ref-tests.sh +6 -6
index bd2d45c971..6b359d940e 100644
@@ -523,7 +523,7 @@ test_expect_success 'Verify descending sort' '
 
 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
+	test_grep "^usage: ${git_for_each_ref}" actual
 '
 
 cat >expected <<\EOF
@@ -622,7 +622,7 @@ test_expect_success 'Quoting style: tcl' '
 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 @@ test_expect_success "${git_for_each_ref} --stdin: fails if extra args" '
 	>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 "${git_for_each_ref} with nested tags" '
 
 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)"
t/lib-bitmap.sh +6 -6
index 62aa6744a6..2de6646128 100644
@@ -173,7 +173,7 @@ rev_list_tests_head () {
 
 	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 @@ basic_bitmap_tests () {
 		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 @@ test_rev_exists () {
 		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
 	'
 }
 
t/lib-bundle-uri-protocol.sh +13 -13
index de09b6b02e..ecf3284297 100644
@@ -63,9 +63,9 @@ test_expect_success "connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: no
 		>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 @@ test_expect_success "connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: hav
 		>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 @@ test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque
 		>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 @@ test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque
 		>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 @@ test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque
 		>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
t/lib-httpd.sh +1 -1
index fc646447d5..a216e5376f 100644
@@ -275,7 +275,7 @@ test_http_push_nonff () {
 	'
 
 	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' '
t/lint-style.pl +257 -32

374 lines not on this page · raw message

t/lint-style/grep-assert.expect new file +13 -0
new file mode 100644
index 0000000000..93c518ed43
@@ -0,0 +1,13 @@
+lint-style/grep-assert.test:2: error: replace 'grep' with 'test_grep': grep "pattern" actual
+lint-style/grep-assert.test:3: error: replace 'grep' with 'test_grep': grep -E "extended" actual
+lint-style/grep-assert.test:4: error: replace 'grep' with 'test_grep': grep -e "explicit" actual
+lint-style/grep-assert.test:5: error: replace 'grep' with 'test_grep': grep -f patterns.txt actual
+lint-style/grep-assert.test:6: error: replace 'grep' with 'test_grep': grep -Fe "fixed-explicit" actual
+lint-style/grep-assert.test:7: error: replace 'grep' with 'test_grep': grep "^-looks-like-flag" actual
+lint-style/grep-assert.test:8: error: replace 'grep' with 'test_grep': grep -v "inverted" actual
+lint-style/grep-assert.test:9: error: replace 'grep' with 'test_grep': grep -- "-e" actual
+lint-style/grep-assert.test:10: error: replace 'grep' with 'test_grep': grep "with-stderr-redirect" actual 2 > err
+lint-style/grep-assert.test:12: error: replace 'grep' with 'test_grep': grep "after-or" actual
+lint-style/grep-assert.test:17: error: replace 'grep' with 'test_grep': grep "after-pipe" actual
+lint-style/grep-assert.test:22: error: replace 'grep' with 'test_grep': grep "inside-case" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/grep-assert.test new file +24 -0
new file mode 100644
index 0000000000..1567285754
@@ -0,0 +1,24 @@
+test_expect_success 'grep assertions' '
+	grep "pattern" actual &&
+	grep -E "extended" actual &&
+	grep -e "explicit" actual &&
+	grep -f patterns.txt actual &&
+	grep -Fe "fixed-explicit" actual &&
+	grep "^-looks-like-flag" actual &&
+	grep -v "inverted" actual &&
+	grep -- "-e" actual &&
+	grep "with-stderr-redirect" actual 2>err &&
+	cmd ||
+	grep "after-or" actual
+'
+
+test_expect_success 'pipe only suppresses first command' '
+	cmd |
+	grep "piped" && grep "after-pipe" actual
+'
+
+test_expect_success 'case pattern does not hide assertion' '
+	case foo in
+	*) grep "inside-case" actual ;;
+	esac
+'
t/lint-style/grep-fix.expect new file +16 -0
new file mode 100644
index 0000000000..04849634c6
@@ -0,0 +1,16 @@
+test_expect_success 'all fixable forms' '
+	test_grep "pattern" actual &&
+	test_grep -E "extended" actual &&
+	test_grep "quiet" actual &&
+	test_grep -F "combined" actual &&
+	test_grep -i "quiet-insensitive" actual &&
+	test_grep ! "negated" actual &&
+	test_grep ! "neg-quiet" actual &&
+	test_grep ! "shell-neg" actual &&
+	test_grep -e "explicit" actual &&
+	test_grep -Fe "fixed-explicit" actual &&
+	test_grep -f patterns.txt actual &&
+	test_grep -- "-e" actual &&
+	test_grep "continuation" \
+		actual
+'
t/lint-style/grep-fix.test new file +16 -0
new file mode 100644
index 0000000000..a9202fc177
@@ -0,0 +1,16 @@
+test_expect_success 'all fixable forms' '
+	grep "pattern" actual &&
+	grep -E "extended" actual &&
+	grep -q "quiet" actual &&
+	grep -qF "combined" actual &&
+	grep -qi "quiet-insensitive" actual &&
+	! grep "negated" actual &&
+	! grep -q "neg-quiet" actual &&
+	! test_grep "shell-neg" actual &&
+	grep -e "explicit" actual &&
+	grep -Fe "fixed-explicit" actual &&
+	grep -f patterns.txt actual &&
+	grep -- "-e" actual &&
+	grep "continuation" \
+		actual
+'
t/lint-style/grep-missing-file.expect new file +4 -0
new file mode 100644
index 0000000000..8d4dc585b5
@@ -0,0 +1,4 @@
+lint-style/grep-missing-file.test:2: error: grep assertion appears to be missing a file argument: grep "pattern"
+lint-style/grep-missing-file.test:3: error: grep assertion appears to be missing a file argument: ! grep "negated"
+lint-style/grep-missing-file.test:4: error: grep assertion appears to be missing a file argument: grep -e "pattern-flag-no-file"
+lint-style/grep-missing-file.test:5: error: grep assertion appears to be missing a file argument: grep -f patterns.txt
t/lint-style/grep-missing-file.test new file +6 -0
new file mode 100644
index 0000000000..aac0528dfc
@@ -0,0 +1,6 @@
+test_expect_success 'grep missing file argument' '
+	grep "pattern" &&
+	! grep "negated" &&
+	grep -e "pattern-flag-no-file" &&
+	grep -f patterns.txt
+'
t/lint-style/grep-negated.expect new file +5 -0
new file mode 100644
index 0000000000..7029b5544a
@@ -0,0 +1,5 @@
+lint-style/grep-negated.test:2: error: replace '! grep' with 'test_grep !': ! grep "pattern" actual
+lint-style/grep-negated.test:3: error: replace '! grep' with 'test_grep !': ! grep -i "insensitive" actual
+lint-style/grep-negated.test:7: error: replace '! grep -q' with 'test_grep !': ! grep -q "pattern" actual
+lint-style/grep-negated.test:8: error: replace '! grep -qF' with 'test_grep ! -F': ! grep -qF "combined" actual
+hint: run with --fix to apply the suggested replacements.
t/lint-style/grep-negated.test new file +9 -0
new file mode 100644
index 0000000000..862a93b93b
@@ -0,0 +1,9 @@
+test_expect_success 'negated grep' '
+	! grep "pattern" actual &&
+	! grep -i "insensitive" actual
+'
+
+test_expect_success 'negated grep -q' '
+	! grep -q "pattern" actual &&
+	! grep -qF "combined" actual
+'
t/lint-style/grep-not-assert.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/lint-style/grep-not-assert.test new file +43 -0

43 lines not on this page · raw message

t/pack-refs-tests.sh +1 -1
index d76b087b09..a800177446 100644
@@ -195,7 +195,7 @@ test_expect_success 'delete ref while another dangling packed ref' '
 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
 '
 
t/show-ref-exists-tests.sh +1 -1
index 36e8e9df33..1351f690eb 100644
@@ -19,7 +19,7 @@ test_expect_success '--exists with missing reference' '
 
 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' '
t/t0000-basic.sh +8 -8
index 2b63e1c86c..7bd1f4e7b7 100755
@@ -743,7 +743,7 @@ test_expect_success 'subtest: lazy prereqs do not turn off tracing' '
 	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 'subtest: test_atexit is run' '
 
 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_expect_success 'test_oid can look up data for SHA-1' '
 	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_expect_success 'test_oid can look up data for SHA-256' '
 	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_expect_success 'test_bool_env' '
 		# 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 on a failing git command with env' '
 
 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
t/t0001-init.sh +9 -9
index e4d32bb4d2..7dcd1e9d48 100755
@@ -278,9 +278,9 @@ test_expect_success POSIXPERM 'init creates a new deep directory (umask vs. shar
 		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 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage is not allowed wi
 	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 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back
 	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 @@ test_expect_success MINGW 'redirect std handles' '
 		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 @@ test_expect_success '--initial-branch' '
 	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_expect_success 'overridden default main branch name (env)' '
 	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' '
t/t0008-ignores.sh +4 -4
index d77a179bdd..ed95faf327 100755
@@ -790,8 +790,8 @@ test_expect_success 'existing file and directory' '
 	>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 @@ test_expect_success 'existing directory and file' '
 	>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)' '
t/t0009-git-dir-validation.sh +3 -3
index 33d21ed9ea..4cba478e50 100755
@@ -35,7 +35,7 @@ test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '
 		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 @@ test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejecte
 		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 @@ test_expect_success 'setup: .git with garbage content is rejected' '
 		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
 	)
 '
 
t/t0012-help.sh +2 -2
index c33501bdcd..48d128ad04 100755
@@ -131,8 +131,8 @@ test_expect_success 'git help succeeds without git.html' '
 
 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' '
t/t0013-sha1dc.sh +1 -1
index ce3d81227a..3ea3169d92 100755
@@ -16,7 +16,7 @@ fi
 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
t/t0017-env-helper.sh +2 -2
index 32fe848179..da02bce0ef 100755
@@ -88,7 +88,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '
 	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_expect_success 'test-tool env-helper reads config thanks to trace2' '
 		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
t/t0021-conversion.sh +9 -9
index f0d50d769e..50994a6ada 100755
@@ -731,7 +731,7 @@ test_expect_success 'process filter should restart after unexpected write failur
 		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 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '
 
 		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
 	)
 '
 
@@ -953,7 +953,7 @@ test_expect_success 'missing file in delayed checkout' '
 
 	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' '
@@ -974,7 +974,7 @@ test_expect_success 'invalid file in delayed checkout' '
 
 	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'
@@ -1015,7 +1015,7 @@ do
 
 		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
@@ -1053,7 +1053,7 @@ test_expect_success SYMLINKS,CASE_INSENSITIVE_FS \
 		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
 	)
 '
@@ -1144,9 +1144,9 @@ test_expect_success 'delayed checkout correctly reports the number of updated en
 
 		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
 	)
 '
 
t/t0029-core-unsetenvvars.sh +2 -2
index baa1b7e85b..975620b389 100755
@@ -20,10 +20,10 @@ test_expect_success 'core.unsetenvvars works' '
 	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
t/t0030-stripspace.sh +2 -2
index 43155f6bd8..d6e2f63c59 100755
@@ -407,12 +407,12 @@ test_expect_success 'strip comments with changed comment string' '
 
 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' '
t/t0040-parse-options.sh +26 -26

129 lines not on this page · raw message

t/t0041-usage.sh +1 -1
index a0f6f134c7..51af7cc030 100755
@@ -13,7 +13,7 @@ test_expect_success 'setup ' '
 
 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
 '
 
t/t0052-simple-ipc.sh +5 -5
index ff98be31a5..838ccc0e46 100755
@@ -33,19 +33,19 @@ test_expect_success 'servers cannot share the same path' '
 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 "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 "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 "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 'slow response' '
 #
 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 @@ test_expect_success 'stress test threads' '
 		--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
t/t0061-run-command.sh +1 -1
index 60cfe65979..dc9d77b0e1 100755
@@ -97,7 +97,7 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '
 	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' '
t/t0066-dir-iterator.sh +1 -1
index df3e9f5fa5..9fbb41bac5 100755
@@ -127,7 +127,7 @@ test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default
 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
t/t0068-for-each-repo.sh +8 -8
index 80b163ea99..55b37d895a 100755
@@ -21,23 +21,23 @@ test_expect_success 'run based on configured value' '
 
 	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" &&
t/t0070-fundamental.sh +3 -3
index 6b9dcf984b..8f573c2a0e 100755
@@ -10,7 +10,7 @@ Verify wrappers and compatibility functions.
 
 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_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file
 	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 'check for a bug in the regex routines' '
 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' '
t/t0081-find-pack.sh +6 -6
index 26f017422d..ff9d56fbf1 100755
@@ -52,17 +52,17 @@ test_expect_success 'add more packfiles' '
 
 	# 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
 '
t/t0091-bugreport.sh +9 -9
index e38ca7a901..81ed41742c 100755
@@ -40,15 +40,15 @@ test_expect_success 'sanity check "System Info" section' '
 
 	# 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 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '
 	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_expect_success UNZIP '--diagnose creates diagnostics zip archive' '
 	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 @@ test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '
 
 	# 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/"
t/t0092-diagnose.sh +6 -6
index 6cabd6e67b..0bc2b1ed5c 100755
@@ -8,7 +8,7 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '
 	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_expect_success UNZIP 'creates diagnostics zip archive' '
 	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 @@ test_expect_success UNZIP 'counts loose objects' '
 	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 @@ test_expect_success UNZIP '--mode=stats excludes .git dir contents' '
 	# 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 @@ test_expect_success UNZIP '--mode=all includes .git dir contents' '
 	# 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/" &&
t/t0100-previous.sh +1 -1
index dd5d9b4e5e..87a9995ea4 100755
@@ -50,7 +50,7 @@ test_expect_success 'merge @{-1}~1' '
 	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' '
t/t0200-gettext-basic.sh +7 -7
index 8853d8afb9..8db26c1ada 100755
@@ -16,7 +16,7 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '
 '
 
 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' '
 '
 
 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 'sanity: Icelandic locale was compiled' '
 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 <[email protected]>" 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 <[email protected]>" 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' '
t/t0203-gettext-setlocale-sanity.sh +2 -2
index 0ce1f22eff..d8af97de23 100755
@@ -12,7 +12,7 @@ test_expect_success 'git show a ISO-8859-1 commit under C locale' '
 	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 "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_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 l
 	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 "iso-utf8-commit" out
 '
 
 test_done
t/t0204-gettext-reencode-sanity.sh +4 -4
index 28d92bb9b7..48ccddaf7d 100755
@@ -66,22 +66,22 @@ test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' '
 # 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
t/t0210-trace2-normal.sh +3 -3
index 7e1e7af862..beed2b27a5 100755
@@ -333,12 +333,12 @@ test_expect_success 'unsafe URLs are redacted by default' '
 
 	GIT_TRACE2="$(pwd)/trace.normal" \
 		git clone https://user:[email protected]/ 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:[email protected]/ clone2 &&
-	grep "start .* clone https://user:[email protected]" unredacted.normal &&
-	grep "remote.origin.url=https://user:[email protected]" unredacted.normal
+	test_grep "start .* clone https://user:[email protected]" unredacted.normal &&
+	test_grep "remote.origin.url=https://user:[email protected]" unredacted.normal
 '
 
 test_done
t/t0211-trace2-perf.sh +40 -40

159 lines not on this page · raw message

t/t0212-trace2-event.sh +4 -4
index 1211db9f46..f5358a1dd4 100755
@@ -332,7 +332,7 @@ test_expect_success 'unsafe URLs are redacted by default in cmd_start events' '
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 300redact_start git clone https://user:[email protected]/ 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 @@ test_expect_success 'unsafe URLs are redacted by default in child_start events'
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 301redact_child_start git clone https://user:[email protected]/ 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 @@ test_expect_success 'unsafe URLs are redacted by default in exec events' '
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 302redact_exec git clone https://user:[email protected]/ 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 @@ test_expect_success 'unsafe URLs are redacted by default in def_param events' '
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 303redact_def_param url https://user:[email protected]/ &&
-	! grep user:pwd trace.event
+	test_grep ! user:pwd trace.event
 '
 
 test_done
t/t0300-credentials.sh +2 -2
index 64ead1571a..ea11cdb4dc 100755
@@ -1014,7 +1014,7 @@ test_expect_success 'credential config with partial URLs' '
 	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 @@ test_expect_success 'credential config with partial URLs' '
 	do
 		git -c credential.$partial.helper=yep \
 			credential fill <stdin >stdout &&
-		! grep yep stdout ||
+		test_grep ! yep stdout ||
 		return 1
 	done &&
 
t/t0410-partial-clone.sh +41 -41

223 lines not on this page · raw message

t/t0450-txt-doc-vs-help.sh +1 -1
index 822b0d55a5..e7ba967223 100755
@@ -87,7 +87,7 @@ do
 	# -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" '
t/t0500-progress-display.sh +9 -9
index d1a498a216..fe2232cce0 100755
@@ -320,8 +320,8 @@ test_expect_success 'progress generates traces' '
 
 	# 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 @@ test_expect_success 'progress generates traces: start without stop' '
 	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 @@ test_expect_success 'progress generates traces: stop without start' '
 
 	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 @@ test_expect_success 'progress generates traces: start with active progress bar (
 	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
t/t0610-reftable-basics.sh +4 -4
index e19e036898..2f4c5e2a1b 100755
@@ -776,11 +776,11 @@ test_expect_success 'reflog: can delete separate reflog entries' '
 		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 @@ test_expect_success 'reflog: garbage collection deletes reflog entries' '
 		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 &&
t/t1004-read-tree-m-u-wf.sh +4 -4
index 11bf10424f..70ba3eea4d 100755
@@ -142,8 +142,8 @@ test_expect_success '3-way not overwriting local changes (our side)' '
 
 	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 @@ test_expect_success '3-way not overwriting local changes (their side)' '
 
 	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
 
 '
 
t/t1006-cat-file.sh +9 -9
index 8e2c52652c..762c77c351 100755
@@ -696,8 +696,8 @@ test_expect_success '%(deltabase) reports packed delta bases' '
 	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 +826,7 @@ test_expect_success 'cat-file -t and -s on corrupt loose object' '
 		# 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 +1318,37 @@ test_expect_success 'cat-file --batch-all-objects --batch-check ignores replace'
 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='
t/t1007-hash-object.sh +4 -4
index de076293b6..4782a6246a 100755
@@ -202,7 +202,7 @@ done
 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' '
@@ -210,7 +210,7 @@ test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '
 	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' '
@@ -218,7 +218,7 @@ test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '
 	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' '
@@ -229,7 +229,7 @@ test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '
 		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' '
t/t1011-read-tree-sparse-checkout.sh +5 -5
index 742f0fa909..f6a5dbe519 100755
@@ -196,7 +196,7 @@ test_expect_success 'read-tree will not throw away dirty changes, non-sparse' '
 	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 dirty init.t
 '
 
 test_expect_success 'read-tree will not throw away dirty changes, sparse' '
@@ -207,7 +207,7 @@ test_expect_success 'read-tree will not throw away dirty changes, sparse' '
 	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 dirty init.t
 '
 
 test_expect_success 'read-tree updates worktree, dirty case' '
@@ -215,7 +215,7 @@ test_expect_success 'read-tree updates worktree, dirty case' '
 	git checkout -f top &&
 	echo dirty >init.t &&
 	read_tree_u_must_fail -m -u HEAD^ &&
-	grep -q dirty init.t &&
+	test_grep dirty init.t &&
 	rm init.t
 '
 
@@ -224,7 +224,7 @@ test_expect_success 'read-tree removes worktree, dirty case' '
 	git checkout -f top &&
 	echo dirty >added &&
 	read_tree_u_must_succeed -m -u HEAD^ &&
-	grep -q dirty added
+	test_grep dirty added
 '
 
 test_expect_success 'read-tree adds to worktree, absent case' '
@@ -240,7 +240,7 @@ test_expect_success 'read-tree adds to worktree, dirty case' '
 	mkdir sub &&
 	echo dirty >sub/added &&
 	read_tree_u_must_succeed -u -m HEAD^ &&
-	grep -q dirty sub/added
+	test_grep dirty sub/added
 '
 
 test_expect_success 'index removal and worktree narrowing at the same time' '
t/t1050-large.sh +3 -3
index 7d40d08521..d295c265c7 100755
@@ -8,7 +8,7 @@ test_description='adding and checking out large blobs'
 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 --stat' '
 
 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' '
t/t1091-sparse-checkout-builtin.sh +12 -12
index cd0aed9975..74b1761e0c 100755
@@ -129,7 +129,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '
 		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 @@ test_expect_success 'cone mode clears ignored subdirectories' '
 	# 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 @@ test_expect_success 'malformed cone-mode patterns' '
 	# 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 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '
 	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 @@ test_expect_success 'clean' '
 	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_expect_success 'sparse-checkout operations with merge conflicts' '
 		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 &&
t/t1092-sparse-checkout-compatibility.sh +22 -22

135 lines not on this page · raw message

t/t1300-config.sh +8 -8
index 11fc976f3a..b337e25d92 100755
@@ -800,7 +800,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		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
@@ -1615,9 +1615,9 @@ test_expect_success 'git --config-env=key=envvar support' '
 
 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' '
@@ -2048,7 +2048,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	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]
@@ -2068,7 +2068,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	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]
@@ -2599,7 +2599,7 @@ test_expect_success '--type rejects unknown specifiers' '
 
 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
 '
 
@@ -2911,12 +2911,12 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 
 	# 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' '
t/t1305-config-include.sh +1 -1
index f3892578e4..f6115269f9 100755
@@ -353,7 +353,7 @@ test_expect_success 'include cycles are detected' '
 	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' '
t/t1308-config-set.sh +3 -3
index e0e49053f0..de95161a83 100755
@@ -180,7 +180,7 @@ test_expect_success 'find integer if value is non parse-able' '
 
 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 @@ test_expect_success 'proper error on directory "files"' '
 	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_expect_success POSIXPERM,SANITY 'proper error on non-accessible files' '
 	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
 '
t/t1400-update-ref.sh +85 -85

580 lines not on this page · raw message

t/t1403-show-ref.sh +9 -9
index 36c903ca19..7ce5af4ab8 100755

Lines of this file are not shown. View the raw message.

t/t1410-reflog.sh +2 -2
index ce71f9a30a..81de1d40eb 100755

Lines of this file are not shown. View the raw message.

t/t1415-worktree-refs.sh +2 -2
index 51d79bae83..6b2ad04aef 100755

Lines of this file are not shown. View the raw message.

t/t1430-bad-ref-name.sh +28 -28

56 lines not on this page · raw message

t/t1450-fsck.sh +6 -6
index 54e81c2636..47d460a699 100755

Lines of this file are not shown. View the raw message.

t/t1451-fsck-buffer.sh +3 -3
index 3a3d33f405..14151eabbd 100755

Lines of this file are not shown. View the raw message.

t/t1460-refs-migrate.sh +1 -1
index 5246468024..8f42697143 100755

Lines of this file are not shown. View the raw message.

t/t1500-rev-parse.sh +3 -3
index 38067d95f7..4174ca40c3 100755

Lines of this file are not shown. View the raw message.

t/t1502-rev-parse-parseopt.sh +1 -1
index 3962f1d288..2cab204376 100755

Lines of this file are not shown. View the raw message.

t/t1503-rev-parse-verify.sh +5 -5
index 75a708f9ba..87638a4a2c 100755

Lines of this file are not shown. View the raw message.

t/t1510-repo-setup.sh +5 -5
index bbfe05b8e4..d330b8df47 100755

Lines of this file are not shown. View the raw message.

t/t1512-rev-parse-disambiguation.sh +2 -2
index 1a380a4184..0e6b42ec78 100755

Lines of this file are not shown. View the raw message.

t/t1515-rev-parse-outside-repo.sh +1 -1
index 75e89c4b6e..2cb3c7cc9c 100755

Lines of this file are not shown. View the raw message.

t/t1800-hook.sh +9 -9
index 0132e772e4..9aae3ff074 100755

Lines of this file are not shown. View the raw message.

t/t2004-checkout-cache-temp.sh +2 -2
index 0afe0ff7ca..db06c321ed 100755

Lines of this file are not shown. View the raw message.

t/t2019-checkout-ambiguous-ref.sh +2 -2
index 1fcef4be95..0ac35798c2 100755

Lines of this file are not shown. View the raw message.

t/t2024-checkout-dwim.sh +4 -4
index a3b1449ef1..752e7f0e61 100755

Lines of this file are not shown. View the raw message.

t/t2030-unresolve-info.sh +3 -3
index be3fcdde07..9af24b1204 100755

Lines of this file are not shown. View the raw message.

t/t2060-switch.sh +3 -3
index c91c4db936..cec2fe6876 100755

Lines of this file are not shown. View the raw message.

t/t2070-restore.sh +1 -1
index 16d6348b69..2c222fb934 100755

Lines of this file are not shown. View the raw message.

t/t2080-parallel-checkout-basics.sh +7 -7
index 5ffe1a41e2..88a6ec1826 100755

Lines of this file are not shown. View the raw message.

t/t2081-parallel-checkout-collisions.sh +12 -12
index f6fcfc0c1e..db45f5f30f 100755

Lines of this file are not shown. View the raw message.

t/t2082-parallel-checkout-attributes.sh +6 -6
index 79fb11f139..1f43c2a267 100755

Lines of this file are not shown. View the raw message.

t/t2103-update-index-ignore-missing.sh +3 -3
index 6938ecca86..fbfc86b1fa 100755

Lines of this file are not shown. View the raw message.

t/t2200-add-update.sh +1 -1
index 0a96655cfe..bb019d7a7c 100755

Lines of this file are not shown. View the raw message.

t/t2203-add-intent.sh +3 -3
index 44c1936e4d..69fef7e646 100755

Lines of this file are not shown. View the raw message.

t/t2400-worktree-add.sh +12 -12
index 58b4445cc4..87b926728a 100755

Lines of this file are not shown. View the raw message.

t/t2402-worktree-list.sh +8 -8
index 93f92e854a..eeaf800d74 100755

Lines of this file are not shown. View the raw message.

t/t2403-worktree-move.sh +3 -3
index 0bb33e8b1b..69768c1207 100755

Lines of this file are not shown. View the raw message.

t/t2405-worktree-submodule.sh +3 -3
index 11018f37c7..f5c94d490b 100755

Lines of this file are not shown. View the raw message.

t/t2407-worktree-heads.sh +13 -13
index 57c201869f..a6d7e0f83b 100755

Lines of this file are not shown. View the raw message.

t/t2500-untracked-overwriting.sh +4 -4
index 5c0bf4d21f..8e84f29e8d 100755

Lines of this file are not shown. View the raw message.

t/t2501-cwd-empty.sh +2 -2
index be9140bbaa..eb6451d71d 100755

Lines of this file are not shown. View the raw message.

t/t3001-ls-files-others-exclude.sh +3 -3
index 202fb8d9ea..29a0a25b30 100755

Lines of this file are not shown. View the raw message.

t/t3007-ls-files-recurse-submodules.sh +3 -3
index 61771eec83..218964edab 100755

Lines of this file are not shown. View the raw message.

t/t3200-branch.sh +6 -6
index e7829c2c4b..bd839998b1 100755

Lines of this file are not shown. View the raw message.

t/t3202-show-branch.sh +5 -5
index a1139f79e2..f402334936 100755

Lines of this file are not shown. View the raw message.

t/t3203-branch-output.sh +2 -2
index a6bd88a58d..7c44186341 100755

Lines of this file are not shown. View the raw message.

t/t3206-range-diff.sh +39 -39

78 lines not on this page · raw message

t/t3207-branch-submodule.sh +2 -2
index fe72b24716..4afb2d3198 100755

Lines of this file are not shown. View the raw message.

t/t3301-notes.sh +16 -16
index d6c50460d0..18a6000171 100755

Lines of this file are not shown. View the raw message.

t/t3310-notes-merge-manual-resolve.sh +8 -8
index 0bb366fdb8..caa1bf250b 100755

Lines of this file are not shown. View the raw message.

t/t3320-notes-merge-worktrees.sh +1 -1
index 96243b7222..9a2c3ac3f7 100755

Lines of this file are not shown. View the raw message.

t/t3400-rebase.sh +8 -8
index c0c00fbb7b..e62e07b894 100755

Lines of this file are not shown. View the raw message.

t/t3402-rebase-merge.sh +8 -8
index 761de63b6b..41ebcdec9f 100755

Lines of this file are not shown. View the raw message.

t/t3404-rebase-interactive.sh +36 -36

72 lines not on this page · raw message

t/t3406-rebase-message.sh +3 -3
index bc51a9d3a7..7b61188e1f 100755

Lines of this file are not shown. View the raw message.

t/t3415-rebase-autosquash.sh +5 -5
index 5033411a43..07a5a11678 100755

Lines of this file are not shown. View the raw message.

t/t3416-rebase-onto-threedots.sh +2 -2
index ea501f2b42..e7f725b8d0 100755

Lines of this file are not shown. View the raw message.

t/t3418-rebase-continue.sh +5 -5
index f9b8999db5..03e0714864 100755

Lines of this file are not shown. View the raw message.

t/t3420-rebase-autostash.sh +12 -13
index f0bbc476ff..c78052690e 100755

Lines of this file are not shown. View the raw message.

t/t3422-rebase-incompatible-options.sh +2 -2
index b9408f9ba1..d6830b82e0 100755

Lines of this file are not shown. View the raw message.

t/t3429-rebase-edit-todo.sh +1 -1
index abd66f3602..28e4bdd0d9 100755

Lines of this file are not shown. View the raw message.

t/t3430-rebase-merges.sh +16 -16
index 84b2d0e664..9bd61c58b3 100755

Lines of this file are not shown. View the raw message.

t/t3500-cherry.sh +2 -2
index 3e66827d76..03655c8a48 100755

Lines of this file are not shown. View the raw message.

t/t3501-revert-cherry-pick.sh +3 -3
index 8025a28cfd..7291a8fae1 100755

Lines of this file are not shown. View the raw message.

t/t3504-cherry-pick-rerere.sh +3 -3
index 18aeba161c..462973fa91 100755

Lines of this file are not shown. View the raw message.

t/t3510-cherry-pick-sequence.sh +12 -12
index 66ff9db270..5777dff496 100755

Lines of this file are not shown. View the raw message.

t/t3602-rm-sparse-checkout.sh +2 -2
index 02c7acd617..252df28bbf 100755

Lines of this file are not shown. View the raw message.

t/t3705-add-sparse-checkout.sh +5 -5
index 53a4782267..64ad7a2949 100755

Lines of this file are not shown. View the raw message.

t/t3800-mktag.sh +2 -2
index e3cf0ffbe5..d3ddf076c4 100755

Lines of this file are not shown. View the raw message.

t/t3901-i18n-patch.sh +10 -10
index ef7d7e1edc..0f32d98a2d 100755

Lines of this file are not shown. View the raw message.

t/t3903-stash.sh +14 -14
index ecc35aae82..53328b8475 100755

Lines of this file are not shown. View the raw message.

t/t3904-stash-patch.sh +2 -2
index 90a4ff2c10..adc45c7073 100755

Lines of this file are not shown. View the raw message.

t/t3908-stash-in-worktree.sh +1 -1
index 2b2b366ef9..e7ae838bb2 100755

Lines of this file are not shown. View the raw message.

t/t4000-diff-format.sh +1 -1
index 32b14e3a71..bc46c2a2a9 100755

Lines of this file are not shown. View the raw message.

t/t4001-diff-rename.sh +2 -2
index 4f520d600d..ad474100af 100755

Lines of this file are not shown. View the raw message.

t/t4011-diff-symlink.sh +1 -1
index ac837b6c9e..4103bd6989 100755

Lines of this file are not shown. View the raw message.

t/t4013-diff-various.sh +1 -1
index d35695f5b0..b7a382c881 100755

Lines of this file are not shown. View the raw message.

t/t4014-format-patch.sh +172 -172

344 lines not on this page · raw message

t/t4015-diff-whitespace.sh +8 -8
index b691d29479..6e7ba8abf6 100755

Lines of this file are not shown. View the raw message.

t/t4017-diff-retval.sh +1 -1
index c2863c99b7..5ec6f5c612 100755

Lines of this file are not shown. View the raw message.

t/t4018-diff-funcname.sh +1 -1
index e026fac1f4..b538b715f5 100755

Lines of this file are not shown. View the raw message.

t/t4019-diff-wserror.sh +4 -4
index 4001dacee3..939ec8a0b4 100755

Lines of this file are not shown. View the raw message.

t/t4020-diff-external.sh +9 -9
index 7ec5854f74..93d37230c6 100755

Lines of this file are not shown. View the raw message.

t/t4021-format-patch-numbered.sh +2 -2
index 9be65fd444..204851f061 100755

Lines of this file are not shown. View the raw message.

t/t4022-diff-rewrite.sh +7 -7
index 6fed993ea0..b5c1b6868f 100755

Lines of this file are not shown. View the raw message.

t/t4028-format-patch-mime-headers.sh +3 -3
index a06a747926..39344cf761 100755

Lines of this file are not shown. View the raw message.

t/t4031-diff-rewrite-binary.sh +9 -9
index 15e012ccc7..5b31ca02fe 100755

Lines of this file are not shown. View the raw message.

t/t4033-diff-patience.sh +1 -1
index 113304dc59..8002c0f434 100755

Lines of this file are not shown. View the raw message.

t/t4036-format-patch-signer-mime.sh +3 -3
index 98d9713d8b..f57d3dc9fb 100755

Lines of this file are not shown. View the raw message.

t/t4038-diff-combined.sh +3 -3
index 2ce26e585c..e11b711388 100755

Lines of this file are not shown. View the raw message.

t/t4051-diff-function-context.sh +19 -19
index 4838a1df8b..117eaede8a 100755

Lines of this file are not shown. View the raw message.

t/t4053-diff-no-index.sh +2 -2
index 15076dfe0d..8e0394cf5a 100755

Lines of this file are not shown. View the raw message.

t/t4063-diff-blobs.sh +1 -1
index 50fdb5ea52..1318cceb19 100755

Lines of this file are not shown. View the raw message.

t/t4065-diff-anchored.sh +13 -13
index b3f510f040..9f06ac65be 100755

Lines of this file are not shown. View the raw message.

t/t4067-diff-partial-clone.sh +6 -6
index 30813109ac..142abdf74a 100755

Lines of this file are not shown. View the raw message.

t/t4073-diff-stat-name-width.sh +12 -12
index ec5d3c3c1f..2412420645 100755

Lines of this file are not shown. View the raw message.

t/t4103-apply-binary.sh +1 -1
index f2d41e06bc..bdd4ccf9e3 100755

Lines of this file are not shown. View the raw message.

t/t4120-apply-popt.sh +1 -1
index c960fdf622..c7801aec2c 100755

Lines of this file are not shown. View the raw message.

t/t4124-apply-ws-rule.sh +5 -5
index 205d86d05e..4c487fb576 100755

Lines of this file are not shown. View the raw message.

t/t4128-apply-root.sh +1 -1
index 5eba15fa66..c772faea62 100755

Lines of this file are not shown. View the raw message.

t/t4140-apply-ita.sh +2 -2
index 0b11a8aef4..49ebb51564 100755

Lines of this file are not shown. View the raw message.

t/t4141-apply-too-large.sh +1 -1
index eac6f7e151..b114a7adf7 100755

Lines of this file are not shown. View the raw message.

t/t4150-am.sh +24 -24

48 lines not on this page · raw message

t/t4200-rerere.sh +3 -3
index 1717f407c8..5466fb78bc 100755

Lines of this file are not shown. View the raw message.

t/t4201-shortlog.sh +1 -1
index 9f41d56d9a..023fbff546 100755

Lines of this file are not shown. View the raw message.

t/t4202-log.sh +42 -42

84 lines not on this page · raw message

t/t4204-patch-id.sh +1 -1
index 605faea0c7..90e661fe98 100755

Lines of this file are not shown. View the raw message.

t/t4205-log-pretty-formats.sh +1 -1
index 3865f6abc7..4be5c51489 100755

Lines of this file are not shown. View the raw message.

t/t4209-log-pickaxe.sh +5 -5
index 0e2f80a268..802597efc7 100755

Lines of this file are not shown. View the raw message.

t/t4211-line-log.sh +36 -36

72 lines not on this page · raw message

t/t4216-log-bloom.sh +9 -9
index 1064990de3..c66f25b932 100755

Lines of this file are not shown. View the raw message.

t/t4252-am-options.sh +11 -11
index bda8822b3d..c36c7e9017 100755

Lines of this file are not shown. View the raw message.

t/t4254-am-corrupt.sh +3 -3
index 96ddf3c53a..f68b5d3127 100755

Lines of this file are not shown. View the raw message.

t/t4258-am-quoted-cr.sh +1 -1
index 201915b45a..50e81b9330 100755

Lines of this file are not shown. View the raw message.

t/t4301-merge-tree-write-tree.sh +9 -9
index 6e117ee93c..b2684911d1 100755

Lines of this file are not shown. View the raw message.

t/t5000-tar-tree.sh +5 -5
index a8c28533dc..3ad600c793 100755

Lines of this file are not shown. View the raw message.

t/t5004-archive-corner-cases.sh +1 -1
index df513a4269..768b0ff85d 100755

Lines of this file are not shown. View the raw message.

t/t5100-mailinfo.sh +1 -1
index e57e1ae739..e01078abe7 100755

Lines of this file are not shown. View the raw message.

t/t5150-request-pull.sh +9 -9
index 270ce6ea48..67c5dc7e85 100755

Lines of this file are not shown. View the raw message.

t/t5300-pack-object.sh +10 -10
index 3179b4963e..9dabb3615a 100755

Lines of this file are not shown. View the raw message.

t/t5302-pack-index.sh +3 -3
index 9697448cb2..735de1023e 100755

Lines of this file are not shown. View the raw message.

t/t5304-prune.sh +4 -4
index 2be7cd30de..db8e389c2c 100755

Lines of this file are not shown. View the raw message.

t/t5310-pack-bitmaps.sh +7 -7
index efeb71593b..33ccb579c5 100755

Lines of this file are not shown. View the raw message.

t/t5317-pack-objects-filter-objects.sh +6 -6
index dddb79ba62..2948a7cd2a 100755

Lines of this file are not shown. View the raw message.

t/t5318-commit-graph.sh +4 -4
index 1c40f904f8..6cecfbeaa1 100755

Lines of this file are not shown. View the raw message.

t/t5319-multi-pack-index.sh +7 -7
index 9154d9795f..68143cb5b7 100755

Lines of this file are not shown. View the raw message.

t/t5324-split-commit-graph.sh +5 -5
index 49a057cc2e..89c0c4f901 100755

Lines of this file are not shown. View the raw message.

t/t5325-reverse-index.sh +1 -1
index 285c8b4a49..5493791938 100755

Lines of this file are not shown. View the raw message.

t/t5326-multi-pack-bitmaps.sh +14 -14
index 62bd973d92..18464d2fc0 100755

Lines of this file are not shown. View the raw message.

t/t5328-commit-graph-64bit-time.sh +1 -1
index d8891e6a92..4e86aff509 100755

Lines of this file are not shown. View the raw message.

t/t5329-pack-objects-cruft.sh +4 -4
index 25ddda5cf3..12cda06373 100755

Lines of this file are not shown. View the raw message.

t/t5334-incremental-multi-pack-index.sh +1 -1
index 68a103d13d..f0b82b5f65 100755

Lines of this file are not shown. View the raw message.

t/t5335-compact-multi-pack-index.sh +2 -2
index ec1dafe89f..0f75ef2ccc 100755

Lines of this file are not shown. View the raw message.

t/t5351-unpack-large-objects.sh +1 -1
index d76eb4be93..7f621c5331 100755

Lines of this file are not shown. View the raw message.

t/t5402-post-merge-hook.sh +2 -2
index 915af2de95..c77aa56421 100755

Lines of this file are not shown. View the raw message.

t/t5403-post-checkout-hook.sh +1 -1
index cb0300b2d2..39009ce327 100755

Lines of this file are not shown. View the raw message.

t/t5404-tracking-branches.sh +1 -1
index cc07889667..a1661d34f2 100755

Lines of this file are not shown. View the raw message.

t/t5406-remote-rejects.sh +1 -1
index dcbeb42082..bb293b7010 100755

Lines of this file are not shown. View the raw message.

t/t5407-post-rewrite-hook.sh +4 -4
index ad7f8c6f00..ed9896e27a 100755

Lines of this file are not shown. View the raw message.

t/t5409-colorize-remote-messages.sh +18 -18
index 3010913bb1..5da8b0ad64 100755

Lines of this file are not shown. View the raw message.

t/t5411/test-0013-bad-protocol.sh +7 -7
index 8d22e17aee..83e4eb2df6 100644

Lines of this file are not shown. View the raw message.

t/t5411/test-0014-bad-protocol--porcelain.sh +7 -7
index 298a3d1fec..6258e575ff 100644

Lines of this file are not shown. View the raw message.

t/t5500-fetch-pack.sh +19 -19
index 649a615ec9..6c1edf2117 100755

Lines of this file are not shown. View the raw message.

t/t5504-fetch-receive-strict.sh +7 -7
index 438250c75e..75b2b87999 100755

Lines of this file are not shown. View the raw message.

t/t5505-remote.sh +10 -10
index e592c0bcde..6f5e86dede 100755

Lines of this file are not shown. View the raw message.

t/t5510-fetch.sh +5 -5
index eca9a973b5..a0bbbce0f7 100755

Lines of this file are not shown. View the raw message.

t/t5512-ls-remote.sh +4 -4
index 5930f55186..fe44f39871 100755

Lines of this file are not shown. View the raw message.

t/t5514-fetch-multiple.sh +1 -1
index 523aff6268..5d79472c39 100755

Lines of this file are not shown. View the raw message.

t/t5516-fetch-push.sh +10 -10
index 4d1672de1e..49d01f74d5 100755

Lines of this file are not shown. View the raw message.

t/t5520-pull.sh +2 -2
index 0e0019347e..4ae3b4fd59 100755

Lines of this file are not shown. View the raw message.

t/t5524-pull-msg.sh +3 -3
index b2be3605f5..493156a2b8 100755

Lines of this file are not shown. View the raw message.

t/t5526-fetch-submodules.sh +8 -8
index 1242ee9185..7b3b7359da 100755

Lines of this file are not shown. View the raw message.

t/t5529-push-errors.sh +2 -2
index 80b06a0cd2..35b1f212a8 100755

Lines of this file are not shown. View the raw message.

t/t5530-upload-pack-error.sh +9 -9
index d40292cfb7..25d409fc94 100755

Lines of this file are not shown. View the raw message.

t/t5531-deep-submodule-push.sh +1 -1
index 05debd1134..7d239dd31f 100755

Lines of this file are not shown. View the raw message.

t/t5532-fetch-proxy.sh +1 -1
index 95d0f33b29..ccbca7c2f2 100755

Lines of this file are not shown. View the raw message.

t/t5533-push-cas.sh +6 -6
index cba26a872d..2e14aecaa2 100755

Lines of this file are not shown. View the raw message.

t/t5534-push-signed.sh +11 -11
index 2a782214ee..21f0262cbd 100755

Lines of this file are not shown. View the raw message.

t/t5537-fetch-shallow.sh +1 -1
index 9982dd2aa6..f323ceebd2 100755

Lines of this file are not shown. View the raw message.

t/t5538-push-shallow.sh +1 -1
index dc0e972943..afab456b32 100755

Lines of this file are not shown. View the raw message.

t/t5539-fetch-http-shallow.sh +2 -2
index 3ea75d34ca..44e6f609df 100755

Lines of this file are not shown. View the raw message.

t/t5541-http-push-smart.sh +16 -16
index 538b603f03..8c34833027 100755

Lines of this file are not shown. View the raw message.

t/t5544-pack-objects-hook.sh +6 -6
index 89147a052e..908d3b8ebe 100755

Lines of this file are not shown. View the raw message.

t/t5550-http-fetch-dumb.sh +2 -2
index b0080bf204..f00eeae48f 100755

Lines of this file are not shown. View the raw message.

t/t5551-http-fetch-smart.sh +23 -23

46 lines not on this page · raw message

t/t5552-skipping-fetch-negotiator.sh +3 -3
index eeddb85b1d..96e224168e 100755

Lines of this file are not shown. View the raw message.

t/t5554-noop-fetch-negotiator.sh +2 -2
index 17e73b606d..d50ffe55b6 100755

Lines of this file are not shown. View the raw message.

t/t5557-http-get.sh +1 -1
index 67fcc23f11..e8ffe3f8e9 100755

Lines of this file are not shown. View the raw message.

t/t5558-clone-bundle-uri.sh +19 -19
index 7a0943bd36..0d5be0d56e 100755

Lines of this file are not shown. View the raw message.

t/t5562-http-backend-content-length.sh +1 -1
index b6ee06f5c8..59c6a50dd2 100755

Lines of this file are not shown. View the raw message.

t/t5564-http-proxy.sh +5 -5
index 817cd1f984..866e2b4e93 100755

Lines of this file are not shown. View the raw message.

t/t5581-http-curl-verbose.sh +1 -1
index cded79c16b..6788cf8171 100755

Lines of this file are not shown. View the raw message.

t/t5583-push-branches.sh +4 -4
index e7e1b6dab6..1ccfaee75a 100755

Lines of this file are not shown. View the raw message.

t/t5601-clone.sh +14 -14
index 3dd229c186..b6167582a1 100755

Lines of this file are not shown. View the raw message.

t/t5604-clone-reference.sh +4 -4
index c232ab8c15..39a0c318df 100755

Lines of this file are not shown. View the raw message.

t/t5605-clone-local.sh +1 -1
index 2397f8fa61..156362f145 100755

Lines of this file are not shown. View the raw message.

t/t5606-clone-options.sh +3 -3
index 8a15237736..1b48da496b 100755

Lines of this file are not shown. View the raw message.

t/t5612-clone-refspec.sh +1 -1
index 3126cfd7e9..dd1fe0abb4 100755

Lines of this file are not shown. View the raw message.

t/t5616-partial-clone.sh +30 -30

60 lines not on this page · raw message

t/t5619-clone-local-ambiguous-transport.sh +1 -1
index cce62bf78d..e4218e5c7f 100755

Lines of this file are not shown. View the raw message.

t/t5620-backfill.sh +6 -6
index d2ea68e065..7462280470 100755

Lines of this file are not shown. View the raw message.

t/t5700-protocol-v1.sh +23 -23

46 lines not on this page · raw message

t/t5701-git-serve.sh +7 -7
index d4c28bae39..108eb30945 100755

Lines of this file are not shown. View the raw message.

t/t5702-protocol-v2.sh +76 -76

152 lines not on this page · raw message

t/t5703-upload-pack-ref-in-want.sh +11 -11
index 249137b467..330d049b2c 100755

Lines of this file are not shown. View the raw message.

t/t5705-session-id-in-capabilities.sh +6 -6
index ed38c76c29..ef0db57d1f 100755

Lines of this file are not shown. View the raw message.

t/t5750-bundle-uri-parse.sh +4 -4
index 294f9d9c64..70ae94c4d0 100755

Lines of this file are not shown. View the raw message.

t/t5801-remote-helpers.sh +2 -2
index d21877150e..5fafc857b8 100755

Lines of this file are not shown. View the raw message.

t/t5810-proto-disable-local.sh +1 -1
index 96a2c46e7a..cb3b13f8d3 100755

Lines of this file are not shown. View the raw message.

t/t5813-proto-disable-ssh.sh +2 -2
index 045e2fe6ce..1800d27f89 100755

Lines of this file are not shown. View the raw message.

t/t6000-rev-list-misc.sh +13 -13
index a95ba576fa..ce968f685e 100755

Lines of this file are not shown. View the raw message.

t/t6005-rev-list-count.sh +4 -4
index 6cde997e13..0bef5838df 100755

Lines of this file are not shown. View the raw message.

t/t6006-rev-list-format.sh +2 -2
index 581984467d..d2648e1619 100755

Lines of this file are not shown. View the raw message.

t/t6009-rev-list-parent.sh +2 -2
index 9c9a8459af..fd6ee2eff8 100755

Lines of this file are not shown. View the raw message.

t/t6020-bundle-misc.sh +6 -6
index 500c81b8a1..939d4214f4 100755

Lines of this file are not shown. View the raw message.

t/t6022-rev-list-missing.sh +2 -2
index 08e92dd002..1e472a45af 100755

Lines of this file are not shown. View the raw message.

t/t6030-bisect-porcelain.sh +75 -75

150 lines not on this page · raw message

t/t6040-tracking-info.sh +1 -1
index 0242b5bf7a..369f810d85 100755

Lines of this file are not shown. View the raw message.

t/t6112-rev-list-filters-objects.sh +12 -12
index e0a825bccf..a1c244c32d 100755

Lines of this file are not shown. View the raw message.

t/t6115-rev-list-du.sh +2 -2
index 04c577dad6..b9650b80a4 100755

Lines of this file are not shown. View the raw message.

t/t6120-describe.sh +7 -7
index 8ee3d2c37d..12b4fb307b 100755

Lines of this file are not shown. View the raw message.

t/t6200-fmt-merge-msg.sh +41 -41

82 lines not on this page · raw message

t/t6402-merge-rename.sh +2 -2
index ff00b74e9c..fb1595ae53 100755

Lines of this file are not shown. View the raw message.

t/t6403-merge-file.sh +3 -3
index 801284cf8f..dcc5c2ac99 100755

Lines of this file are not shown. View the raw message.

t/t6404-recursive-merge.sh +1 -1
index 346f3608b9..ce420fe1ac 100755

Lines of this file are not shown. View the raw message.

t/t6406-merge-attr.sh +10 -10
index 8f6fbef002..7f49c5908a 100755

Lines of this file are not shown. View the raw message.

t/t6417-merge-ours-theirs.sh +15 -15
index 62d1406119..9486251399 100755

Lines of this file are not shown. View the raw message.

t/t6418-merge-text-auto.sh +1 -1
index 41288a60ce..c86a1bedd5 100755

Lines of this file are not shown. View the raw message.

t/t6422-merge-rename-corner-cases.sh +4 -4
index e18d5a227d..d1f1de14e7 100755

Lines of this file are not shown. View the raw message.

t/t6423-merge-rename-directories.sh +36 -36

72 lines not on this page · raw message

t/t6424-merge-unrelated-index-changes.sh +3 -3
index ddc7524f6c..12e58bc335 100755

Lines of this file are not shown. View the raw message.

t/t6427-diff3-conflict-markers.sh +5 -5
index 57569c4f4b..818550bc95 100755

Lines of this file are not shown. View the raw message.

t/t6432-merge-recursive-space-options.sh +2 -2
index db4b77e63d..110b48cb67 100755

Lines of this file are not shown. View the raw message.

t/t6436-merge-overwrite.sh +3 -3
index 70b5d2d694..5e39a15fac 100755

Lines of this file are not shown. View the raw message.

t/t6437-submodule-merge.sh +6 -6
index a564758f52..0c5264c5e3 100755

Lines of this file are not shown. View the raw message.

t/t6500-gc.sh +4 -4
index ea9aaad470..3ee4b18d88 100755

Lines of this file are not shown. View the raw message.

t/t6600-test-reach.sh +2 -2
index b5b314e570..ee46d66b84 100755

Lines of this file are not shown. View the raw message.

t/t7001-mv.sh +8 -8
index 920479e925..7cf4aa5ba1 100755

Lines of this file are not shown. View the raw message.

t/t7002-mv-sparse-checkout.sh +19 -19
index 4d3f221224..9c0e82ba31 100755

Lines of this file are not shown. View the raw message.

t/t7003-filter-branch.sh +8 -8
index c475769858..86011e7b1f 100755

Lines of this file are not shown. View the raw message.

t/t7004-tag.sh +1 -1
index d918005dd9..2ad30040df 100755

Lines of this file are not shown. View the raw message.

t/t7006-pager.sh +8 -8
index 9717e825f0..75ca7a25da 100755

Lines of this file are not shown. View the raw message.

t/t7012-skip-worktree-writing.sh +3 -3
index cd5c20fe51..f9fee199e2 100755

Lines of this file are not shown. View the raw message.

t/t7030-verify-tag.sh +26 -26

52 lines not on this page · raw message

t/t7031-verify-tag-signed-ssh.sh +23 -23

46 lines not on this page · raw message

t/t7102-reset.sh +1 -1
index 0503a64d3f..298272cb13 100755

Lines of this file are not shown. View the raw message.

t/t7110-reset-merge.sh +20 -20
index 9a335071af..a241ecfc0b 100755

Lines of this file are not shown. View the raw message.

t/t7201-co.sh +3 -3
index 7613b1d2a4..0ddd1ad7aa 100755

Lines of this file are not shown. View the raw message.

t/t7300-clean.sh +1 -1
index 00d4070156..13ad3eec77 100755

Lines of this file are not shown. View the raw message.

t/t7301-clean-interactive.sh +1 -1
index f743e5b8f4..de59ba3724 100755

Lines of this file are not shown. View the raw message.

t/t7400-submodule-basic.sh +16 -16
index 65fcfae93a..eefdecb0bd 100755

Lines of this file are not shown. View the raw message.

t/t7402-submodule-rebase.sh +1 -1
index 06cee3432f..872eb2d3ab 100755

Lines of this file are not shown. View the raw message.

t/t7406-submodule-update.sh +13 -13
index 6abb00876a..9554720152 100755

Lines of this file are not shown. View the raw message.

t/t7416-submodule-dash-url.sh +10 -10
index 3d944a00e0..9f4af88ed5 100755

Lines of this file are not shown. View the raw message.

t/t7417-submodule-path-url.sh +1 -1
index 5e3051da8b..7a6f61585b 100755

Lines of this file are not shown. View the raw message.

t/t7450-bad-git-dotfiles.sh +7 -7
index 8cc86522b2..2a4d7f2d47 100755

Lines of this file are not shown. View the raw message.

t/t7501-commit-basic-functionality.sh +7 -7
index a37509f004..88b4468319 100755

Lines of this file are not shown. View the raw message.

t/t7502-commit-porcelain.sh +1 -1
index 05f6da4ad9..62e3970e3f 100755

Lines of this file are not shown. View the raw message.

t/t7507-commit-verbose.sh +3 -3
index acdb6b1455..8e9d8ff3ae 100755

Lines of this file are not shown. View the raw message.

t/t7508-status.sh +3 -3
index c2057bc94c..c37ffbe444 100755

Lines of this file are not shown. View the raw message.

t/t7510-signed-commit.sh +34 -34

68 lines not on this page · raw message

t/t7516-commit-races.sh +2 -2
index de7c4ca790..29669f64ae 100755

Lines of this file are not shown. View the raw message.

t/t7519-status-fsmonitor.sh +7 -7
index 7ee69ecdd4..93973ed25a 100755

Lines of this file are not shown. View the raw message.

t/t7527-builtin-fsmonitor.sh +42 -42

84 lines not on this page · raw message

t/t7528-signed-commit-ssh.sh +34 -34

68 lines not on this page · raw message

t/t7600-merge.sh +5 -5
index f877d9a433..a9c3c3920d 100755

Lines of this file are not shown. View the raw message.

t/t7603-merge-reduce-heads.sh +10 -10
index 1f8c3b7ccb..92007dc501 100755

Lines of this file are not shown. View the raw message.

t/t7606-merge-custom.sh +1 -1
index 81fb7c474c..0ae2f59b1d 100755

Lines of this file are not shown. View the raw message.

t/t7607-merge-state.sh +2 -2
index 89a62ac53b..bfe782d89e 100755

Lines of this file are not shown. View the raw message.

t/t7610-mergetool.sh +9 -9
index 957f8e20ba..0128b14452 100755

Lines of this file are not shown. View the raw message.

t/t7700-repack.sh +7 -7
index c6ff3aed30..47217e7cc4 100755

Lines of this file are not shown. View the raw message.

t/t7703-repack-geometric.sh +2 -2
index 04d5d8fc33..d00715efcc 100755

Lines of this file are not shown. View the raw message.

t/t7704-repack-cruft.sh +6 -6
index 9e03b04315..b342e82447 100755

Lines of this file are not shown. View the raw message.

t/t7800-difftool.sh +13 -13
index 8a91ff3603..02e47f466a 100755

Lines of this file are not shown. View the raw message.

t/t7810-grep.sh +11 -11
index 1b195bee59..d314393fce 100755

Lines of this file are not shown. View the raw message.

t/t7814-grep-recurse-submodules.sh +1 -1
index 167fe66150..e1cf53dc9e 100755

Lines of this file are not shown. View the raw message.

t/t7900-maintenance.sh +17 -17
index 9db4a76f67..cf170886c8 100755

Lines of this file are not shown. View the raw message.

t/t8008-blame-formats.sh +1 -1
index c12a4196d6..016a837f14 100755

Lines of this file are not shown. View the raw message.

t/t8010-cat-file-filters.sh +1 -1
index b3be2aa387..a4afb33c3e 100755

Lines of this file are not shown. View the raw message.

t/t8012-blame-colors.sh +1 -1
index 5562eba436..d2653b3c31 100755

Lines of this file are not shown. View the raw message.

t/t9001-send-email.sh +95 -95

190 lines not on this page · raw message

t/t9003-help-autocorrect.sh +3 -3
index 8da318d2b5..87b438fcac 100755

Lines of this file are not shown. View the raw message.

t/t9106-git-svn-commit-diff-clobber.sh +1 -1
index bca496c40e..376100fd7a 100755

Lines of this file are not shown. View the raw message.

t/t9107-git-svn-migrate.sh +15 -15
index aa908bbc2f..6d7d2aa491 100755

Lines of this file are not shown. View the raw message.

t/t9110-git-svn-use-svm-props.sh +10 -10
index ad37d980c9..9dc4a31a44 100755

Lines of this file are not shown. View the raw message.

t/t9111-git-svn-use-svnsync-props.sh +9 -9
index 6c93073551..e45d81bfc4 100755

Lines of this file are not shown. View the raw message.

t/t9114-git-svn-dcommit-merge.sh +3 -3
index e06538b1c8..2240b58d4d 100755

Lines of this file are not shown. View the raw message.

t/t9116-git-svn-log.sh +4 -4
index d74d7b2de6..b2e1248651 100755

Lines of this file are not shown. View the raw message.

t/t9117-git-svn-init-clone.sh +6 -6
index 3b038c338f..c5d2617823 100755

Lines of this file are not shown. View the raw message.

t/t9119-git-svn-info.sh +8 -8
index 088d1c57a8..0dbb2e4a97 100755

Lines of this file are not shown. View the raw message.

t/t9122-git-svn-author.sh +4 -4
index 0fc289ae0f..21401ad2b1 100755

Lines of this file are not shown. View the raw message.

t/t9130-git-svn-authors-file.sh +4 -4
index 90325db909..67a4d889fd 100755

Lines of this file are not shown. View the raw message.

t/t9138-git-svn-authors-prog.sh +7 -7
index 5bb38cb23a..5fdf324111 100755

Lines of this file are not shown. View the raw message.

t/t9140-git-svn-reset.sh +2 -2
index a420b2a87a..c70a3b36a4 100755

Lines of this file are not shown. View the raw message.

t/t9153-git-svn-rewrite-uuid.sh +2 -2
index 8cb2b5c69c..2387c7d4f2 100755

Lines of this file are not shown. View the raw message.

t/t9200-git-cvsexportcommit.sh +1 -1
index 581cf3d28f..8e0272df20 100755

Lines of this file are not shown. View the raw message.

t/t9210-scalar.sh +17 -17
index f2a6df77ce..c8463d0ac4 100755

Lines of this file are not shown. View the raw message.

t/t9211-scalar-clone.sh +8 -8
index 2043f48a1a..002d6ecdc1 100755

Lines of this file are not shown. View the raw message.

t/t9300-fast-import.sh +5 -5
index 479437760b..fe6c2617ac 100755

Lines of this file are not shown. View the raw message.

t/t9350-fast-export.sh +27 -27

54 lines not on this page · raw message

t/t9351-fast-export-anonymize.sh +18 -18
index 156a647484..ed784a56b0 100755

Lines of this file are not shown. View the raw message.

t/t9400-git-cvsserver-server.sh +2 -2
index 4b45398bab..b2379e4671 100755

Lines of this file are not shown. View the raw message.

t/t9501-gitweb-standalone-http-status.sh +29 -29

58 lines not on this page · raw message

t/t9502-gitweb-standalone-parse-output.sh +19 -19
index 81d5625557..d8280d17e5 100755

Lines of this file are not shown. View the raw message.

t/t9800-git-p4-basic.sh +5 -5
index 0816763e46..140a3a7f24 100755

Lines of this file are not shown. View the raw message.

t/t9801-git-p4-branch.sh +24 -24

48 lines not on this page · raw message

t/t9806-git-p4-options.sh +5 -5
index c26d297433..dce710ea56 100755

Lines of this file are not shown. View the raw message.

t/t9807-git-p4-submit.sh +1 -1
index 6ae7ced51b..73c5953ca7 100755

Lines of this file are not shown. View the raw message.

t/t9810-git-p4-rcs.sh +4 -4
index 5fe83315ec..70355a32a3 100755

Lines of this file are not shown. View the raw message.

t/t9813-git-p4-preserve-users.sh +4 -4
index fd018c87a8..838ddd324c 100755

Lines of this file are not shown. View the raw message.

t/t9814-git-p4-rename.sh +4 -4
index 2a9838f37f..6c5ecb9bea 100755

Lines of this file are not shown. View the raw message.

t/t9827-git-p4-change-filetype.sh +2 -2
index d3670bd7a2..9a8231a788 100755

Lines of this file are not shown. View the raw message.

t/t9832-unshelve.sh +3 -3
index 6b3cb0414a..a5e90b9bf7 100755

Lines of this file are not shown. View the raw message.

t/t9833-errors.sh +2 -2
index e22369ccdf..2fb59cede7 100755

Lines of this file are not shown. View the raw message.

t/t9835-git-p4-metadata-encoding-python2.sh +18 -18
index b969c7e0d5..7eb0c8ecd1 100755

Lines of this file are not shown. View the raw message.

t/t9836-git-p4-metadata-encoding-python3.sh +19 -19
index da6669bf71..16118495cd 100755

Lines of this file are not shown. View the raw message.

t/t9850-shell.sh +1 -1
index 21c3af48bd..27cc49bbd1 100755

Lines of this file are not shown. View the raw message.

t/t9902-completion.sh +13 -13
index 28f61f08fb..382b57d7b4 100755

Lines of this file are not shown. View the raw message.

The rest of this diff is not shown. View the raw message.

Signature · 1 line
gitgitgadget

Part of the diff is not shown here. View the raw message.

#9 lore raw
In reply to Michael Montalbo via GitGitGadget
Quoted 6 lines · Michael Montalbo via GitGitGadget

The test suite has a test_grep wrapper that prints file contents on assertion failure, making debugging easier. Many tests still use bare 'grep' for assertions, which silently swallows context on failure.

This series adds a lint tool (lint-style.pl) to mechanically detect and convert these, then applies it across the test suite.

I do not think we want an automated tool that rewrites the source files. I was hoping that we would get a patch or two that _adds_ to existing test-lint framework (i.e., 'test-grep' that 'test-lint' target depends on in t/Makefile) that gives diagnosis in a similar fashion as test-lint-shell-syntax and test-chainlint do.

Also some existing uses of "grep" are not end-user facing and should not be rewritten to "test_grep".

#10 lore raw
Quoted 9 lines · Junio C Hamano

I do not think we want an automated tool that rewrites the source files. I was hoping that we would get a patch or two that _adds_ to existing test-lint framework (i.e., 'test-grep' that 'test-lint' target depends on in t/Makefile) that gives diagnosis in a similar fashion as test-lint-shell-syntax and test-chainlint do.

Also some existing uses of "grep" are not end-user facing and should not be rewritten to "test_grep".

Apologies for not responding explicitly before sending a re-roll, I will do that in
the future. v2 attempts to address these points as noted in the "changes" section.

#11 lore raw

v2 cover t: add greplint.pl and convert grep to test_grep

In reply to Michael Montalbo via GitGitGadget

test_grep is a wrapper around grep for test assertions that prints the file contents on failure for easier debugging. Bare grep fails silently, making it hard to diagnose what went wrong.

This series converts existing bare grep assertions to test_grep and adds greplint.pl to prevent new ones from being introduced.

Patch 1 documents test_grep in t/README.

Patch 2 fixes three greps missing file arguments (t2402, t7507, t7700). They were reading empty stdin and passing vacuously.

Patch 3 extracts chainlint's Lexer, ShellParser, and ScriptParser into a shared module (lib-shell-parser.pl) so greplint.pl can reuse the same tokenizer. No functional change to chainlint.

Patch 4 fixes a latent line-counting bug in scan_dqstring where newlines from $() bodies inside double-quoted strings were counted twice. This does not affect chainlint (which uses byte offsets) but matters for greplint.pl's line-number reporting.

Patch 5 converts existing assertion greps to test_grep, including sourced test helpers. Greps used as data filters or on files that may not exist are left unconverted with lint-ok annotations.

Patch 6 adds greplint.pl with test fixtures (modeled on chainlint/) and wires it into the Makefile as test-greplint and check-greplint.

Changes since v1:

* Dropped lint-style.pl and the --fix mode concept. Replaced with greplint.pl, which more closely follows chainlint's conventions, and reuses its parser logic via a shared module.

* A regex approach to grep linting was prototyped in an attempt to reduce the number of patches in the series, but this approach produced false positives from grep inside heredoc bodies (e.g. write_script) and cross-line pipelines where the pipe or redirect is on a different line from the grep. The shared module's Lexer already collapses these into single tokens, giving zero false positives with less code than the regex heuristics would need, which is why it was retained in the current version.

* Reverted incorrect conversions where grep was used as a data filter inside redirected compound commands, not as a test assertion.

Known limitation / follow-up:

* Assertions like grep pattern file >/dev/null and grep pattern <file are not converted because greplint.pl treats any redirect as a filter. The former is ambiguous because >/dev/null becomes dead code under test_grep (which already suppresses matching-line output). The latter requires removing the redirect and passing the file as a positional argument, since test_grep does not support stdin redirects. Both are left as bare grep. A follow-up series can address these once a convention is established.

Michael Montalbo (6):
t/README: document test_grep helper
t: fix grep assertions missing file arguments
t: extract chainlint's parser into shared module
t: fix Lexer line count for $() inside double-quoted strings
t: convert grep assertions to test_grep
t: add greplint to detect bare grep assertions

 t/.gitattributes                              |   2 +
 t/Makefile                                    |  29 +-
 t/README                                      |  21 +
 t/chainlint.pl                                | 529 +----------------
 t/for-each-ref-tests.sh                       |  12 +-
 t/greplint-cat.pl                             |  27 +
 t/greplint.pl                                 | 241 ++++++++
 t/greplint/bare-grep-after-and.expect         |   1 +
 t/greplint/bare-grep-after-and.test           |   4 +
 t/greplint/bare-grep-after-semicolon.expect   |   1 +
 t/greplint/bare-grep-after-semicolon.test     |   4 +
 t/greplint/bare-grep-compound-body.expect     |   3 +
 t/greplint/bare-grep-compound-body.test       |  17 +
 t/greplint/bare-grep-count-mode.expect        |   1 +
 t/greplint/bare-grep-count-mode.test          |   3 +
 t/greplint/bare-grep-explicit-pattern.expect  |   1 +
 t/greplint/bare-grep-explicit-pattern.test    |   3 +
 t/greplint/bare-grep-flags.expect             |   1 +
 t/greplint/bare-grep-flags.test               |   3 +
 t/greplint/bare-grep-lint-ok.expect           |   0
 t/greplint/bare-grep-lint-ok.test             |   4 +
 t/greplint/bare-grep-negated.expect           |   1 +
 t/greplint/bare-grep-negated.test             |   3 +
 t/greplint/bare-grep-pattern-file.expect      |   1 +
 t/greplint/bare-grep-pattern-file.test        |   3 +
 t/greplint/bare-grep-simple.expect            |   1 +
 t/greplint/bare-grep-simple.test              |   3 +
 t/greplint/bare-grep-subshell.expect          |   1 +
 t/greplint/bare-grep-subshell.test            |   5 +
 .../dqstring-continuation-offset.expect       |   1 +
 t/greplint/dqstring-continuation-offset.test  |  11 +
 t/greplint/filter-command-substitution.expect |   0
 t/greplint/filter-command-substitution.test   |   3 +
 t/greplint/filter-pipe-input.expect           |   0
 t/greplint/filter-pipe-input.test             |   3 +
 t/greplint/filter-pipe-output.expect          |   0
 t/greplint/filter-pipe-output.test            |   3 +
 t/greplint/filter-redirect-output.expect      |   0
 t/greplint/filter-redirect-output.test        |   3 +
 t/greplint/filter-stdin-redirect.expect       |   0
 t/greplint/filter-stdin-redirect.test         |   3 +
 t/greplint/grep-as-argument.expect            |   0
 t/greplint/grep-as-argument.test              |   3 +
 t/greplint/grep-as-value.expect               |   0
 t/greplint/grep-as-value.test                 |   6 +
 t/greplint/wrong-negation.expect              |   1 +
 t/greplint/wrong-negation.test                |   3 +
 t/lib-bitmap.sh                               |  12 +-
 t/lib-bundle-uri-protocol.sh                  |  26 +-
 t/lib-httpd.sh                                |   2 +-
 t/lib-shell-parser.pl                         | 534 ++++++++++++++++++
 t/pack-refs-tests.sh                          |   2 +-
 t/show-ref-exists-tests.sh                    |   2 +-
 t/t0000-basic.sh                              |  16 +-
 t/t0001-init.sh                               |  18 +-
 t/t0008-ignores.sh                            |   8 +-
 t/t0009-git-dir-validation.sh                 |   6 +-
 t/t0012-help.sh                               |   4 +-
 t/t0013-sha1dc.sh                             |   2 +-
 t/t0017-env-helper.sh                         |   4 +-
 t/t0021-conversion.sh                         |  18 +-
 t/t0029-core-unsetenvvars.sh                  |   4 +-
 t/t0030-stripspace.sh                         |   4 +-
 t/t0031-lockfile-pid.sh                       |   2 +-
 t/t0040-parse-options.sh                      |  52 +-
 t/t0041-usage.sh                              |   2 +-
 t/t0052-simple-ipc.sh                         |  10 +-
 t/t0061-run-command.sh                        |   2 +-
 t/t0066-dir-iterator.sh                       |   2 +-
 t/t0068-for-each-repo.sh                      |  16 +-
 t/t0070-fundamental.sh                        |   6 +-
 t/t0081-find-pack.sh                          |  12 +-
 t/t0091-bugreport.sh                          |  18 +-
 t/t0092-diagnose.sh                           |  12 +-
 t/t0100-previous.sh                           |   2 +-
 t/t0200-gettext-basic.sh                      |  14 +-
 t/t0203-gettext-setlocale-sanity.sh           |   4 +-
 t/t0204-gettext-reencode-sanity.sh            |   8 +-
 t/t0210-trace2-normal.sh                      |   6 +-
 t/t0211-trace2-perf.sh                        |  80 +--
 t/t0212-trace2-event.sh                       |   8 +-
 t/t0300-credentials.sh                        |   4 +-
 t/t0410-partial-clone.sh                      |  82 +--
 t/t0450-txt-doc-vs-help.sh                    |   2 +-
 t/t0500-progress-display.sh                   |  18 +-
 t/t0610-reftable-basics.sh                    |   8 +-
 t/t1004-read-tree-m-u-wf.sh                   |   8 +-
 t/t1006-cat-file.sh                           |  18 +-
 t/t1007-hash-object.sh                        |   8 +-
 t/t1011-read-tree-sparse-checkout.sh          |  10 +-
 t/t1050-large.sh                              |   6 +-
 t/t1091-sparse-checkout-builtin.sh            |  24 +-
 t/t1092-sparse-checkout-compatibility.sh      |  44 +-
 t/t1300-config.sh                             |  16 +-
 t/t1305-config-include.sh                     |   2 +-
 t/t1308-config-set.sh                         |   6 +-
 t/t1400-update-ref.sh                         | 170 +++---
 t/t1403-show-ref.sh                           |  18 +-
 t/t1410-reflog.sh                             |   4 +-
 t/t1415-worktree-refs.sh                      |   4 +-
 t/t1430-bad-ref-name.sh                       |  56 +-
 t/t1450-fsck.sh                               |  12 +-
 t/t1451-fsck-buffer.sh                        |   6 +-
 t/t1460-refs-migrate.sh                       |   2 +-
 t/t1500-rev-parse.sh                          |   6 +-
 t/t1502-rev-parse-parseopt.sh                 |   2 +-
 t/t1503-rev-parse-verify.sh                   |  10 +-
 t/t1510-repo-setup.sh                         |  10 +-
 t/t1512-rev-parse-disambiguation.sh           |   4 +-
 t/t1515-rev-parse-outside-repo.sh             |   2 +-
 t/t1800-hook.sh                               |  18 +-
 t/t2004-checkout-cache-temp.sh                |   4 +-
 t/t2019-checkout-ambiguous-ref.sh             |   4 +-
 t/t2024-checkout-dwim.sh                      |   8 +-
 t/t2030-unresolve-info.sh                     |   6 +-
 t/t2060-switch.sh                             |   6 +-
 t/t2070-restore.sh                            |   2 +-
 t/t2080-parallel-checkout-basics.sh           |  14 +-
 t/t2081-parallel-checkout-collisions.sh       |  24 +-
 t/t2082-parallel-checkout-attributes.sh       |  12 +-
 t/t2103-update-index-ignore-missing.sh        |   6 +-
 t/t2200-add-update.sh                         |   2 +-
 t/t2203-add-intent.sh                         |   6 +-
 t/t2400-worktree-add.sh                       |  24 +-
 t/t2402-worktree-list.sh                      |  16 +-
 t/t2403-worktree-move.sh                      |   6 +-
 t/t2405-worktree-submodule.sh                 |   6 +-
 t/t2407-worktree-heads.sh                     |  26 +-
 t/t2500-untracked-overwriting.sh              |   8 +-
 t/t2501-cwd-empty.sh                          |   4 +-
 t/t3001-ls-files-others-exclude.sh            |   6 +-
 t/t3007-ls-files-recurse-submodules.sh        |   6 +-
 t/t3200-branch.sh                             |  12 +-
 t/t3202-show-branch.sh                        |  10 +-
 t/t3203-branch-output.sh                      |   4 +-
 t/t3206-range-diff.sh                         |  78 +--
 t/t3207-branch-submodule.sh                   |   4 +-
 t/t3301-notes.sh                              |  32 +-
 t/t3310-notes-merge-manual-resolve.sh         |  16 +-
 t/t3320-notes-merge-worktrees.sh              |   2 +-
 t/t3400-rebase.sh                             |  16 +-
 t/t3402-rebase-merge.sh                       |  16 +-
 t/t3404-rebase-interactive.sh                 |  72 +--
 t/t3406-rebase-message.sh                     |   6 +-
 t/t3415-rebase-autosquash.sh                  |  10 +-
 t/t3416-rebase-onto-threedots.sh              |   4 +-
 t/t3418-rebase-continue.sh                    |  10 +-
 t/t3420-rebase-autostash.sh                   |  26 +-
 t/t3422-rebase-incompatible-options.sh        |   4 +-
 t/t3429-rebase-edit-todo.sh                   |   2 +-
 t/t3430-rebase-merges.sh                      |  32 +-
 t/t3500-cherry.sh                             |   4 +-
 t/t3501-revert-cherry-pick.sh                 |   6 +-
 t/t3504-cherry-pick-rerere.sh                 |   6 +-
 t/t3510-cherry-pick-sequence.sh               |  24 +-
 t/t3602-rm-sparse-checkout.sh                 |   4 +-
 t/t3705-add-sparse-checkout.sh                |  10 +-
 t/t3800-mktag.sh                              |   4 +-
 t/t3901-i18n-patch.sh                         |  16 +-
 t/t3903-stash.sh                              |  28 +-
 t/t3904-stash-patch.sh                        |   4 +-
 t/t3908-stash-in-worktree.sh                  |   2 +-
 t/t4000-diff-format.sh                        |   2 +-
 t/t4001-diff-rename.sh                        |   4 +-
 t/t4011-diff-symlink.sh                       |   2 +-
 t/t4013-diff-various.sh                       |   2 +-
 t/t4014-format-patch.sh                       | 344 +++++------
 t/t4015-diff-whitespace.sh                    |  16 +-
 t/t4017-diff-retval.sh                        |   2 +-
 t/t4018-diff-funcname.sh                      |   2 +-
 t/t4019-diff-wserror.sh                       |   8 +-
 t/t4020-diff-external.sh                      |  18 +-
 t/t4021-format-patch-numbered.sh              |   4 +-
 t/t4022-diff-rewrite.sh                       |  14 +-
 t/t4028-format-patch-mime-headers.sh          |   6 +-
 t/t4031-diff-rewrite-binary.sh                |  18 +-
 t/t4033-diff-patience.sh                      |   2 +-
 t/t4036-format-patch-signer-mime.sh           |   6 +-
 t/t4038-diff-combined.sh                      |   6 +-
 t/t4051-diff-function-context.sh              |  38 +-
 t/t4053-diff-no-index.sh                      |   4 +-
 t/t4063-diff-blobs.sh                         |   2 +-
 t/t4065-diff-anchored.sh                      |  26 +-
 t/t4067-diff-partial-clone.sh                 |  12 +-
 t/t4073-diff-stat-name-width.sh               |  24 +-
 t/t4103-apply-binary.sh                       |   2 +-
 t/t4120-apply-popt.sh                         |   2 +-
 t/t4124-apply-ws-rule.sh                      |  10 +-
 t/t4128-apply-root.sh                         |   2 +-
 t/t4140-apply-ita.sh                          |   4 +-
 t/t4141-apply-too-large.sh                    |   2 +-
 t/t4150-am.sh                                 |  48 +-
 t/t4200-rerere.sh                             |   6 +-
 t/t4201-shortlog.sh                           |   2 +-
 t/t4202-log.sh                                |  84 +--
 t/t4204-patch-id.sh                           |   2 +-
 t/t4205-log-pretty-formats.sh                 |   2 +-
 t/t4209-log-pickaxe.sh                        |  10 +-
 t/t4211-line-log.sh                           |  72 +--
 t/t4216-log-bloom.sh                          |  18 +-
 t/t4252-am-options.sh                         |  22 +-
 t/t4254-am-corrupt.sh                         |   6 +-
 t/t4258-am-quoted-cr.sh                       |   2 +-
 t/t4301-merge-tree-write-tree.sh              |  18 +-
 t/t5000-tar-tree.sh                           |  10 +-
 t/t5004-archive-corner-cases.sh               |   2 +-
 t/t5100-mailinfo.sh                           |   2 +-
 t/t5150-request-pull.sh                       |  18 +-
 t/t5300-pack-object.sh                        |  22 +-
 t/t5302-pack-index.sh                         |   6 +-
 t/t5304-prune.sh                              |   8 +-
 t/t5310-pack-bitmaps.sh                       |  14 +-
 t/t5317-pack-objects-filter-objects.sh        |  12 +-
 t/t5318-commit-graph.sh                       |   8 +-
 t/t5319-multi-pack-index.sh                   |  16 +-
 t/t5324-split-commit-graph.sh                 |  10 +-
 t/t5325-reverse-index.sh                      |   2 +-
 t/t5326-multi-pack-bitmaps.sh                 |  28 +-
 t/t5328-commit-graph-64bit-time.sh            |   2 +-
 t/t5329-pack-objects-cruft.sh                 |   8 +-
 t/t5334-incremental-multi-pack-index.sh       |   2 +-
 t/t5335-compact-multi-pack-index.sh           |   4 +-
 t/t5351-unpack-large-objects.sh               |   2 +-
 t/t5402-post-merge-hook.sh                    |   4 +-
 t/t5403-post-checkout-hook.sh                 |   2 +-
 t/t5404-tracking-branches.sh                  |   2 +-
 t/t5406-remote-rejects.sh                     |   2 +-
 t/t5407-post-rewrite-hook.sh                  |   8 +-
 t/t5409-colorize-remote-messages.sh           |  36 +-
 t/t5500-fetch-pack.sh                         |  38 +-
 t/t5504-fetch-receive-strict.sh               |  14 +-
 t/t5505-remote.sh                             |  20 +-
 t/t5510-fetch.sh                              |  10 +-
 t/t5512-ls-remote.sh                          |   8 +-
 t/t5514-fetch-multiple.sh                     |   2 +-
 t/t5516-fetch-push.sh                         |  20 +-
 t/t5520-pull.sh                               |   4 +-
 t/t5524-pull-msg.sh                           |   6 +-
 t/t5526-fetch-submodules.sh                   |  16 +-
 t/t5529-push-errors.sh                        |   4 +-
 t/t5530-upload-pack-error.sh                  |  18 +-
 t/t5531-deep-submodule-push.sh                |   2 +-
 t/t5532-fetch-proxy.sh                        |   2 +-
 t/t5533-push-cas.sh                           |  12 +-
 t/t5534-push-signed.sh                        |  22 +-
 t/t5537-fetch-shallow.sh                      |   2 +-
 t/t5538-push-shallow.sh                       |   2 +-
 t/t5539-fetch-http-shallow.sh                 |   4 +-
 t/t5541-http-push-smart.sh                    |  32 +-
 t/t5544-pack-objects-hook.sh                  |  12 +-
 t/t5550-http-fetch-dumb.sh                    |   4 +-
 t/t5551-http-fetch-smart.sh                   |  46 +-
 t/t5552-skipping-fetch-negotiator.sh          |   6 +-
 t/t5554-noop-fetch-negotiator.sh              |   4 +-
 t/t5557-http-get.sh                           |   2 +-
 t/t5558-clone-bundle-uri.sh                   |  38 +-
 t/t5562-http-backend-content-length.sh        |   2 +-
 t/t5564-http-proxy.sh                         |  10 +-
 t/t5581-http-curl-verbose.sh                  |   2 +-
 t/t5583-push-branches.sh                      |   8 +-
 t/t5601-clone.sh                              |  28 +-
 t/t5604-clone-reference.sh                    |   8 +-
 t/t5605-clone-local.sh                        |   2 +-
 t/t5606-clone-options.sh                      |   6 +-
 t/t5612-clone-refspec.sh                      |   2 +-
 t/t5616-partial-clone.sh                      |  60 +-
 t/t5619-clone-local-ambiguous-transport.sh    |   2 +-
 t/t5620-backfill.sh                           |  12 +-
 t/t5700-protocol-v1.sh                        |  46 +-
 t/t5701-git-serve.sh                          |  14 +-
 t/t5702-protocol-v2.sh                        | 154 ++---
 t/t5703-upload-pack-ref-in-want.sh            |  22 +-
 t/t5705-session-id-in-capabilities.sh         |  12 +-
 t/t5750-bundle-uri-parse.sh                   |   8 +-
 t/t5801-remote-helpers.sh                     |   4 +-
 t/t5810-proto-disable-local.sh                |   2 +-
 t/t5813-proto-disable-ssh.sh                  |   4 +-
 t/t6000-rev-list-misc.sh                      |  26 +-
 t/t6005-rev-list-count.sh                     |   8 +-
 t/t6006-rev-list-format.sh                    |   4 +-
 t/t6009-rev-list-parent.sh                    |   4 +-
 t/t6020-bundle-misc.sh                        |  12 +-
 t/t6022-rev-list-missing.sh                   |   4 +-
 t/t6030-bisect-porcelain.sh                   | 150 ++---
 t/t6040-tracking-info.sh                      |   2 +-
 t/t6112-rev-list-filters-objects.sh           |  24 +-
 t/t6115-rev-list-du.sh                        |   4 +-
 t/t6120-describe.sh                           |  14 +-
 t/t6200-fmt-merge-msg.sh                      |  82 +--
 t/t6402-merge-rename.sh                       |   4 +-
 t/t6403-merge-file.sh                         |   6 +-
 t/t6404-recursive-merge.sh                    |   2 +-
 t/t6406-merge-attr.sh                         |  20 +-
 t/t6417-merge-ours-theirs.sh                  |  30 +-
 t/t6418-merge-text-auto.sh                    |   2 +-
 t/t6422-merge-rename-corner-cases.sh          |   8 +-
 t/t6423-merge-rename-directories.sh           |  72 +--
 t/t6424-merge-unrelated-index-changes.sh      |   6 +-
 t/t6427-diff3-conflict-markers.sh             |  10 +-
 t/t6432-merge-recursive-space-options.sh      |   4 +-
 t/t6436-merge-overwrite.sh                    |   6 +-
 t/t6437-submodule-merge.sh                    |  10 +-
 t/t6500-gc.sh                                 |   8 +-
 t/t6600-test-reach.sh                         |   4 +-
 t/t7001-mv.sh                                 |  16 +-
 t/t7002-mv-sparse-checkout.sh                 |  38 +-
 t/t7003-filter-branch.sh                      |  16 +-
 t/t7004-tag.sh                                |   2 +-
 t/t7006-pager.sh                              |  16 +-
 t/t7012-skip-worktree-writing.sh              |   6 +-
 t/t7030-verify-tag.sh                         |  52 +-
 t/t7031-verify-tag-signed-ssh.sh              |  46 +-
 t/t7102-reset.sh                              |   2 +-
 t/t7110-reset-merge.sh                        |  40 +-
 t/t7201-co.sh                                 |   6 +-
 t/t7300-clean.sh                              |   2 +-
 t/t7301-clean-interactive.sh                  |   2 +-
 t/t7400-submodule-basic.sh                    |  32 +-
 t/t7402-submodule-rebase.sh                   |   2 +-
 t/t7406-submodule-update.sh                   |  26 +-
 t/t7416-submodule-dash-url.sh                 |  20 +-
 t/t7417-submodule-path-url.sh                 |   2 +-
 t/t7450-bad-git-dotfiles.sh                   |  14 +-
 t/t7501-commit-basic-functionality.sh         |  16 +-
 t/t7502-commit-porcelain.sh                   |   2 +-
 t/t7507-commit-verbose.sh                     |   6 +-
 t/t7508-status.sh                             |   6 +-
 t/t7510-signed-commit.sh                      |  68 +--
 t/t7516-commit-races.sh                       |   4 +-
 t/t7519-status-fsmonitor.sh                   |  14 +-
 t/t7527-builtin-fsmonitor.sh                  |  82 +--
 t/t7528-signed-commit-ssh.sh                  |  68 +--
 t/t7600-merge.sh                              |  10 +-
 t/t7603-merge-reduce-heads.sh                 |  20 +-
 t/t7606-merge-custom.sh                       |   2 +-
 t/t7607-merge-state.sh                        |   4 +-
 t/t7610-mergetool.sh                          |  18 +-
 t/t7700-repack.sh                             |  14 +-
 t/t7703-repack-geometric.sh                   |   4 +-
 t/t7704-repack-cruft.sh                       |  12 +-
 t/t7800-difftool.sh                           |  26 +-
 t/t7810-grep.sh                               |  22 +-
 t/t7814-grep-recurse-submodules.sh            |   2 +-
 t/t7900-maintenance.sh                        |  34 +-
 t/t8008-blame-formats.sh                      |   2 +-
 t/t8010-cat-file-filters.sh                   |   2 +-
 t/t8012-blame-colors.sh                       |   2 +-
 t/t9001-send-email.sh                         | 190 +++----
 t/t9003-help-autocorrect.sh                   |   6 +-
 t/t9106-git-svn-commit-diff-clobber.sh        |   2 +-
 t/t9107-git-svn-migrate.sh                    |  30 +-
 t/t9110-git-svn-use-svm-props.sh              |  20 +-
 t/t9111-git-svn-use-svnsync-props.sh          |  18 +-
 t/t9114-git-svn-dcommit-merge.sh              |   6 +-
 t/t9116-git-svn-log.sh                        |   8 +-
 t/t9117-git-svn-init-clone.sh                 |  12 +-
 t/t9119-git-svn-info.sh                       |  16 +-
 t/t9122-git-svn-author.sh                     |   8 +-
 t/t9130-git-svn-authors-file.sh               |   8 +-
 t/t9138-git-svn-authors-prog.sh               |  14 +-
 t/t9140-git-svn-reset.sh                      |   4 +-
 t/t9153-git-svn-rewrite-uuid.sh               |   4 +-
 t/t9200-git-cvsexportcommit.sh                |   2 +-
 t/t9210-scalar.sh                             |  34 +-
 t/t9211-scalar-clone.sh                       |  16 +-
 t/t9300-fast-import.sh                        |  10 +-
 t/t9350-fast-export.sh                        |  54 +-
 t/t9351-fast-export-anonymize.sh              |  36 +-
 t/t9400-git-cvsserver-server.sh               |   4 +-
 t/t9501-gitweb-standalone-http-status.sh      |  58 +-
 t/t9502-gitweb-standalone-parse-output.sh     |  38 +-
 t/t9800-git-p4-basic.sh                       |  10 +-
 t/t9801-git-p4-branch.sh                      |  48 +-
 t/t9806-git-p4-options.sh                     |  10 +-
 t/t9807-git-p4-submit.sh                      |   2 +-
 t/t9810-git-p4-rcs.sh                         |   8 +-
 t/t9813-git-p4-preserve-users.sh              |   8 +-
 t/t9814-git-p4-rename.sh                      |   8 +-
 t/t9827-git-p4-change-filetype.sh             |   4 +-
 t/t9832-unshelve.sh                           |   6 +-
 t/t9833-errors.sh                             |   4 +-
 t/t9835-git-p4-metadata-encoding-python2.sh   |  36 +-
 t/t9836-git-p4-metadata-encoding-python3.sh   |  38 +-
 t/t9850-shell.sh                              |   2 +-
 t/t9902-completion.sh                         |  26 +-
 385 files changed, 3772 insertions(+), 3329 deletions(-)
 create mode 100644 t/greplint-cat.pl
 create mode 100644 t/greplint.pl
 create mode 100644 t/greplint/bare-grep-after-and.expect
 create mode 100644 t/greplint/bare-grep-after-and.test
 create mode 100644 t/greplint/bare-grep-after-semicolon.expect
 create mode 100644 t/greplint/bare-grep-after-semicolon.test
 create mode 100644 t/greplint/bare-grep-compound-body.expect
 create mode 100644 t/greplint/bare-grep-compound-body.test
 create mode 100644 t/greplint/bare-grep-count-mode.expect
 create mode 100644 t/greplint/bare-grep-count-mode.test
 create mode 100644 t/greplint/bare-grep-explicit-pattern.expect
 create mode 100644 t/greplint/bare-grep-explicit-pattern.test
 create mode 100644 t/greplint/bare-grep-flags.expect
 create mode 100644 t/greplint/bare-grep-flags.test
 create mode 100644 t/greplint/bare-grep-lint-ok.expect
 create mode 100644 t/greplint/bare-grep-lint-ok.test
 create mode 100644 t/greplint/bare-grep-negated.expect
 create mode 100644 t/greplint/bare-grep-negated.test
 create mode 100644 t/greplint/bare-grep-pattern-file.expect
 create mode 100644 t/greplint/bare-grep-pattern-file.test
 create mode 100644 t/greplint/bare-grep-simple.expect
 create mode 100644 t/greplint/bare-grep-simple.test
 create mode 100644 t/greplint/bare-grep-subshell.expect
 create mode 100644 t/greplint/bare-grep-subshell.test
 create mode 100644 t/greplint/dqstring-continuation-offset.expect
 create mode 100644 t/greplint/dqstring-continuation-offset.test
 create mode 100644 t/greplint/filter-command-substitution.expect
 create mode 100644 t/greplint/filter-command-substitution.test
 create mode 100644 t/greplint/filter-pipe-input.expect
 create mode 100644 t/greplint/filter-pipe-input.test
 create mode 100644 t/greplint/filter-pipe-output.expect
 create mode 100644 t/greplint/filter-pipe-output.test
 create mode 100644 t/greplint/filter-redirect-output.expect
 create mode 100644 t/greplint/filter-redirect-output.test
 create mode 100644 t/greplint/filter-stdin-redirect.expect
 create mode 100644 t/greplint/filter-stdin-redirect.test
 create mode 100644 t/greplint/grep-as-argument.expect
 create mode 100644 t/greplint/grep-as-argument.test
 create mode 100644 t/greplint/grep-as-value.expect
 create mode 100644 t/greplint/grep-as-value.test
 create mode 100644 t/greplint/wrong-negation.expect
 create mode 100644 t/greplint/wrong-negation.test
 create mode 100644 t/lib-shell-parser.pl

base-commit: 1ff279f3404a482a83fb04c7457e41ab26884aea
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2135

Range-diff vs v1:

 1:  43402040bf = 1:  5959cab258 t/README: document test_grep helper
 5:  c0db9fdb5b = 2:  f3e8e19e6e t: fix grep assertions missing file arguments
 2:  a1069efa8f ! 3:  460461b5fe t: extract chainlint's parser into shared module
     @@ Metadata
       ## Commit message ##
          t: extract chainlint's parser into shared module
      
     -    Move the Lexer, ShellParser, and ScriptParser packages from
     -    chainlint.pl into t/lib-shell-parser.pl so they can be reused by
     -    other tools.  ScriptParser's check_test() is a no-op in the shared
     -    module; callers subclass ScriptParser and override it.
     +    Move chainlint.pl's Lexer, ShellParser, and ScriptParser into a
     +    shared module (lib-shell-parser.pl) so other lint tools can reuse
     +    the same shell parsing infrastructure.  A subsequent commit adds
     +    greplint.pl, which needs the same tokenizer to correctly identify
     +    command boundaries.
      
     -    chainlint.pl defines TestParser (&&-chain detection) and
     -    ChainlintParser (a ScriptParser subclass whose check_test runs
     -    TestParser and formats the results).  The shared module is loaded
     -    via do() for portability with minimal Perl installations.
     +    ScriptParser's check_test() becomes a no-op in the shared module.
     +    chainlint.pl defines ChainlintParser (extending ScriptParser)
     +    with the &&-chain check_test() implementation.
      
     -    A subsequent commit introduces lint-style.pl which needs the same
     -    shell parser to properly tokenize test scripts.  Sharing the parser
     -    avoids reimplementing heredoc handling, $(...) nesting, pipe
     -    tracking, quoting, and test body extraction.
     +    No functional change: chainlint produces the same output and
     +    check-chainlint self-tests pass.
      
          Signed-off-by: Michael Montalbo <[email protected]>
      
     @@ t/chainlint.pl: my $jobs = -1;
      -	return @tokens;
      -}
      +use File::Basename;
     -+my $_lib = dirname($0) . "/lib-shell-parser.pl";
     -+$_lib = "./$_lib" unless $_lib =~ m{^/};
     -+do $_lib or die "failed to load $_lib: $@$!\n";
     ++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
      @@ t/chainlint.pl: DONE:
     + # 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';
     @@ t/chainlint.pl: DONE:
      -# at the top-level of test scripts but also within compound commands such as
      -# loops and function definitions.
      -package ScriptParser;
     -+# ChainlintParser is a subclass of ScriptParser which checks each test
     -+# body for broken &&-chains via TestParser, then formats and collects
     -+# the results.
     -+package ChainlintParser;
     - 
     +-
      -use base 'ShellParser';
     -+our @ISA = ('ScriptParser');
     - 
     - sub new {
     - 	my $class = shift @_;
     -@@ t/chainlint.pl: sub new {
     - 	return $self;
     - }
     +-
     +-sub new {
     +-	my $class = shift @_;
     +-	my $self = $class->SUPER::new(@_);
     +-	$self->{ntests} = 0;
     +-	$self->{nerrs} = 0;
     +-	return $self;
     +-}
     ++# ChainlintParser extends ScriptParser with &&-chain checking
     ++package ChainlintParser;
       
      -# 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,
     @@ t/chainlint.pl: sub new {
      -	}
      -	return $s
      -}
     --
     ++our @ISA = ('ScriptParser');
     + 
       sub format_problem {
       	local $_ = shift;
     - 	/^AMP$/ && return "missing '&&'";
      @@ t/chainlint.pl: sub format_problem {
       
       sub check_test {
     @@ t/chainlint.pl: sub check_test {
      -}
      -
       # main contains high-level functionality for processing command-line switches,
     - # feeding input test scripts to ScriptParser, and reporting results.
     +-# 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(); };
      @@ t/chainlint.pl: sub check_script {
       		}
       		my $s = do { local $/; <$fh> };
     @@ t/chainlint.pl: sub check_script {
      
       ## t/lib-shell-parser.pl (new) ##
      @@
     ++# Copyright (c) 2021-2022 Eric Sunshine <[email protected]>
     ++#
     ++# 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;
      +
     -+# Copyright (c) 2021-2022 Eric Sunshine <[email protected]>
     -+#
      +# 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
     @@ t/lib-shell-parser.pl (new)
      +# 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 {
     @@ t/lib-shell-parser.pl (new)
      +}
      +
      +# ScriptParser is a subclass of ShellParser which identifies individual test
     -+# definitions within test scripts and calls check_test() for each test body
     -+# found.  Callers subclass ScriptParser and override check_test() to
     -+# implement specific checks (e.g. chainlint checks &&-chains, lint-style
     -+# checks grep usage).
     ++# 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`
     @@ t/lib-shell-parser.pl (new)
      +}
      +
      +sub check_test {
     -+	# no-op; subclasses override to implement specific checks
     ++	# no-op; subclass and override to implement lint checks
      +}
      +
      +sub parse_cmd {
 3:  93c2b29683 ! 4:  c1b86748d1 t: fix Lexer line count for $() inside double-quoted strings
     @@ Commit message
      
          scan_dqstring's post-loop newline counter re-counts newlines that
          were already counted during recursive parsing of $() bodies.  This
     -    happens because scan_dollar's returned text can contain newlines
     -    (from token text of multi-line strings and from \n command separator
     -    tokens), and the catch-all counter at the end of scan_dqstring
     -    counts all of them again.
     +    happens because scan_dollar returns text containing newlines (from
     +    multi-line command substitutions), and the catch-all counter at the
     +    end of scan_dqstring counts all of them again.
      
          Fix this by counting newlines inline as non-special characters are
          consumed, and removing the post-loop catch-all.  Each newline is
          now counted exactly once: literal newlines at the inline match,
     -    line splices at the \<newline> handler, and $() newlines by
     +    line splices at the backslash handler, and $() newlines by
          scan_token during the recursive parse.
      
     -    This does not affect chainlint's output because chainlint annotates
     -    the original body text using byte offsets, not token line numbers.
     -    It does matter for tools like lint-style.pl (introduced in a
     -    subsequent commit) that use token line numbers to locate and fix
     -    specific lines in the original file.
     -
     -    Add check-shell-parser.pl to verify that the Lexer reports correct
     -    line numbers after multi-line $() in double-quoted strings.
     +    This is a latent bug: any consumer that relies on token line
     +    numbers rather than byte offsets would get incorrect results for
     +    tokens following a multi-line $() inside a double-quoted string.
     +    chainlint is not affected because it annotates the original body
     +    text using byte offsets, not token line numbers.
      
          Signed-off-by: Michael Montalbo <[email protected]>
      
     - ## t/Makefile ##
     -@@ t/Makefile: check-meson:
     - 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 check-shell-parser
     - else
     - GIT_TEST_CHAIN_LINT = 0
     - endif
     -@@ t/Makefile: test-lint-executable:
     - test-lint-shell-syntax:
     - 	@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)
     - 
     -+check-shell-parser:
     -+	@'$(PERL_PATH_SQ)' check-shell-parser.pl
     - 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)
     -@@ t/Makefile: perf:
     - 	$(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-shell-parser $(UNIT_TESTS)
     - 
     - .PHONY: libgit-sys-test libgit-rs-test
     - libgit-sys-test:
     -
     - ## t/check-shell-parser.pl (new) ##
     -@@
     -+#!/usr/bin/perl
     -+
     -+# Tests for the shared shell parser (lib-shell-parser.pl).
     -+
     -+use strict;
     -+use warnings;
     -+use File::Basename;
     -+
     -+my $_lib = dirname($0) . "/lib-shell-parser.pl";
     -+$_lib = "./$_lib" unless $_lib =~ m{^/};
     -+do $_lib or die "$0: failed to load $_lib: $@$!\n";
     -+
     -+my $rc = 0;
     -+
     -+sub check {
     -+	my ($desc, $body, $want_token, $want_line) = @_;
     -+	my $parser = ShellParser->new(\$body);
     -+	my @tokens = $parser->parse();
     -+	for my $t (reverse @tokens) {
     -+		next unless $t->[0] eq $want_token && defined $t->[3];
     -+		if ($t->[3] != $want_line) {
     -+			print STDERR "FAIL: $desc: " .
     -+				"'$want_token' at line $t->[3], " .
     -+				"expected line $want_line\n";
     -+			$rc = 1;
     -+		}
     -+		return;
     -+	}
     -+	print STDERR "FAIL: $desc: token '$want_token' not found\n";
     -+	$rc = 1;
     -+}
     -+
     -+# Multi-line $() inside a dq-string: MARKER should be at line 3.
     -+check('dq-string with multi-line $()', <<'BODY', 'MARKER', 3);
     -+	x="$(echo one
     -+	echo two)" &&
     -+	MARKER here
     -+BODY
     -+
     -+# Two multi-line $() substitutions: verifies drift does not accumulate.
     -+# MARKER should be at line 5.
     -+check('two dq-string $()', <<'BODY', 'MARKER', 5);
     -+	x="$(echo a
     -+	b)" &&
     -+	y="$(echo c
     -+	d)" &&
     -+	MARKER here
     -+BODY
     -+
     -+# $() outside a dq-string: no double-counting either way.
     -+# MARKER should be at line 3.
     -+check('bare $() spanning lines', <<'BODY', 'MARKER', 3);
     -+	x=$(echo one
     -+	echo two) &&
     -+	MARKER here
     -+BODY
     -+
     -+exit $rc;
     -
       ## t/lib-shell-parser.pl ##
      @@ t/lib-shell-parser.pl: sub scan_dqstring {
       	my $b = $self->{buff};
     @@ t/lib-shell-parser.pl: sub scan_dqstring {
       	while (1) {
      -		# slurp up non-special characters
      -		$s .= $1 if $$b =~ /\G([^"\$\\]+)/gc;
     -+		# slurp up non-special characters; count newlines
     -+		# inline so we don't need a catch-all counter that
     -+		# would miscount newlines from recursive $() parsing
     ++		# Slurp non-special characters; count newlines here because
     ++		# newlines inside $() are already counted by the recursive parse.
      +		if ($$b =~ /\G([^"\$\\]+)/gc) {
     -+			my $chunk = $1;
     -+			$self->{lineno} += () = $chunk =~ /\n/sg;
     -+			$s .= $chunk;
     ++			$s .= $1;
     ++			$self->{lineno} += $1 =~ tr/\n//;
      +		}
       		# handle special characters
       		last unless $$b =~ /\G(.)/sgc;
 6:  1527293f1c ! 5:  3a589ef738 t: lint and convert grep assertions to test_grep
     @@ Metadata
      Author: Michael Montalbo <[email protected]>
      
       ## Commit message ##
     -    t: lint and convert grep assertions to test_grep
     -
     -    Extend lint-style.pl with a rule that detects bare 'grep' used as a
     -    test assertion and converts it to test_grep.  test_grep prints the
     -    file contents on failure, making test debugging significantly easier.
     -
     -    parse_commands() is extended to split at shell structural tokens
     -    ({, }, (, ), |) and keywords (if, then, for, etc.), and each
     -    command gains a token_pos index so that rules can scan backward and
     -    forward in the token stream for context.
     -
     -    Three new functions implement the grep-assertion rule:
     -
     -     - is_filter_context() scans the surrounding tokens for pipes,
     -       control-flow keywords (if/elif/while/until), for-in value
     -       lists, and brace groups with output redirects.
     -
     -     - is_grep_assertion() classifies a grep command: convertible
     -       assertion (pattern and file present), filter (not an assertion),
     -       or missing file argument (flagged as a likely bug).
     -
     -     - check_bare_grep() ties them together and calls
     -       report_violation() with the appropriate fix.
     -
     -    The --fix mode handles:
     -     - Replacing 'grep' with 'test_grep'
     -     - Moving negation from '! grep' to 'test_grep !'
     -     - Stripping the -q flag (test_grep inherently checks match status)
     -
     -    Five files require '# lint-ok' annotations for intentional grep
     -    usage that cannot be mechanically converted: t1400 (packed-refs
     -    may not exist on reftable), t3901 (piped stdin via case block),
     -    t6437 (glob argument breaks test_grep's test -f check), t7450
     -    (file may not exist after failed MINGW clone), and t7527 ($?
     -    capture on the next line).
     -
     -    The test-lint-style scope is extended to include sourced test
     -    fragments in subdirectories (t5411/*.sh and similar) via a new
     -    TSOURCED variable.
     -
     -    Run '--fix' to convert all ~2800 grep assertions across ~340 files
     -    in the test suite.  test-lib-functions.sh and lib-rebase.sh are
     -    excluded from linting since they implement test infrastructure
     -    rather than test assertions.
     +    t: convert grep assertions to test_grep
     +
     +    Replace bare grep with test_grep in test assertions across the
     +    suite, including sourced test helpers (lib-*.sh, *-tests.sh).
     +    test_grep prints the contents of the file being searched on
     +    failure, making debugging easier than a bare grep which fails
     +    silently.
     +
     +    Only assertion-style greps are converted: grep used as a filter
     +    in pipelines, command substitutions, conditionals, or with
     +    redirected I/O is left as-is with a "# lint-ok" annotation.
     +    Existing '! test_grep' calls are rewritten to 'test_grep !' so
     +    that the diagnostic output is preserved on failure.
     +
     +    The conversion was generated using a grep-assertion linter
     +    (greplint.pl, added in the following commit) to identify bare
     +    grep calls at command position.  To reproduce:
     +
     +        # Step 1: mark bare greps that should not be converted
     +        sed -i '/! grep "$m" \.git\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \
     +            t/t1400-update-ref.sh
     +        sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \
     +            t/t3420-rebase-autostash.sh
     +        sed -i '/grep -vf before commits\.raw/s/$/ # lint-ok: data filter/' \
     +            t/t5326-multi-pack-bitmaps.sh
     +        sed -i '/! grep $d shallow-client\/\.git\/shallow/s/$/ # lint-ok: file may not exist after repack/' \
     +            t/t5537-fetch-shallow.sh
     +        sed -i '/grep -E "^\[0-9a-f\].*|| :/s/$/ # lint-ok: data filter/' \
     +            t/t5702-protocol-v2.sh
     +        sed -i '/! grep gitdir squatting-clone/s/$/ # lint-ok: file may not exist after failed clone/' \
     +            t/t7450-bad-git-dotfiles.sh
     +
     +        # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'
     +        # (must come before steps 3-4 so greplint does not see them)
     +        sed -i 's/! test_grep/test_grep !/' t/t0031-lockfile-pid.sh
     +        sed -i 's/! test_grep/test_grep !/' t/t5300-pack-object.sh
     +        sed -i 's/! test_grep/test_grep !/' t/t5319-multi-pack-index.sh
     +
     +        # Step 3: convert '! grep' -> 'test_grep !'
     +        perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |
     +        while IFS=: read f l; do
     +            sed -i "${l}s/! *grep/test_grep !/" "$f"
     +        done
     +
     +        # Step 4: convert remaining 'grep' -> 'test_grep'
     +        perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |
     +        while IFS=: read f l; do
     +            sed -i "${l}s/grep/test_grep/" "$f"
     +        done
     +
     +    To verify, run: make -C t test-greplint
      
          Signed-off-by: Michael Montalbo <[email protected]>
      
     - ## t/Makefile ##
     -@@ t/Makefile: test-lint-shell-syntax:
     - check-shell-parser:
     - 	@'$(PERL_PATH_SQ)' check-shell-parser.pl
     - 
     -+TSOURCED = $(sort $(wildcard t[0-9]*/*.sh))
     -+
     - test-lint-style:
     --	@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)
     -+	@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF) $(TSOURCED)
     - 
     - check-lint-style:
     - 	@rc=0; for t in $(LINT_STYLE_TESTS); do \
     -
       ## t/for-each-ref-tests.sh ##
      @@ t/for-each-ref-tests.sh: test_expect_success 'Verify descending sort' '
       
     @@ t/lib-httpd.sh: test_http_push_nonff () {
       
       	test_expect_success 'non-fast-forward push shows help message' '
      
     - ## t/lint-style.pl ##
     -@@
     - # Detection uses parsed tokens from the shared shell parser for
     - # correct handling of heredocs, $(...), pipes, and quoting.
     - # Fixes modify the original file text to preserve formatting.
     -+#
     -+# Architecture: the harness (LintParser, parse_commands) tokenizes
     -+# test bodies and splits them into commands.  Rules are independent
     -+# functions that examine each command and its surrounding token
     -+# context to decide if there is a violation.
     - 
     - use strict;
     - use warnings;
     -@@ t/lint-style.pl: do $_lib or die "$0: failed to load $_lib: $@$!\n";
     - # on each test body.  Per-file state (file name, raw lines, dirty
     - # flag) is stored on the instance before calling parse().
     - #
     --# Subroutines defined below (parse_commands, check_test_grep_negation,
     --# etc.) are in package main and called with the main:: prefix.
     --# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible
     --# across packages since 'package' does not introduce a new scope.
     -+# Subroutines defined below are in package main and called with
     -+# the main:: prefix.  File-scoped lexicals ($fix_mode, etc.) are
     -+# visible across packages since 'package' does not introduce a
     -+# new scope.
     - package LintParser;
     - our @ISA = ('ScriptParser');
     - 
     -@@ t/lint-style.pl: package main;
     - my $exit_code = 0;
     - my $has_fixable = 0;
     - 
     -+my %skip_file = map { $_ => 1 }
     -+	grep { m{(?:test-lib-functions|lib-rebase)\.sh$} } @ARGV;
     -+
     - sub err {
     - 	my ($file, $lineno, $line, $msg, %opts) = @_;
     - 	$line =~ s/^\s+//;
     -@@ t/lint-style.pl: sub err {
     - 	$exit_code = 1 unless $fix_mode && $opts{fixable};
     - }
     - 
     --# Report a lint violation found by a rule.  In --fix mode, apply
     --# the regex substitution on the raw line and report success.
     --# Otherwise just report.  Returns 1 if the line was modified.
     -+# Report a lint violation.  In --fix mode, apply the regex
     -+# substitution on the raw line.  Returns 1 if modified.
     - sub report_violation {
     - 	my ($file, $cmd, $line_ref, $match, $fix, $from) = @_;
     - 	my $lineno = $cmd->{lineno};
     -@@ t/lint-style.pl: sub report_violation {
     - 	return 0;
     - }
     - 
     -+# --- Harness: tokenize and split into commands ---
     -+#
     - # Split a token stream into commands at &&, ||, ;;, and \n.
     -+# Each command is {tokens => [...], lineno => N, token_pos => I}
     -+# where token_pos is the index in @all_tokens where the command's
     -+# first token appeared (so rules can look backward for context).
     - sub parse_commands {
     --	my ($content) = @_;
     --	my $parser = ShellParser->new(\$content);
     --	my @all_tokens = $parser->parse();
     --
     -+	my ($all_tokens) = @_;
     - 	my @commands;
     - 	my @current;
     - 	my $lineno = 1;
     -+	my $first_pos = 0;
     -+
     -+	my %shell_keyword;
     -+	@shell_keyword{qw(if then else elif fi for do done
     -+			   while until case in esac)} = ();
     - 
     --	for (my $ti = 0; $ti < @all_tokens; $ti++) {
     --		my $text = $all_tokens[$ti]->[0];
     -+	for (my $ti = 0; $ti < @$all_tokens; $ti++) {
     -+		my $text = $all_tokens->[$ti]->[0];
     - 		if ($text =~ /^(?:&&|\|\||;;|\n)$/) {
     -+			# Command separators: flush current command
     -+			if (@current) {
     -+				push @commands, {
     -+					tokens    => [@current],
     -+					lineno    => $lineno,
     -+					token_pos => $first_pos,
     -+				};
     -+				@current = ();
     -+			}
     -+		} elsif ($text =~ /^[{}()|]$/ || exists $shell_keyword{$text}) {
     -+			# Shell structural tokens and keywords:
     -+			# flush current command (these are boundaries,
     -+			# not part of the command's arguments)
     - 			if (@current) {
     - 				push @commands, {
     --					tokens => [@current],
     --					lineno => $lineno,
     -+					tokens    => [@current],
     -+					lineno    => $lineno,
     -+					token_pos => $first_pos,
     - 				};
     - 				@current = ();
     - 			}
     - 		} else {
     --			$lineno = $all_tokens[$ti]->[3]
     --				if !@current && defined $all_tokens[$ti]->[3];
     -+			if (!@current) {
     -+				# Record line number of the first token
     -+				$lineno = $all_tokens->[$ti]->[3]
     -+					if defined $all_tokens->[$ti]->[3];
     -+				$first_pos = $ti;
     -+			}
     - 			push @current, $text;
     - 		}
     - 	}
     - 	if (@current) {
     - 		push @commands, {
     --			tokens => [@current],
     --			lineno => $lineno,
     -+			tokens    => [@current],
     -+			lineno    => $lineno,
     -+			token_pos => $first_pos,
     - 		};
     - 	}
     - 	return @commands;
     - }
     - 
     - # --- Rule: '! test_grep' should be 'test_grep !' ---
     --# Shell-level negation suppresses test_grep's diagnostic output
     --# on failure.  Built-in negation preserves it.
     - sub check_test_grep_negation {
     --	my ($cmd, $file, $line_ref) = @_;
     -+	my ($cmd, $file, $line_ref, $all_tokens) = @_;
     - 	my @tokens = @{$cmd->{tokens}};
     - 	return unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';
     - 
     -@@ t/lint-style.pl: sub check_test_grep_negation {
     - 		qr/!\s*test_grep/, 'test_grep !', '! test_grep');
     - }
     - 
     --# Map parsed commands back to raw file lines for --fix.
     --# Detection uses parsed tokens (correct handling of quoting,
     --# heredocs, pipes) but fixes must modify the original text
     --# to preserve formatting.
     -+# --- Rule: bare 'grep' should be 'test_grep' ---
     -+
     -+# Check if this command is in a filter context by looking at
     -+# the surrounding tokens in the stream.  This is grep-rule
     -+# specific: it knows what contexts make a grep not an assertion.
     -+sub is_filter_context {
     -+	my ($all_tokens, $cmd) = @_;
     -+	my $pos = $cmd->{token_pos};
     -+
     -+	# Scan backward to the previous command separator.
     -+	# If we find '|', this command is part of a pipeline.
     -+	# If we find if/elif/while/until, it's a condition.
     -+	for (my $j = $pos - 1; $j >= 0; $j--) {
     -+		my $t = $all_tokens->[$j]->[0];
     -+		# Stop at command separators (but not \n after |)
     -+		last if $t =~ /^(?:&&|\|\||;;)$/;
     -+		if ($t eq "\n") {
     -+			# \n after | is a line continuation, keep scanning
     -+			next if $j > 0 && $all_tokens->[$j - 1]->[0] eq '|';
     -+			last;
     -+		}
     -+		return 1 if $t eq '|';
     -+		return 1 if $t =~ /^(?:if|elif|while|until)$/;
     -+		# for ... in ITEMS ... do: if we're between 'in' and 'do',
     -+		# we're in a value list, not a command
     -+		return 1 if $t eq 'in';
     -+	}
     -+
     -+	# Forward: pipe after command
     -+	for (my $j = $pos + @{$cmd->{tokens}}; $j < @$all_tokens; $j++) {
     -+		my $t = $all_tokens->[$j]->[0];
     -+		last if $t =~ /^(?:&&|\|\||;;|\n)$/;
     -+		return 1 if $t eq '|';
     -+	}
     -+
     -+	# { cmd; } >output
     -+	return 1 if is_in_redirected_brace($all_tokens, $pos);
     -+
     -+	return 0;
     -+}
     -+
     -+# Check if position $pos is inside a brace group whose output is
     -+# redirected: { grep ...; } >file.  Scan backward for the enclosing
     -+# '{', then forward for the matching '}', and check what follows it.
     -+sub is_in_redirected_brace {
     -+	my ($all_tokens, $pos) = @_;
     -+	my $brace_depth = 0;
     -+	for (my $j = $pos - 1; $j >= 0; $j--) {
     -+		my $t = $all_tokens->[$j]->[0];
     -+		$brace_depth++ if $t eq '}';
     -+		if ($t eq '{') {
     -+			return 0 if $brace_depth > 0;
     -+			$brace_depth--;
     -+			# Found our enclosing '{'. Find matching '}'
     -+			my $depth = 1;
     -+			for (my $k = $j + 1; $k < @$all_tokens; $k++) {
     -+				$depth++ if $all_tokens->[$k]->[0] eq '{';
     -+				$depth-- if $all_tokens->[$k]->[0] eq '}';
     -+				if ($depth == 0) {
     -+					my $after = $k + 1 < @$all_tokens ?
     -+						$all_tokens->[$k + 1]->[0] : '';
     -+					return $after =~ /^>{1,2}/;
     -+				}
     -+			}
     -+			return 0;
     -+		}
     -+	}
     -+	return 0;
     -+}
     -+
     -+# Classify a grep command: assertion, filter, or bug.
     -+#
     -+# Returns:
     -+#   1  assertion (PATTERN + FILE), can be converted to test_grep
     -+#   0  not a grep, or grep used as a filter
     -+#  -1  likely bug (e.g., missing file argument)
     -+sub is_grep_assertion {
     -+	my ($cmd, $all_tokens) = @_;
     -+	my @tokens = @{$cmd->{tokens}};
     -+
     -+	# Find grep, possibly after "!"
     -+	my $i = 0;
     -+	$i++ if $tokens[0] eq '!';
     -+	return 0 unless defined $tokens[$i] && $tokens[$i] eq 'grep';
     -+	return 0 if grep { $_ eq 'test_grep' } @tokens;
     -+
     -+	# Check surrounding context (pipes, control flow, brace redirects)
     -+	return 0 if is_filter_context($all_tokens, $cmd);
     -+
     -+	$i++;  # skip 'grep'
     -+
     -+	# Check grep's own flags and arguments
     -+	my @positional;
     -+	my $has_pattern_flag = 0;
     -+	my $end_of_flags = 0;
     -+	while ($i < @tokens) {
     -+		my $tok = $tokens[$i];
     -+		if ($tok eq '|' || $tok eq '<') {
     -+			return 0;
     -+		}
     -+		if ($tok =~ /^>{1,2}$/) {
     -+			# Stdout redirect means filter (grep ... >out).
     -+			# Stderr redirect (2>err) is fine: skip the fd
     -+			# and the target, and keep classifying.
     -+			my $prev = $i > 0 ? $tokens[$i - 1] : '';
     -+			return 0 unless $prev =~ /^\d+$/ && $prev >= 2;
     -+			pop @positional if @positional && $positional[-1] eq $prev;
     -+			$i += 2;
     -+			next;
     -+		}
     -+		if (!$end_of_flags && $tok =~ /^-\w*[clLrR]/) {
     -+			return 0;
     -+		}
     -+		if (!$end_of_flags && $tok eq '--') {
     -+			$end_of_flags = 1;
     -+		} elsif (!$end_of_flags && $tok =~ /^-\w*[ef]$/) {
     -+			$has_pattern_flag = 1;
     -+			$i++;
     -+		} elsif (!$end_of_flags && $tok =~ /^-/) {
     -+			# skip other flags
     -+		} else {
     -+			push @positional, $tok;
     -+		}
     -+		$i++;
     -+	}
     -+
     -+	my $need = $has_pattern_flag ? 1 : 2;
     -+	return 0 if !@positional && !$has_pattern_flag;
     -+	return -1 if @positional < $need;
     -+	return 0 if $positional[-1] =~ /^-/;
     -+	return 1;
     -+}
     -+
     -+sub check_bare_grep {
     -+	my ($cmd, $file, $line_ref, $all_tokens) = @_;
     -+	my @tokens = @{$cmd->{tokens}};
     -+
     -+	my $result = is_grep_assertion($cmd, $all_tokens);
     -+	return unless $result;
     -+
     -+	if ($result == -1) {
     -+		err $file, $cmd->{lineno}, join(' ', @tokens),
     -+			"grep assertion appears to be missing a file argument";
     -+		return 0;
     -+	}
     -+
     -+	# Determine negation and -q flag
     -+	my $negated = $tokens[0] eq '!';
     -+	my $has_q = 0;
     -+	my ($pre_q, $post_q) = ('', '');
     -+	for my $tok (@tokens) {
     -+		if ($tok =~ /^-(\w*)q(\w*)$/) {
     -+			$has_q = 1;
     -+			($pre_q, $post_q) = ($1, $2);
     -+			last;
     -+		}
     -+		last if $tok !~ /^-/ && $tok ne '!' && $tok ne 'grep';
     -+	}
     -+
     -+	# Build the replacement
     -+	my $fix = "test_grep";
     -+	$fix .= " !" if $negated;
     -+	if ($has_q) {
     -+		my $rest = "$pre_q$post_q";
     -+		$fix .= " -$rest" if $rest;
     -+	}
     -+
     -+	# Build the match pattern
     -+	my $neg_match = $negated ? '!\s*' : '\b';
     -+	my $neg_from  = $negated ? '! '   : '';
     -+	my ($match, $from);
     -+	if ($has_q) {
     -+		$match = qr/${neg_match}grep\s+-\w*q\w*/;
     -+		$from  = "${neg_from}grep -${pre_q}q${post_q}";
     -+	} else {
     -+		$match = qr/${neg_match}grep\b/;
     -+		$from  = "${neg_from}grep";
     -+	}
     -+
     -+	return report_violation($file, $cmd, $line_ref,
     -+		$match, $fix, $from);
     -+}
     -+
     -+# --- Harness: LintParser.check_test ---
     -+#
     -+# Called by ScriptParser::parse_cmd for each test_expect_success
     -+# or test_expect_failure block.  Extracts the body, tokenizes it,
     -+# splits into commands, and runs each rule.
     - package LintParser;
     - 
     - sub check_test {
     --	# Called by ScriptParser::parse_cmd for each test_expect_success
     --	# or test_expect_failure block.
     - 	my $self = shift @_;
     - 	my $title = ScriptParser::unwrap(shift @_);
     - 
     -@@ t/lint-style.pl: sub check_test {
     - 	}
     - 	return unless $body;
     - 
     -+	# Tokenize the body once; commands and rules share the stream
     -+	my $parser = ShellParser->new(\$body);
     -+	my @all_tokens = $parser->parse();
     -+	my @commands = main::parse_commands(\@all_tokens);
     -+
     - 	# Map each command back to its file line number.
     - 	# $lineno_base is where the body starts in the file;
     - 	# $cmd->{lineno} is relative to the body (starting at 1).
     - 	my $raw_lines = $self->{raw_lines};
     --	for my $cmd (main::parse_commands($body)) {
     -+	for my $cmd (@commands) {
     - 		my $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;
     - 		$cmd->{lineno} = $ln;
     - 		next unless $ln >= 1 && $ln <= @$raw_lines;
     - 		next if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;
     - 
     --		if (main::check_test_grep_negation($cmd, $self->{file}, \$raw_lines->[$ln - 1])) {
     --			$self->{dirty} = 1;
     --		}
     -+		my $line_ref = \$raw_lines->[$ln - 1];
     -+		# Stop after the first fix: later rules should not
     -+		# re-match against already-modified text.
     -+		my $modified = 0;
     -+		$modified ||= main::check_test_grep_negation(
     -+			$cmd, $self->{file}, $line_ref, \@all_tokens);
     -+		$modified ||= main::check_bare_grep(
     -+			$cmd, $self->{file}, $line_ref, \@all_tokens);
     -+		$self->{dirty} = 1 if $modified;
     - 	}
     - }
     - 
     - package main;
     - 
     - for my $file (@ARGV) {
     -+	next if $skip_file{$file};
     - 	# :unix:crlf strips \r on Windows (same as chainlint.pl)
     - 	open(my $fh, '<:unix:crlf', $file) or die "$0: $file: $!\n";
     - 	my @raw_lines = <$fh>;
     -
     - ## t/lint-style/grep-assert.expect (new) ##
     -@@
     -+lint-style/grep-assert.test:2: error: replace 'grep' with 'test_grep': grep "pattern" actual
     -+lint-style/grep-assert.test:3: error: replace 'grep' with 'test_grep': grep -E "extended" actual
     -+lint-style/grep-assert.test:4: error: replace 'grep' with 'test_grep': grep -e "explicit" actual
     -+lint-style/grep-assert.test:5: error: replace 'grep' with 'test_grep': grep -f patterns.txt actual
     -+lint-style/grep-assert.test:6: error: replace 'grep' with 'test_grep': grep -Fe "fixed-explicit" actual
     -+lint-style/grep-assert.test:7: error: replace 'grep' with 'test_grep': grep "^-looks-like-flag" actual
     -+lint-style/grep-assert.test:8: error: replace 'grep' with 'test_grep': grep -v "inverted" actual
     -+lint-style/grep-assert.test:9: error: replace 'grep' with 'test_grep': grep -- "-e" actual
     -+lint-style/grep-assert.test:10: error: replace 'grep' with 'test_grep': grep "with-stderr-redirect" actual 2 > err
     -+lint-style/grep-assert.test:12: error: replace 'grep' with 'test_grep': grep "after-or" actual
     -+lint-style/grep-assert.test:17: error: replace 'grep' with 'test_grep': grep "after-pipe" actual
     -+lint-style/grep-assert.test:22: error: replace 'grep' with 'test_grep': grep "inside-case" actual
     -+hint: run with --fix to apply the suggested replacements.
     -
     - ## t/lint-style/grep-assert.test (new) ##
     -@@
     -+test_expect_success 'grep assertions' '
     -+	grep "pattern" actual &&
     -+	grep -E "extended" actual &&
     -+	grep -e "explicit" actual &&
     -+	grep -f patterns.txt actual &&
     -+	grep -Fe "fixed-explicit" actual &&
     -+	grep "^-looks-like-flag" actual &&
     -+	grep -v "inverted" actual &&
     -+	grep -- "-e" actual &&
     -+	grep "with-stderr-redirect" actual 2>err &&
     -+	cmd ||
     -+	grep "after-or" actual
     -+'
     -+
     -+test_expect_success 'pipe only suppresses first command' '
     -+	cmd |
     -+	grep "piped" && grep "after-pipe" actual
     -+'
     -+
     -+test_expect_success 'case pattern does not hide assertion' '
     -+	case foo in
     -+	*) grep "inside-case" actual ;;
     -+	esac
     -+'
     -
     - ## t/lint-style/grep-fix.expect (new) ##
     -@@
     -+test_expect_success 'all fixable forms' '
     -+	test_grep "pattern" actual &&
     -+	test_grep -E "extended" actual &&
     -+	test_grep "quiet" actual &&
     -+	test_grep -F "combined" actual &&
     -+	test_grep -i "quiet-insensitive" actual &&
     -+	test_grep ! "negated" actual &&
     -+	test_grep ! "neg-quiet" actual &&
     -+	test_grep ! "shell-neg" actual &&
     -+	test_grep -e "explicit" actual &&
     -+	test_grep -Fe "fixed-explicit" actual &&
     -+	test_grep -f patterns.txt actual &&
     -+	test_grep -- "-e" actual &&
     -+	test_grep "continuation" \
     -+		actual
     -+'
     -
     - ## t/lint-style/grep-fix.test (new) ##
     -@@
     -+test_expect_success 'all fixable forms' '
     -+	grep "pattern" actual &&
     -+	grep -E "extended" actual &&
     -+	grep -q "quiet" actual &&
     -+	grep -qF "combined" actual &&
     -+	grep -qi "quiet-insensitive" actual &&
     -+	! grep "negated" actual &&
     -+	! grep -q "neg-quiet" actual &&
     -+	! test_grep "shell-neg" actual &&
     -+	grep -e "explicit" actual &&
     -+	grep -Fe "fixed-explicit" actual &&
     -+	grep -f patterns.txt actual &&
     -+	grep -- "-e" actual &&
     -+	grep "continuation" \
     -+		actual
     -+'
     -
     - ## t/lint-style/grep-missing-file.expect (new) ##
     -@@
     -+lint-style/grep-missing-file.test:2: error: grep assertion appears to be missing a file argument: grep "pattern"
     -+lint-style/grep-missing-file.test:3: error: grep assertion appears to be missing a file argument: ! grep "negated"
     -+lint-style/grep-missing-file.test:4: error: grep assertion appears to be missing a file argument: grep -e "pattern-flag-no-file"
     -+lint-style/grep-missing-file.test:5: error: grep assertion appears to be missing a file argument: grep -f patterns.txt
     -
     - ## t/lint-style/grep-missing-file.test (new) ##
     -@@
     -+test_expect_success 'grep missing file argument' '
     -+	grep "pattern" &&
     -+	! grep "negated" &&
     -+	grep -e "pattern-flag-no-file" &&
     -+	grep -f patterns.txt
     -+'
     -
     - ## t/lint-style/grep-negated.expect (new) ##
     -@@
     -+lint-style/grep-negated.test:2: error: replace '! grep' with 'test_grep !': ! grep "pattern" actual
     -+lint-style/grep-negated.test:3: error: replace '! grep' with 'test_grep !': ! grep -i "insensitive" actual
     -+lint-style/grep-negated.test:7: error: replace '! grep -q' with 'test_grep !': ! grep -q "pattern" actual
     -+lint-style/grep-negated.test:8: error: replace '! grep -qF' with 'test_grep ! -F': ! grep -qF "combined" actual
     -+hint: run with --fix to apply the suggested replacements.
     -
     - ## t/lint-style/grep-negated.test (new) ##
     -@@
     -+test_expect_success 'negated grep' '
     -+	! grep "pattern" actual &&
     -+	! grep -i "insensitive" actual
     -+'
     -+
     -+test_expect_success 'negated grep -q' '
     -+	! grep -q "pattern" actual &&
     -+	! grep -qF "combined" actual
     -+'
     -
     - ## t/lint-style/grep-not-assert.expect (new) ##
     -
     - ## t/lint-style/grep-not-assert.test (new) ##
     -@@
     -+test_expect_success 'grep used as filter (not assertion)' '
     -+	grep "pattern" file | wc -l &&
     -+	grep "pattern" file >output &&
     -+	grep "pattern" file 1>output &&
     -+	grep -c "count" file &&
     -+	grep -ci "count-insensitive" file &&
     -+	grep -l "list" file &&
     -+	grep -rl "recursive-list" dir &&
     -+	grep -L "list-without" file1 file2 &&
     -+	result=$(grep "pattern" file) &&
     -+	result=$(echo $(grep "nested-subshell" file)) &&
     -+	grep "pattern" <stdin &&
     -+	grep "pattern" file && # lint-ok
     -+	cmd | grep "pattern-only" &&
     -+	cmd |
     -+	grep "cross-line-pipe" &&
     -+	grep -r "recursive" dir
     -+'
     -+
     -+test_expect_success 'grep in control flow (not assertion)' '
     -+	if grep "condition" file
     -+	then
     -+		echo yes
     -+	elif grep "other-condition" file
     -+	then
     -+		echo no
     -+	fi
     -+'
     -+
     -+test_expect_success 'grep in brace group with redirect' '
     -+	{ grep "captured" out; } >result
     -+'
     -+
     -+test_expect_success 'grep in for-in value list' '
     -+	for cmd in grep sed awk; do
     -+		echo "$cmd"
     -+	done
     -+'
     -+
     -+test_expect_success 'grep in subshell' '
     -+	(cd sub && grep "pattern" file >output) &&
     -+	(cmd | grep "piped-in-subshell")
     -+'
     -
       ## t/pack-refs-tests.sh ##
      @@ t/pack-refs-tests.sh: test_expect_success 'delete ref while another dangling packed ref' '
       test_expect_success 'pack ref directly below refs/' '
     @@ t/t0030-stripspace.sh: test_expect_success 'strip comments with changed comment
       
       test_expect_success '-c with single line' '
      
     + ## t/t0031-lockfile-pid.sh ##
     +@@ t/t0031-lockfile-pid.sh: test_expect_success 'PID info not shown by default' '
     + 		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
     + 	)
     + '
     + 
     +
       ## t/t0040-parse-options.sh ##
      @@ t/t0040-parse-options.sh: test_expect_success 'non ambiguous option (after two options it abbreviates)' '
       
     @@ t/t0052-simple-ipc.sh: test_expect_success 'servers cannot share the same path'
       	test-tool simple-ipc send --token=big >actual &&
       	test_line_count -ge 10000 actual &&
      -	grep -q "big: [0]*9999\$" actual
     -+	test_grep "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 "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 "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
     @@ t/t0203-gettext-setlocale-sanity.sh: test_expect_success 'git show a ISO-8859-1
       	git show >out 2>err &&
       	test_must_be_empty err &&
      -	grep -q "iso-c-commit" out
     -+	test_grep "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' '
     @@ t/t0203-gettext-setlocale-sanity.sh: test_expect_success GETTEXT_LOCALE 'git sho
       	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 "iso-utf8-commit" out
     ++	test_grep -q "iso-utf8-commit" out
       '
       
       test_done
     @@ t/t0410-partial-clone.sh: test_expect_success TTY 'promisor.quiet=unconfigured s
      +	test_grep "Receiving objects" err
       '
       
     - . "$TEST_DIRECTORY"/lib-httpd.sh
     + test_expect_success 'promisor.quiet from submodule repo is honored' '
      @@ t/t0410-partial-clone.sh: test_expect_success 'fetching of missing objects from an HTTP server' '
       	test_line_count = 1 promisorlist &&
       	IDX=$(sed "s/promisor$/idx/" promisorlist) &&
     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree will not th
       	read_tree_u_must_fail -m -u HEAD^ &&
       	test_path_is_file init.t &&
      -	grep -q dirty init.t
     -+	test_grep dirty init.t
     ++	test_grep -q dirty init.t
       '
       
       test_expect_success 'read-tree will not throw away dirty changes, sparse' '
     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree will not th
       	read_tree_u_must_fail -m -u HEAD^ &&
       	test_path_is_file init.t &&
      -	grep -q dirty init.t
     -+	test_grep dirty init.t
     ++	test_grep -q dirty init.t
       '
       
       test_expect_success 'read-tree updates worktree, dirty case' '
     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree updates wor
       	echo dirty >init.t &&
       	read_tree_u_must_fail -m -u HEAD^ &&
      -	grep -q dirty init.t &&
     -+	test_grep dirty init.t &&
     ++	test_grep -q dirty init.t &&
       	rm init.t
       '
       
     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree removes wor
       	echo dirty >added &&
       	read_tree_u_must_succeed -m -u HEAD^ &&
      -	grep -q dirty added
     -+	test_grep dirty added
     ++	test_grep -q dirty added
       '
       
       test_expect_success 'read-tree adds to worktree, absent case' '
     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree adds to wor
       	echo dirty >sub/added &&
       	read_tree_u_must_succeed -u -m HEAD^ &&
      -	grep -q dirty sub/added
     -+	test_grep dirty sub/added
     ++	test_grep -q dirty sub/added
       '
       
       test_expect_success 'index removal and worktree narrowing at the same time' '
     @@ t/t3310-notes-merge-manual-resolve.sh: EOF
       	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 refs/notes/m merge_commit_msg &&
     -+	test_grep 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 "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 "$sha1" merge_commit_msg ||
     ++		test_grep -q "$sha1" merge_commit_msg ||
       		exit 1
       	done ) &&
       	# Verify contents of merge result
     @@ t/t3310-notes-merge-manual-resolve.sh: EOF
       	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 refs/notes/m merge_commit_msg &&
     -+	test_grep 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 "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 "$sha1" merge_commit_msg ||
     ++		test_grep -q "$sha1" merge_commit_msg ||
       		exit 1
       	done ) &&
       	# Verify contents of merge result
     @@ t/t3404-rebase-interactive.sh: test_expect_success 'rebase -i --root retain root
       	) &&
       	git cat-file commit HEAD >output &&
      -	grep -q "^author Twerp Snog" output &&
     -+	test_grep "^author Twerp Snog" output &&
     ++	test_grep -q "^author Twerp Snog" output &&
       	git cat-file commit HEAD >actual &&
      -	grep -q "^different author$" actual
     -+	test_grep "^different author$" actual
     ++	test_grep -q "^different author$" actual
       '
       
       test_expect_success 'rebase -i --root temporary sentinel commit' '
     @@ t/t3420-rebase-autostash.sh: testrebase () {
       		test_when_finished git stash drop &&
       		test_path_is_missing $dotest/autostash &&
      -		! grep dirty file3 &&
     ++		! grep dirty file3 && # lint-ok: file may not exist after --quit
       		git stash show -p >actual &&
       		test_cmp expect actual &&
       		git reset --hard &&
     @@ t/t3501-revert-cherry-pick.sh: test_expect_success 'cherry-pick works with dirty
       	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 "^modified$" renamed
     ++	test_grep -q "^modified$" renamed
       '
       
       test_expect_success 'advice from failed revert' '
     @@ t/t3800-mktag.sh: test_expect_success 'invalid header entry config & fsck' '
       cat >tag.sig <<EOF
      
       ## t/t3901-i18n-patch.sh ##
     -@@ t/t3901-i18n-patch.sh: check_encoding () {
     - 		git cat-file commit HEAD~$j |
     - 		case "$header" in
     - 		8859)
     --			grep "^encoding ISO8859-1" ;;
     -+			grep "^encoding ISO8859-1" ;; # lint-ok: piped stdin via case
     - 		*)
     --			ret=0; grep "^encoding ISO8859-1" || ret=$?
     -+			ret=0; grep "^encoding ISO8859-1" || ret=$? # lint-ok: piped stdin via case
     - 			test "$ret" != 0 ;;
     - 		esac || return 1
     - 		j=$i
      @@ t/t3901-i18n-patch.sh: test_expect_success 'format-patch output (ISO-8859-1)' '
       
       	git format-patch --stdout main..HEAD^ >out-l1 &&
     @@ t/t4000-diff-format.sh: test_expect_success 'git diff-files --no-patch --patch s
       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 "^: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
       '
     @@ t/t5300-pack-object.sh: test_expect_success !PTHREADS,!FAIL_PREREQS \
       '
       
       test_expect_success 'pack-objects in too-many-packs mode' '
     -@@ t/t5300-pack-object.sh: test_expect_success '--path-walk pack everything' '
     +@@ t/t5300-pack-object.sh: test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat
     + 
     + 	# --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 &&
     @@ t/t5319-multi-pack-index.sh: test_expect_success 'git-fsck incorrect offset' '
       '
       
       test_expect_success 'corrupt MIDX is not reused' '
     -@@ t/t5319-multi-pack-index.sh: test_expect_success 'usage shown without sub-command' '
     +@@ t/t5319-multi-pack-index.sh: test_expect_success 'load reverse index when missing .idx, .pack' '
     + 
     + 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 &&
     @@ t/t5326-multi-pack-bitmaps.sh: test_midx_bitmap_cases () {
       
       			(
      -				grep -vf before commits.raw &&
     -+				test_grep -vf before commits.raw &&
     ++				grep -vf before commits.raw && # lint-ok: data filter
       				# mark missing commits as preferred
       				sed "s/^/+/" before
       			) >snapshot &&
     @@ t/t5409-colorize-remote-messages.sh: test_expect_success 'setup' '
       
       test_expect_success 'disallow (color) control sequences in sideband' '
      
     - ## t/t5411/test-0013-bad-protocol.sh ##
     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
     - 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
     --	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
     -+	test_grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-\EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
     - 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
     --	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
     -+	test_grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
     - 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
     - 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
     - 	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -
     - ## t/t5411/test-0014-bad-protocol--porcelain.sh ##
     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive: bad protocol (hook --die-read-version, $PROTO
     - 	Done
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
     --	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-read-version option" out-$test_count &&
     -+	test_grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive: bad protocol (hook --die-write-version, $PROT
     - 	Done
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
     --	grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-write-version option" out-$test_count &&
     -+	test_grep "remote: error: fail to negotiate version with proc-receive hook" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive: bad protocol (hook --die-read-commands, $PROT
     - 	Done
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-read-commands option" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive: bad protocol (hook --die-read-push-options, $
     - 	Done
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-read-push-options option" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success "proc-receive: bad protocol (hook --die-write-report, $PROTO
     - 	Done
     - 	EOF
     - 	test_cmp expect actual &&
     --	grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
     -+	test_grep "remote: fatal: die with the --die-write-report option" out-$test_count &&
     - 
     - 	test_cmp_refs -C "$upstream" <<-EOF
     - 	<COMMIT-A> refs/heads/main
     -
       ## t/t5500-fetch-pack.sh ##
      @@ t/t5500-fetch-pack.sh: test_expect_success 'single given branch clone' '
       	GIT_TRACE2_EVENT="$(pwd)/branch-a/trace2_event" \
     @@ t/t5516-fetch-push.sh: test_expect_success 'push --porcelain' '
       	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 ! Done .git/bar
     ++	test_grep ! -q Done .git/bar
       '
       
       test_expect_success 'push --porcelain rejected' '
     @@ t/t5520-pull.sh: test_expect_success '--rebase with rebase.autostash succeeds on
       	git -C dst pull --rebase >actual 2>&1 &&
      -	grep -q "Fast-forward" actual &&
      -	grep -q "Applied autostash." actual
     -+	test_grep "Fast-forward" actual &&
     -+	test_grep "Applied autostash." actual
     ++	test_grep -q "Fast-forward" actual &&
     ++	test_grep -q "Applied autostash." actual
       '
       
       test_expect_success '--rebase with conflicts shows advice' '
     @@ t/t5537-fetch-shallow.sh: test_expect_success '.git/shallow is edited by repack'
       	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 &&
     ++	! grep $d shallow-client/.git/shallow && # lint-ok: file may not exist after repack
       
       	git -C shallow-server branch branch-orig $d &&
       	git -C shallow-client fetch --prune --depth=2 \
     @@ t/t5702-protocol-v2.sh: test_expect_success 'when server does not send "ready",
       	test_grep "expected no other sections to be sent after no .ready." err
       '
       
     +@@ t/t5702-protocol-v2.sh: test_expect_success 'part of packfile response provided as URI' '
     + 	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
      @@ t/t5702-protocol-v2.sh: test_expect_success 'packfile-uri path redacted in trace' '
       		-c fetch.uriprotocols=http,https \
       		clone "$HTTPD_URL/smart/http_parent" http_child &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to does not change
       	git config branch.from-main.merge > actual &&
       	git rev-parse from-topic_2 >actual2 &&
      -	grep -q "^refs/heads/main$" actual &&
     -+	test_grep "^refs/heads/main$" actual &&
     ++	test_grep -q "^refs/heads/main$" actual &&
       	cmp expect2 actual2
       '
       
     @@ t/t6423-merge-rename-directories.sh: test_expect_success '5c: Transitive rename
       		test_path_is_missing x/d &&
       		test_path_is_file y/d &&
      -		grep -q "<<<<" y/d  # conflict markers should be present
     -+		test_grep "<<<<" y/d  # conflict markers should be present
     ++		test_grep -q "<<<<" y/d  # conflict markers should be present
       	)
       '
       
     @@ t/t6423-merge-rename-directories.sh: test_expect_success '9e: N-to-1 whammo' '
      -		grep -q dir2/yo error_line &&
      -		grep -q dir3/yo error_line &&
      -		grep -q dirN/yo error_line &&
     -+		test_grep dir1/yo error_line &&
     -+		test_grep dir2/yo error_line &&
     -+		test_grep dir3/yo error_line &&
     -+		test_grep 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 &&
     @@ t/t6423-merge-rename-directories.sh: test_expect_success '11b: Avoid losing dirt
       		test_grep "error: Your local changes to the following files would be overwritten by merge" err &&
       
      -		grep -q stuff z/c &&
     -+		test_grep stuff z/c &&
     ++		test_grep -q stuff z/c &&
       		test_seq 1 10 >expected &&
       		echo stuff >>expected &&
       		test_cmp expected z/c
     @@ t/t6423-merge-rename-directories.sh: test_expect_success '11c: Avoid losing not-
       		test_grep "error: Your local changes to the following files would be overwritten by merge" err &&
       
      -		grep -q stuff y/c &&
     -+		test_grep stuff y/c &&
     ++		test_grep -q stuff y/c &&
       		test_seq 1 10 >expected &&
       		echo stuff >>expected &&
       		test_cmp expected y/c &&
     @@ t/t6423-merge-rename-directories.sh: test_expect_success '11d: Avoid losing not-
       		test_grep "error: Your local changes to the following files would be overwritten by merge" err &&
       
      -		grep -q stuff z/c &&
     -+		test_grep stuff z/c &&
     ++		test_grep -q stuff z/c &&
       		test_seq 1 10 >expected &&
       		echo stuff >>expected &&
       		test_cmp expected z/c
     @@ t/t6437-submodule-merge.sh: test_expect_success 'merging should fail for changes
       	)
       '
       
     -@@ t/t6437-submodule-merge.sh: test_expect_success 'file/submodule conflict' '
     - 		# directory, though, so just grep for its content in all
     - 		# files, and ignore "grep: path: Is a directory" message
     - 		echo Checking if contents from B:path showed up anywhere &&
     --		grep -q content * 2>/dev/null
     -+		grep -q content * 2>/dev/null # lint-ok: glob arg breaks test_grep
     - 	)
     - '
     - 
      @@ t/t6437-submodule-merge.sh: test_expect_success 'merging should fail with no merge base' '
       	git commit -m "b" &&
       	test_must_fail git merge a 2>actual &&
     @@ t/t7012-skip-worktree-writing.sh: test_expect_success 'read-tree updates worktre
       	echo dirty >> init.t &&
       	test_must_fail git read-tree -m -u HEAD^ &&
      -	grep -q dirty init.t &&
     -+	test_grep 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
       '
     @@ t/t7012-skip-worktree-writing.sh: test_expect_success 'read-tree removes worktre
       	echo dirty >> added &&
       	test_must_fail git read-tree -m -u HEAD^ &&
      -	grep -q dirty added &&
     -+	test_grep dirty added &&
     ++	test_grep -q dirty added &&
       	test "$(git ls-files -t added)" = "S added" &&
       	git update-index --no-skip-worktree added
       '
     @@ t/t7450-bad-git-dotfiles.sh: test_expect_success WINDOWS 'prevent git~1 squattin
       			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
     -+		! grep gitdir squatting-clone/d/a/git~2 # lint-ok: file may not exist
     ++		! grep gitdir squatting-clone/d/a/git~2 # lint-ok: file may not exist after failed clone
       	fi
       '
       
     @@ t/t7501-commit-basic-functionality.sh: test_expect_success 'editor not invoked i
       	EDITOR=./editor git commit -a -F msg &&
       	git show -s --pretty=format:%s >subject &&
      -	grep -q good subject &&
     -+	test_grep 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 good subject
     ++	test_grep -q good subject
       '
       
       test_expect_success 'partial commit that involves removal (1)' '
     @@ t/t7501-commit-basic-functionality.sh: test_expect_success 'amend does not add s
       
       test_expect_success 'commit complains about completely bogus dates' '
      @@ t/t7501-commit-basic-functionality.sh: test_expect_success 'git commit <file> with dirty index' '
     + 	git add chz &&
     + 	git commit elif -m "tacocat is a palindrome" &&
       	git show --stat >stat &&
     - 	grep elif stat &&
     +-	grep elif stat &&
     ++	test_grep elif stat &&
       	git diff --cached >diff &&
      -	grep chz diff
      +	test_grep chz diff
     @@ t/t7510-signed-commit.sh: test_expect_success GPG 'verify and show signatures' '
      -	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 -F -e "No public key" -e "public key not found" 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' '
     @@ t/t7519-status-fsmonitor.sh: test_expect_success 'setup' '
       cat >expect <<EOF &&
      
       ## t/t7527-builtin-fsmonitor.sh ##
     -@@ t/t7527-builtin-fsmonitor.sh: verify_fsmonitor_works () {
     - 			--token 0 >/dev/null 2>&1
     - 	maybe_timeout 5 \
     - 		git -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null
     --	! grep -q "cookie_wait timed out" "$PWD/smoke.trace" 2>/dev/null
     -+	! grep -q "cookie_wait timed out" "$PWD/smoke.trace" 2>/dev/null # lint-ok: $? capture
     - 	ret=$?
     - 	rm -rf test_fsmonitor_smoke smoke.trace
     - 	return $ret
      @@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'implicit daemon start' '
       	GIT_TRACE2_EVENT="$PWD/.git/trace" \
       		test-tool -C test_implicit fsmonitor-client query --token 0 >actual &&
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
       
      -	grep -q "AAA.*pos 0" "$PWD/subdir_case_wrong.log1" &&
      -	grep -q "zzz.*pos 6" "$PWD/subdir_case_wrong.log1" &&
     -+	test_grep "AAA.*pos 0" "$PWD/subdir_case_wrong.log1" &&
     -+	test_grep "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 "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 "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
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
      -	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 " M AAA" "$PWD/subdir_case_wrong.out" &&
     -+	test_grep " M zzz" "$PWD/subdir_case_wrong.out" &&
     -+	test_grep " 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' '
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
      -	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 "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try1.log" &&
     -+	test_grep "fsmonitor_refresh_callback.*file-3-a.*pos 4"  "$PWD/file_case_wrong-try1.log" &&
     -+	test_grep "fsmonitor_refresh_callback.*FILE-4-A.*pos 6"  "$PWD/file_case_wrong-try1.log" &&
     -+	test_grep "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.
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
       	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 "H dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf1.out" &&
     -+	test_grep "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
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
      -	! 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 ! "fsmonitor_refresh_callback.*FILE-3-A.*pos" "$PWD/file_case_wrong-try2.log" &&
     -+	test_grep ! "fsmonitor_refresh_callback.*file-3-a.*pos" "$PWD/file_case_wrong-try2.log" &&
     -+	test_grep ! "fsmonitor_refresh_callback.*FILE-4-A.*pos" "$PWD/file_case_wrong-try2.log" &&
     -+	test_grep ! "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.
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
       	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 "h dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf2.out" &&
     -+	test_grep "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
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor
       
       	# 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 "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 "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 "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try3.log" &&
     -+	test_grep "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 " M dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-try3.out" &&
     -+	test_grep " 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
     @@ t/t7700-repack.sh: test_expect_success 'repack --keep-pack' '
       		ls .git/objects/pack/*.pack >new-counts &&
      -		grep -q $P1 new-counts &&
      -		grep -q $P4 new-counts &&
     -+		test_grep $P1 new-counts &&
     -+		test_grep $P4 new-counts &&
     ++		test_grep -q $P1 new-counts &&
     ++		test_grep -q $P4 new-counts &&
       		test_line_count = 3 new-counts &&
       		git fsck &&
       
     @@ t/t7703-repack-geometric.sh: test_expect_success '--geometric with small-pack ro
       		test_line_count = 3 after &&
       		comm -3 small before | tr -d "\t" >large &&
      -		grep -qFf large after
     -+		test_grep -Ff large after
     ++		test_grep -qFf large after
       	)
       '
       
     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'clone to target directory wi
       	test ! -d trunk &&
       	git svn init "$svnrepo"/project/trunk trunk 2>warning &&
      -	! grep -q prefix warning &&
     -+	test_grep ! prefix warning &&
     ++	test_grep ! -q prefix warning &&
       	rm -rf trunk &&
       	rm -f warning
       	'
     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'init without -s/-T/-b/-t doe
       	test ! -d trunk &&
       	git svn clone "$svnrepo"/project/trunk 2>warning &&
      -	! grep -q prefix warning &&
     -+	test_grep ! prefix warning &&
     ++	test_grep ! -q prefix warning &&
       	rm -rf trunk &&
       	rm -f warning
       	'
     @@ t/t9117-git-svn-init-clone.sh: EOF
       	test ! -d project &&
       	git svn init -s "$svnrepo"/project project 2>warning &&
      -	! grep -q prefix warning &&
     -+	test_grep ! prefix warning &&
     ++	test_grep ! -q prefix warning &&
       	test_svn_configured_prefix "origin/" &&
       	rm -rf project &&
       	rm -f warning
     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'init with -s/-T/-b/-t assume
       	test ! -d project &&
       	git svn clone -s "$svnrepo"/project 2>warning &&
      -	! grep -q prefix warning &&
     -+	test_grep ! prefix warning &&
     ++	test_grep ! -q prefix warning &&
       	test_svn_configured_prefix "origin/" &&
       	rm -rf project &&
       	rm -f warning
     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'clone with -s/-T/-b/-t assum
       	test ! -d project &&
       	git svn init -s "$svnrepo"/project project --prefix "" 2>warning &&
      -	! grep -q prefix warning &&
     -+	test_grep ! prefix warning &&
     ++	test_grep ! -q prefix warning &&
       	test_svn_configured_prefix "" &&
       	rm -rf project &&
       	rm -f warning
     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'init with -s/-T/-b/-t and --
       	test ! -d project &&
       	git svn clone -s "$svnrepo"/project --prefix "" 2>warning &&
      -	! grep -q prefix warning &&
     -+	test_grep ! prefix warning &&
     ++	test_grep ! -q prefix warning &&
       	test_svn_configured_prefix "" &&
       	rm -rf project &&
       	rm -f warning
     @@ t/t9502-gitweb-standalone-parse-output.sh: test_expect_success 'forks: setup' '
      -	grep -q ">foo_baz\\.git<"        gitweb.body &&
      -	grep -q ">foo/foo-forked\\.git<" gitweb.body &&
      -	grep -q ">fork of .*<"           gitweb.body
     -+	test_grep ">\\.git<"               gitweb.body &&
     -+	test_grep ">foo\\.git<"            gitweb.body &&
     -+	test_grep ">foo_baz\\.git<"        gitweb.body &&
     -+	test_grep ">foo\\.bar\\.git<"      gitweb.body &&
     -+	test_grep ">foo_baz\\.git<"        gitweb.body &&
     -+	test_grep ">foo/foo-forked\\.git<" gitweb.body &&
     -+	test_grep ">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' '
     @@ t/t9502-gitweb-standalone-parse-output.sh: test_expect_success 'enable forks fea
      -	grep -q ">foo_baz\\.git<"        gitweb.body &&
      -	grep -v ">foo/foo-forked\\.git<" gitweb.body &&
      -	grep -v ">fork of .*<"           gitweb.body
     -+	test_grep ">\\.git<"               gitweb.body &&
     -+	test_grep ">foo\\.git<"            gitweb.body &&
     -+	test_grep ">foo_baz\\.git<"        gitweb.body &&
     -+	test_grep ">foo\\.bar\\.git<"      gitweb.body &&
     -+	test_grep ">foo_baz\\.git<"        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
       '
     @@ t/t9502-gitweb-standalone-parse-output.sh: test_expect_success 'enable forks fea
       	gitweb_run "p=foo.git;a=forks" &&
      -	grep -q ">foo/foo-forked\\.git<" gitweb.body &&
      -	grep -q ">fork of foo<"          gitweb.body
     -+	test_grep ">foo/foo-forked\\.git<" gitweb.body &&
     -+	test_grep ">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 "200 OK"        gitweb.headers &&
     -+	test_grep ">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)' '
     @@ t/t9807-git-p4-submit.sh: test_expect_success 'submit --update-shelve' '
       		change=$(last_shelve) &&
       		p4 unshelve -c $change -s $change &&
      -		grep -q updated-line shelf.t &&
     -+		test_grep 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 ...
     @@ t/t9832-unshelve.sh: EOF
       		git p4 unshelve $change &&
       		git show refs/remotes/p4-unshelved/$change >actual &&
      -		grep -q "Further description" actual &&
     -+		test_grep "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 &&
     @@ t/t9832-unshelve.sh: test_expect_success 'create shelved changelist based on p4
       		change=$(last_shelved_change) &&
       		p4 describe -S $change >out.txt &&
      -		grep -q "Change to be unshelved" out.txt
     -+		test_grep "Change to be unshelved" out.txt
     ++		test_grep -q "Change to be unshelved" out.txt
       	)
       '
       
     @@ t/t9832-unshelve.sh: test_expect_success 'try to unshelve the change' '
       		cd "$git" &&
       		git p4 unshelve $change >out.txt &&
      -		grep -q "unshelved changelist $change" out.txt
     -+		test_grep "unshelved changelist $change" out.txt
     ++		test_grep -q "unshelved changelist $change" out.txt
       	)
       '
       
     @@ t/t9833-errors.sh: test_expect_success 'error handling' '
       		export P4PASSWD &&
       		test_must_fail git p4 clone //depot/foo 2>errmsg &&
      -		grep -q "failure accessing depot.*P4PASSWD" errmsg
     -+		test_grep "failure accessing depot.*P4PASSWD" errmsg
     ++		test_grep -q "failure accessing depot.*P4PASSWD" errmsg
       	)
       '
       
     @@ t/t9833-errors.sh: test_expect_success 'ticket logged out' '
       		p4 logout &&
       		test_must_fail git p4 submit 2>errmsg &&
      -		grep -q "failure accessing depot" errmsg
     -+		test_grep "failure accessing depot" errmsg
     ++		test_grep -q "failure accessing depot" errmsg
       	)
       '
       
     @@ t/t9902-completion.sh: test_expect_success '__git_pretty_aliases' '
       	run_completion "git " &&
       	# built-in
      -	grep -q "^add \$" out &&
     -+	test_grep "^add \$" out &&
     ++	test_grep -q "^add \$" out &&
       	# script
      -	grep -q "^rebase \$" out &&
     -+	test_grep "^rebase \$" out &&
     ++	test_grep -q "^rebase \$" out &&
       	# plumbing
      -	! grep -q "^ls-files \$" out &&
     -+	test_grep ! "^ls-files \$" out &&
     ++	test_grep ! -q "^ls-files \$" out &&
       
       	run_completion "git r" &&
      -	! grep -q -v "^r" out
     -+	test_grep ! -v "^r" out
     ++	test_grep ! -q -v "^r" out
       '
       
       test_expect_success 'double dash "git" itself' '
 4:  c1b90101ef ! 6:  e5ecb37401 t: add lint-style.pl with test_grep negation rule
     @@ Metadata
      Author: Michael Montalbo <[email protected]>
      
       ## Commit message ##
     -    t: add lint-style.pl with test_grep negation rule
     +    t: add greplint to detect bare grep assertions
      
     -    Add a mechanical lint checker for test scripts, similar in spirit to
     -    check-non-portable-shell.pl but focused on test conventions rather
     -    than portability.
     +    Without a lint guard, bare grep assertions will creep back into
     +    tests over time, defeating the previous commit's conversion.
      
     -    The tool defines LintParser, a subclass of ScriptParser (from the
     -    shared lib-shell-parser.pl module).  ScriptParser's
     -    parse_cmd() finds test_expect_success blocks and calls check_test()
     -    for each body; LintParser overrides check_test() to run lint rules
     -    on the parsed commands.  A "# lint-ok" comment suppresses all
     -    checks for intentional style violations.
     +    Add greplint.pl to catch bare 'grep' used as a test assertion
     +    (where 'test_grep' should be used) and '! test_grep' (where
     +    'test_grep !' should be used).
      
     -    The first rule detects '! test_grep' and replaces it with
     -    'test_grep !'.  Shell-level negation suppresses the diagnostic
     -    output that test_grep prints on failure; the built-in negation
     -    preserves it.
     +    greplint.pl reuses the shared shell parser from lib-shell-parser.pl
     +    to tokenize test bodies.  The Lexer collapses heredocs, command
     +    substitutions, and quoted strings into single tokens, so 'grep'
     +    appearing inside these contexts is not flagged.  A flat walk over
     +    the token stream tracks command position and pipeline state to
     +    distinguish assertion greps from filter greps.
      
     -    Three violations inside test bodies are converted via --fix.  One
     -    additional violation in a helper function outside test_expect_success
     -    (t7900's test_geometric_repack_needed) is converted manually, since
     -    the parser only processes test bodies.
     +    For double-quoted test bodies, a source-line walk counts
     +    backslash-continuation lines that the Lexer consumes without
     +    emitting into the body text, adjusting the reported line number
     +    accordingly.
     +
     +    Add test fixtures in greplint/ (modeled on chainlint/) covering
     +    detection of bare grep assertions, correct skipping of filters,
     +    pipelines, redirects, command substitutions, and lint-ok annotations.
     +
     +    Wire into the Makefile as:
     +      - test-greplint: runs greplint.pl on $(T) $(THELPERS) $(TPERF)
     +      - check-greplint: runs greplint.pl on fixtures, diffs against expected
     +      - clean-greplint: removes temp dir
     +
     +    Add eol=lf entries in t/.gitattributes for greplint fixtures,
     +    matching chainlint, so that check-greplint passes on Windows
     +    where core.autocrlf would otherwise cause CRLF mismatches
     +    between expected and actual output.
      
          Signed-off-by: Michael Montalbo <[email protected]>
      
     @@ t/.gitattributes
      @@
       t[0-9][0-9][0-9][0-9]/* -whitespace
       /chainlint/*.expect eol=lf -whitespace
     -+/lint-style/*.expect eol=lf -whitespace
     -+/lint-style/*.test eol=lf -whitespace
     ++/greplint/*.expect eol=lf -whitespace
     ++/greplint/*.test eol=lf -whitespace
       /t0110/url-* binary
       /t3206/* eol=lf
       /t3900/*.txt eol=lf
      
       ## t/Makefile ##
     -@@ t/Makefile: TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))
     +@@ t/Makefile: TEST_LINT ?= test-lint
     + 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;
     +@@ t/Makefile: TEST_SHELL_PATH_SQ = $(subst ','\'',$(TEST_SHELL_PATH))
     + 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)))
     +@@ t/Makefile: TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh
     + 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
     -+LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))
       UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)
       UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))
     - UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)
     +@@ t/Makefile: test: pre-clean check-meson $(TEST_LINT)
     + 	$(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:
     +@@ t/Makefile: unit-tests-test-tool:
     + 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
     + 
     +@@ t/Makefile: check-chainlint:
     + 	{ $(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 && \
      @@ t/Makefile: check-meson:
       test-lint: test-lint-duplicates test-lint-executable \
       	test-lint-filenames
       ifneq ($(PERL_PATH),)
     --test-lint: test-lint-shell-syntax check-shell-parser
     -+test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser
     +-test-lint: test-lint-shell-syntax
     ++test-lint: test-lint-shell-syntax test-greplint
       else
       GIT_TEST_CHAIN_LINT = 0
       endif
     -@@ t/Makefile: test-lint-shell-syntax:
     +@@ t/Makefile: test-lint-executable:
     + test-lint-shell-syntax:
     + 	@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)
       
     - check-shell-parser:
     - 	@'$(PERL_PATH_SQ)' check-shell-parser.pl
     -+
     -+test-lint-style:
     -+	@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)
     -+
     -+check-lint-style:
     -+	@rc=0; for t in $(LINT_STYLE_TESTS); do \
     -+		base=$${t%.test}; \
     -+		case $$base in \
     -+		*-fix) \
     -+			cp "$$t" "$$t.tmp" && \
     -+			'$(PERL_PATH_SQ)' lint-style.pl --fix "$$t.tmp" >/dev/null 2>&1; \
     -+			fix_rc=$$?; \
     -+			if test $$fix_rc != 0; then \
     -+				echo "FAIL: $$t (--fix exit code $$fix_rc)"; rc=1; \
     -+			elif ! diff -u "$$base.expect" "$$t.tmp"; then \
     -+				echo "FAIL: $$t (--fix output)"; rc=1; \
     -+			fi; \
     -+			rm -f "$$t.tmp" ;; \
     -+		*) \
     -+			if ! '$(PERL_PATH_SQ)' lint-style.pl "$$t" 2>&1 | \
     -+				diff -u "$$base.expect" -; then \
     -+				echo "FAIL: $$t"; rc=1; \
     -+			fi ;; \
     -+		esac; \
     -+	done; test $$rc = 0
     ++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)
      @@ t/Makefile: perf:
     + 	$(MAKE) -C perf/ all
       
       .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
     - 	check-chainlint clean-chainlint test-chainlint \
     --	check-shell-parser $(UNIT_TESTS)
     -+	check-shell-parser \
     -+	check-lint-style test-lint-style $(UNIT_TESTS)
     +-	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:
      
     - ## t/lint-style.pl (new) ##
     + ## t/greplint-cat.pl (new) ##
      @@
     -+#!/usr/bin/perl
     -+
     -+# Check test scripts for style violations that can be detected
     -+# mechanically, such as using bare 'grep' where test_grep should
     -+# be used.  Use --fix to automatically apply suggested replacements.
     -+#
     -+# Detection uses parsed tokens from the shared shell parser for
     -+# correct handling of heredocs, $(...), pipes, and quoting.
     -+# Fixes modify the original file text to preserve formatting.
     ++#!/usr/bin/env perl
      +
      +use strict;
      +use warnings;
     -+use File::Basename;
     -+# Force LF output so check-lint-style's diff against the
     -+# pre-committed .expect files works on Windows.
     -+binmode(STDOUT, ':unix');
     -+binmode(STDERR, ':unix');
      +
     -+my $fix_mode = 0;
     -+if (@ARGV && $ARGV[0] eq '--fix') {
     -+	$fix_mode = 1;
     -+	shift @ARGV;
     ++# 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;
      +}
      +
     -+# Load the shared shell parser (Lexer, ShellParser, ScriptParser).
     -+my $_lib = dirname($0) . "/lib-shell-parser.pl";
     -+$_lib = "./$_lib" unless $_lib =~ m{^/};
     -+do $_lib or die "$0: failed to load $_lib: $@$!\n";
     ++close $expect;
     +
     + ## t/greplint.pl (new) ##
     +@@
     ++#!/usr/bin/env perl
      +
     -+# LintParser is a subclass of ScriptParser which runs lint rules
     -+# on each test body.  Per-file state (file name, raw lines, dirty
     -+# flag) is stored on the instance before calling parse().
     ++# Detect bare 'grep' used as a test assertion where 'test_grep'
     ++# should be used, and '! test_grep' where 'test_grep !' should
     ++# be used.
      +#
     -+# Subroutines defined below (parse_commands, check_test_grep_negation,
     -+# etc.) are in package main and called with the main:: prefix.
     -+# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible
     -+# across packages since 'package' does not introduce a new scope.
     -+package LintParser;
     -+our @ISA = ('ScriptParser');
     ++# 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.
      +
     -+package main;
     ++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;
     -+my $has_fixable = 0;
      +
     -+sub err {
     -+	my ($file, $lineno, $line, $msg, %opts) = @_;
     -+	$line =~ s/^\s+//;
     -+	$line =~ s/\s+$//;
     -+	$line =~ s/\s+/ /g;
     -+	my $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';
     -+	print "$file:$lineno: $prefix: $msg: $line\n";
     -+	$exit_code = 1 unless $fix_mode && $opts{fixable};
     -+}
     ++# 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;
      +
     -+# Report a lint violation found by a rule.  In --fix mode, apply
     -+# the regex substitution on the raw line and report success.
     -+# Otherwise just report.  Returns 1 if the line was modified.
     -+sub report_violation {
     -+	my ($file, $cmd, $line_ref, $match, $fix, $from) = @_;
     -+	my $lineno = $cmd->{lineno};
     -+	my $display = join(' ', @{$cmd->{tokens}});
     -+	$has_fixable++;  # count for the "--fix" hint
     -+	if ($fix_mode) {
     -+		if ($$line_ref =~ s/$match/$fix/) {
     -+			err $file, $lineno, $display,
     -+				"replace '$from' with '$fix'",
     -+				fixable => 1;
     -+			return 1;
     -+		}
     -+		err $file, $lineno, $display,
     -+			"replace '$from' with '$fix' (could not auto-fix)";
     -+	} else {
     -+		err $file, $lineno, $display,
     -+			"replace '$from' with '$fix'";
     ++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 0;
     ++	return 1;
      +}
      +
     -+# Split a token stream into commands at &&, ||, ;;, and \n.
     -+sub parse_commands {
     -+	my ($content) = @_;
     -+	my $parser = ShellParser->new(\$content);
     -+	my @all_tokens = $parser->parse();
     ++# Some bare greps are intentional (e.g. file may not exist,
     ++# data filter).  A '# lint-ok' annotation on the source line
     ++# suppresses the warning.
     ++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/;
     ++}
      +
     -+	my @commands;
     -+	my @current;
     -+	my $lineno = 1;
     ++# 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;
     ++}
      +
     -+	for (my $ti = 0; $ti < @all_tokens; $ti++) {
     -+		my $text = $all_tokens[$ti]->[0];
     -+		if ($text =~ /^(?:&&|\|\||;;|\n)$/) {
     -+			if (@current) {
     -+				push @commands, {
     -+					tokens => [@current],
     -+					lineno => $lineno,
     -+				};
     -+				@current = ();
     -+			}
     ++# 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 {
     -+			$lineno = $all_tokens[$ti]->[3]
     -+				if !@current && defined $all_tokens[$ti]->[3];
     -+			push @current, $text;
     ++			$lines_seen++;
      +		}
     ++		$src_ln++;
      +	}
     -+	if (@current) {
     -+		push @commands, {
     -+			tokens => [@current],
     -+			lineno => $lineno,
     -+		};
     ++	if ($lines_seen < $body_lineno) {
     ++		warn "body_lineno ($body_lineno) not found within body range " .
     ++		    "($body_start..$end)\n";
      +	}
     -+	return @commands;
     ++	return $file_lineno + $adj;
      +}
      +
     -+# --- Rule: '! test_grep' should be 'test_grep !' ---
     -+# Shell-level negation suppresses test_grep's diagnostic output
     -+# on failure.  Built-in negation preserves it.
     -+sub check_test_grep_negation {
     -+	my ($cmd, $file, $line_ref) = @_;
     -+	my @tokens = @{$cmd->{tokens}};
     -+	return unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';
     -+
     -+	return report_violation($file, $cmd, $line_ref,
     -+		qr/!\s*test_grep/, 'test_grep !', '! test_grep');
     -+}
     -+
     -+# Map parsed commands back to raw file lines for --fix.
     -+# Detection uses parsed tokens (correct handling of quoting,
     -+# heredocs, pipes) but fixes must modify the original text
     -+# to preserve formatting.
     -+package LintParser;
     -+
     ++# ScriptParser calls this for each test body found in the script.
      +sub check_test {
     -+	# Called by ScriptParser::parse_cmd for each test_expect_success
     -+	# or test_expect_failure block.
      +	my $self = shift @_;
      +	my $title = ScriptParser::unwrap(shift @_);
     -+
     -+	# Two test body formats:
     -+	#   Quoted:  test_expect_success 'title' '..body..'
     -+	#   Heredoc: test_expect_success 'title' - <<\EOF
     -+	#              ..body..
     -+	#            EOF
     -+	# For quoted, the body token is the quoted string.
     -+	# For heredoc, the body token is '-' and the actual
     -+	# code arrives as the next argument from the Lexer.
      +	my $body_token = shift @_;
     -+	my $lineno_base = $body_token->[3] || 1;
     ++	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};
     -+			$lineno_base = $herebody->{start_line} || 1;
     ++			$body_start = $herebody->{start_line};
      +		}
      +	}
      +	return unless $body;
      +
     -+	# Map each command back to its file line number.
     -+	# $lineno_base is where the body starts in the file;
     -+	# $cmd->{lineno} is relative to the body (starting at 1).
      +	my $raw_lines = $self->{raw_lines};
     -+	for my $cmd (main::parse_commands($body)) {
     -+		my $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;
     -+		$cmd->{lineno} = $ln;
     -+		next unless $ln >= 1 && $ln <= @$raw_lines;
     -+		next if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;
      +
     -+		if (main::check_test_grep_negation($cmd, $self->{file}, \$raw_lines->[$ln - 1])) {
     -+			$self->{dirty} = 1;
     ++	# 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;
      +		}
      +	}
      +}
     @@ t/lint-style.pl (new)
      +package main;
      +
      +for my $file (@ARGV) {
     -+	# :unix:crlf strips \r on Windows (same as chainlint.pl)
      +	open(my $fh, '<:unix:crlf', $file) or die "$0: $file: $!\n";
      +	my @raw_lines = <$fh>;
      +	close $fh;
     -+
     -+	my $parser = LintParser->new(\join('', @raw_lines));
     ++	my $s = join('', @raw_lines);
     ++	my $parser = GrepLintParser->new(\$s);
      +	$parser->{file} = $file;
      +	$parser->{raw_lines} = \@raw_lines;
     -+	$parser->{dirty} = 0;
      +	$parser->parse();
     -+
     -+	if ($fix_mode && $parser->{dirty}) {
     -+		open(my $out, '>', $file) or die "$0: $file: $!\n";
     -+		print $out @{$parser->{raw_lines}};
     -+		close $out;
     -+	}
     -+}
     -+
     -+if ($has_fixable && !$fix_mode) {
     -+	print "hint: run with --fix to apply the suggested replacements.\n";
      +}
      +exit $exit_code;
      
     - ## t/lint-style/heredoc.expect (new) ##
     + ## t/greplint/bare-grep-after-and.expect (new) ##
      @@
     -+lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep "after-heredoc-is-caught" actual
     -+lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep "not-inside-sed-heredoc" actual
     -+hint: run with --fix to apply the suggested replacements.
     ++3: error: bare grep outside pipeline (use test_grep)
      
     - ## t/lint-style/heredoc.test (new) ##
     + ## t/greplint/bare-grep-after-and.test (new) ##
      @@
     -+test_expect_success 'greps inside heredocs are skipped' '
     -+	cat <<-EOF &&
     -+	grep "inside-strip-tabs" file
     -+	EOF
     -+	cat <<-\EOF &&
     -+	grep "inside-no-expand" file
     -+	EOF
     -+	! test_grep "after-heredoc-is-caught" actual
     ++test_expect_success 'grep after && is flagged' '
     ++	cmd &&
     ++	grep pattern file
      +'
     -+
     -+test_expect_success 'sed with << does not start a heredoc' '
     -+	sed "s/<< foo/bar/" file &&
     -+	! test_grep "not-inside-sed-heredoc" actual
     +
     + ## t/greplint/bare-grep-after-semicolon.expect (new) ##
     +@@
     ++3: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/bare-grep-after-semicolon.test (new) ##
     +@@
     ++test_expect_success 'grep after semicolon is flagged' '
     ++	echo hello;
     ++	grep pattern file
      +'
      
     - ## t/lint-style/test-grep-negation-fix.expect (new) ##
     + ## t/greplint/bare-grep-compound-body.expect (new) ##
      @@
     -+test_expect_success 'negated test_grep' '
     -+	test_grep ! "pattern" actual &&
     -+	test_grep ! -i "insensitive" actual
     ++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)
     +
     + ## t/greplint/bare-grep-compound-body.test (new) ##
     +@@
     ++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
      +'
      
     - ## t/lint-style/test-grep-negation-fix.test (new) ##
     + ## t/greplint/bare-grep-count-mode.expect (new) ##
     +@@
     ++2: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/bare-grep-count-mode.test (new) ##
      @@
     -+test_expect_success 'negated test_grep' '
     -+	! test_grep "pattern" actual &&
     -+	! test_grep -i "insensitive" actual
     ++test_expect_success 'grep -c is flagged (not special-cased)' '
     ++	grep -c pattern file
      +'
      
     - ## t/lint-style/test-grep-negation.expect (new) ##
     + ## t/greplint/bare-grep-explicit-pattern.expect (new) ##
      @@
     -+lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep "pattern" actual
     -+lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i "insensitive" actual
     -+hint: run with --fix to apply the suggested replacements.
     ++2: error: bare grep outside pipeline (use test_grep)
      
     - ## t/lint-style/test-grep-negation.test (new) ##
     + ## t/greplint/bare-grep-explicit-pattern.test (new) ##
      @@
     -+test_expect_success 'negated test_grep' '
     -+	! test_grep "pattern" actual &&
     -+	! test_grep -i "insensitive" actual
     ++test_expect_success 'grep -e is flagged' '
     ++	grep -e pattern file
      +'
      
     - ## t/t0031-lockfile-pid.sh ##
     -@@ t/t0031-lockfile-pid.sh: test_expect_success 'PID info not shown by default' '
     - 		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
     - 	)
     - '
     - 
     + ## t/greplint/bare-grep-flags.expect (new) ##
     +@@
     ++2: error: bare grep outside pipeline (use test_grep)
      
     - ## t/t5300-pack-object.sh ##
     -@@ t/t5300-pack-object.sh: test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat
     - 
     - 	# --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' '
     + ## t/greplint/bare-grep-flags.test (new) ##
     +@@
     ++test_expect_success 'grep -E is flagged' '
     ++	grep -E "pat+ern" file
     ++'
      
     - ## t/t5319-multi-pack-index.sh ##
     -@@ t/t5319-multi-pack-index.sh: test_expect_success 'load reverse index when missing .idx, .pack' '
     - 
     - 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' '
     -
     - ## t/t7900-maintenance.sh ##
     -@@ t/t7900-maintenance.sh: test_geometric_repack_needed () {
     - 	true)
     - 		test_grep "\[\"git\",\"repack\"," trace2.txt;;
     - 	false)
     --		! test_grep "\[\"git\",\"repack\"," trace2.txt;;
     -+		test_grep ! "\[\"git\",\"repack\"," trace2.txt;;
     - 	*)
     - 		BUG "invalid parameter: $NEEDED";;
     - 	esac
     + ## t/greplint/bare-grep-lint-ok.expect (new) ##
     +
     + ## t/greplint/bare-grep-lint-ok.test (new) ##
     +@@
     ++test_expect_success 'grep with lint-ok annotation is not flagged' '
     ++	grep pattern file && # lint-ok
     ++	echo done
     ++'
     +
     + ## t/greplint/bare-grep-negated.expect (new) ##
     +@@
     ++2: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/bare-grep-negated.test (new) ##
     +@@
     ++test_expect_success 'negated grep is flagged' '
     ++	! grep pattern file
     ++'
     +
     + ## t/greplint/bare-grep-pattern-file.expect (new) ##
     +@@
     ++2: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/bare-grep-pattern-file.test (new) ##
     +@@
     ++test_expect_success 'grep -f is flagged' '
     ++	grep -f patterns.txt file
     ++'
     +
     + ## t/greplint/bare-grep-simple.expect (new) ##
     +@@
     ++2: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/bare-grep-simple.test (new) ##
     +@@
     ++test_expect_success 'bare grep is flagged' '
     ++	grep pattern file
     ++'
     +
     + ## t/greplint/bare-grep-subshell.expect (new) ##
     +@@
     ++3: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/bare-grep-subshell.test (new) ##
     +@@
     ++test_expect_success 'grep in subshell is flagged' '
     ++	(
     ++		grep pattern file
     ++	)
     ++'
     +
     + ## t/greplint/dqstring-continuation-offset.expect (new) ##
     +@@
     ++10: error: bare grep outside pipeline (use test_grep)
     +
     + ## t/greplint/dqstring-continuation-offset.test (new) ##
     +@@
     ++# 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
     ++"
     +
     + ## t/greplint/filter-command-substitution.expect (new) ##
     +
     + ## t/greplint/filter-command-substitution.test (new) ##
     +@@
     ++test_expect_success 'grep in command substitution is not flagged' '
     ++	x=$(grep pattern file)
     ++'
     +
     + ## t/greplint/filter-pipe-input.expect (new) ##
     +
     + ## t/greplint/filter-pipe-input.test (new) ##
     +@@
     ++test_expect_success 'grep receiving pipe input is not flagged' '
     ++	cmd | grep pattern
     ++'
     +
     + ## t/greplint/filter-pipe-output.expect (new) ##
     +
     + ## t/greplint/filter-pipe-output.test (new) ##
     +@@
     ++test_expect_success 'grep piping to another command is not flagged' '
     ++	grep pattern file | wc -l
     ++'
     +
     + ## t/greplint/filter-redirect-output.expect (new) ##
     +
     + ## t/greplint/filter-redirect-output.test (new) ##
     +@@
     ++test_expect_success 'grep with output redirect is not flagged' '
     ++	grep pattern file >output
     ++'
     +
     + ## t/greplint/filter-stdin-redirect.expect (new) ##
     +
     + ## t/greplint/filter-stdin-redirect.test (new) ##
     +@@
     ++test_expect_success 'grep reading from stdin redirect is not flagged' '
     ++	grep pattern <input
     ++'
     +
     + ## t/greplint/grep-as-argument.expect (new) ##
     +
     + ## t/greplint/grep-as-argument.test (new) ##
     +@@
     ++test_expect_success 'grep as argument to another command is not flagged' '
     ++	test_must_fail grep pattern file
     ++'
     +
     + ## t/greplint/grep-as-value.expect (new) ##
     +
     + ## t/greplint/grep-as-value.test (new) ##
     +@@
     ++test_expect_success 'grep as value in for-loop is not flagged' '
     ++	for cmd in grep sed awk
     ++	do
     ++		echo $cmd
     ++	done
     ++'
     +
     + ## t/greplint/wrong-negation.expect (new) ##
     +@@
     ++2: error: use "test_grep !" instead of "! test_grep"
     +
     + ## t/greplint/wrong-negation.test (new) ##
     +@@
     ++test_expect_success 'wrong negation of test_grep is flagged' '
     ++	! test_grep pattern file
     ++'
Signature · 1 line
gitgitgadget
#12 lore raw

PATCH v2 1/6 t/README: document test_grep helper

From: Michael Montalbo <[email protected]>

test_grep is a wrapper around grep for test assertions that prints the file contents on failure for easier debugging. It also accepts '!' as its first argument for negation, which preserves the diagnostic output that '! test_grep' would suppress.

Despite being widely used (and the preferred replacement for bare grep in assertions), test_grep has no entry in t/README alongside the other documented helpers like test_cmp and test_line_count. Add one.

---
 t/README | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
t/README +21 -0
index adbbd9acf4..c12a1c317a 100644
@@ -1039,6 +1039,27 @@ see test-lib-functions.sh for the full list and their options.
 
    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_path_is_file <path>
    test_path_is_dir <path>
    test_path_is_missing <path>
Signature · 1 line
gitgitgadget
#13 lore raw

PATCH v2 2/6 t: fix grep assertions missing file arguments

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Three grep assertions were missing their file arguments, causing them to read from empty stdin instead of the intended file:

- t2402: '! grep ...' should read from 'out', matching the grep on the preceding line.
- t7507: the closing quote is in the wrong place, making the entire 'diff --git actual' a single pattern with no file argument instead of pattern 'diff --git' and file 'actual'.
- t7700: '! grep ...' should read from 'packlist', matching the redirect on the preceding line.

Without file arguments these greps always succeed (empty stdin matches nothing), so the assertions were not actually checking anything. All three tests pass with the corrected file arguments, confirming the intended behavior is sound.

---
 t/t2402-worktree-list.sh  | 2 +-
 t/t7507-commit-verbose.sh | 2 +-
 t/t7700-repack.sh         | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)
t/t2402-worktree-list.sh +1 -1
index e0c6abd2f5..93f92e854a 100755
@@ -144,7 +144,7 @@ test_expect_success '"list" all worktrees with prunable annotation' '
 	rm -rf prunable &&
 	git worktree list >out &&
 	grep "/prunable  *[0-9a-f].* prunable$" out &&
-	! grep "/unprunable  *[0-9a-f].* prunable$"
+	! grep "/unprunable  *[0-9a-f].* prunable$" out
 '
 
 test_expect_success '"list" all worktrees --porcelain with prunable' '
t/t7507-commit-verbose.sh +1 -1
index b53d71c086..acdb6b1455 100755
@@ -163,7 +163,7 @@ done
 
 test_expect_success "status ignores commit.verbose=true" '
 	git -c commit.verbose=true status >actual &&
-	! grep "^diff --git actual"
+	! grep "^diff --git" actual
 '
 
 test_done
t/t7700-repack.sh +1 -1
index 63ef63fc50..c6ff3aed30 100755
@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar
 	rm -f .git/objects/pack/* &&
 	mv pack-* .git/objects/pack/ &&
 	git verify-pack -v -- .git/objects/pack/*.idx >packlist &&
-	! grep "^$coid " &&
+	! grep "^$coid " packlist &&
 	echo >.git/objects/info/alternates &&
 	test_must_fail git show $coid
 '
Signature · 1 line
gitgitgadget
#14 lore raw

PATCH v2 3/6 t: extract chainlint's parser into shared module

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Move chainlint.pl's Lexer, ShellParser, and ScriptParser into a shared module (lib-shell-parser.pl) so other lint tools can reuse the same shell parsing infrastructure. A subsequent commit adds greplint.pl, which needs the same tokenizer to correctly identify command boundaries.

ScriptParser's check_test() becomes a no-op in the shared module. chainlint.pl defines ChainlintParser (extending ScriptParser) with the &&-chain check_test() implementation.

No functional change: chainlint produces the same output and check-chainlint self-tests pass.

---
 t/chainlint.pl        | 529 +----------------------------------------
 t/lib-shell-parser.pl | 531 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 543 insertions(+), 517 deletions(-)
 create mode 100644 t/lib-shell-parser.pl
t/chainlint.pl +12 -517
index f0598e3934..cededf15ee 100755
@@ -23,458 +23,9 @@ my $jobs = -1;
 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 whitepace, 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 @@ DONE:
 # 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 @@ DONE:
 	$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;
-
-use base 'ShellParser';
-
-sub new {
-	my $class = shift @_;
-	my $self = $class->SUPER::new(@_);
-	$self->{ntests} = 0;
-	$self->{nerrs} = 0;
-	return $self;
-}
+# ChainlintParser extends ScriptParser with &&-chain checking
+package ChainlintParser;
 
-# 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 format_problem {
 
 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 @@ sub check_test {
 	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 @@ sub check_script {
 		}
 		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);
t/lib-shell-parser.pl new file +531 -0

531 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#15 lore raw

PATCH v2 4/6 t: fix Lexer line count for $() inside double-quoted strings

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

scan_dqstring's post-loop newline counter re-counts newlines that were already counted during recursive parsing of $() bodies. This happens because scan_dollar returns text containing newlines (from multi-line command substitutions), and the catch-all counter at the end of scan_dqstring counts all of them again.

Fix this by counting newlines inline as non-special characters are consumed, and removing the post-loop catch-all. Each newline is now counted exactly once: literal newlines at the inline match, line splices at the backslash handler, and $() newlines by scan_token during the recursive parse.

This is a latent bug: any consumer that relies on token line numbers rather than byte offsets would get incorrect results for tokens following a multi-line $() inside a double-quoted string. chainlint is not affected because it annotates the original body text using byte offsets, not token line numbers.

---
 t/lib-shell-parser.pl | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
t/lib-shell-parser.pl +6 -3
index edbfee0839..c91289dfd6 100644
@@ -93,8 +93,12 @@ sub scan_dqstring {
 	my $b = $self->{buff};
 	my $s = '"';
 	while (1) {
-		# slurp up non-special characters
-		$s .= $1 if $$b =~ /\G([^"\$\\]+)/gc;
+		# 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;
@@ -111,7 +115,6 @@ sub scan_dqstring {
 		}
 		die("internal error scanning dq-string '$c'\n");
 	}
-	$self->{lineno} += () = $s =~ /\n/sg;
 	return $s;
 }
 
Signature · 1 line
gitgitgadget
#16 lore raw

PATCH v2 5/6 t: convert grep assertions to test_grep

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Replace bare grep with test_grep in test assertions across the suite, including sourced test helpers (lib-*.sh, *-tests.sh). test_grep prints the contents of the file being searched on failure, making debugging easier than a bare grep which fails silently.

Only assertion-style greps are converted: grep used as a filter in pipelines, command substitutions, conditionals, or with redirected I/O is left as-is with a "# lint-ok" annotation. Existing '! test_grep' calls are rewritten to 'test_grep !' so that the diagnostic output is preserved on failure.

The conversion was generated using a grep-assertion linter (greplint.pl, added in the following commit) to identify bare grep calls at command position. To reproduce:

    # Step 1: mark bare greps that should not be converted
    sed -i '/! grep "$m" \.git\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \
        t/t1400-update-ref.sh
    sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \
        t/t3420-rebase-autostash.sh
    sed -i '/grep -vf before commits\.raw/s/$/ # lint-ok: data filter/' \
        t/t5326-multi-pack-bitmaps.sh
    sed -i '/! grep $d shallow-client\/\.git\/shallow/s/$/ # lint-ok: file may not exist after repack/' \
        t/t5537-fetch-shallow.sh
    sed -i '/grep -E "^\[0-9a-f\].*|| :/s/$/ # lint-ok: data filter/' \
        t/t5702-protocol-v2.sh
    sed -i '/! grep gitdir squatting-clone/s/$/ # lint-ok: file may not exist after failed clone/' \
        t/t7450-bad-git-dotfiles.sh

    # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'
    # (must come before steps 3-4 so greplint does not see them)
    sed -i 's/! test_grep/test_grep !/' t/t0031-lockfile-pid.sh
    sed -i 's/! test_grep/test_grep !/' t/t5300-pack-object.sh
    sed -i 's/! test_grep/test_grep !/' t/t5319-multi-pack-index.sh

    # Step 3: convert '! grep' -> 'test_grep !'
    perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |
    while IFS=: read f l; do
        sed -i "${l}s/! *grep/test_grep !/" "$f"
    done

    # Step 4: convert remaining 'grep' -> 'test_grep'
    perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |
    while IFS=: read f l; do
        sed -i "${l}s/grep/test_grep/" "$f"
    done

To verify, run: make -C t test-greplint

---
 t/for-each-ref-tests.sh                     |  12 +-
 t/lib-bitmap.sh                             |  12 +-
 t/lib-bundle-uri-protocol.sh                |  26 +-
 t/lib-httpd.sh                              |   2 +-
 t/pack-refs-tests.sh                        |   2 +-
 t/show-ref-exists-tests.sh                  |   2 +-
 t/t0000-basic.sh                            |  16 +-
 t/t0001-init.sh                             |  18 +-
 t/t0008-ignores.sh                          |   8 +-
 t/t0009-git-dir-validation.sh               |   6 +-
 t/t0012-help.sh                             |   4 +-
 t/t0013-sha1dc.sh                           |   2 +-
 t/t0017-env-helper.sh                       |   4 +-
 t/t0021-conversion.sh                       |  18 +-
 t/t0029-core-unsetenvvars.sh                |   4 +-
 t/t0030-stripspace.sh                       |   4 +-
 t/t0031-lockfile-pid.sh                     |   2 +-
 t/t0040-parse-options.sh                    |  52 +--
 t/t0041-usage.sh                            |   2 +-
 t/t0052-simple-ipc.sh                       |  10 +-
 t/t0061-run-command.sh                      |   2 +-
 t/t0066-dir-iterator.sh                     |   2 +-
 t/t0068-for-each-repo.sh                    |  16 +-
 t/t0070-fundamental.sh                      |   6 +-
 t/t0081-find-pack.sh                        |  12 +-
 t/t0091-bugreport.sh                        |  18 +-
 t/t0092-diagnose.sh                         |  12 +-
 t/t0100-previous.sh                         |   2 +-
 t/t0200-gettext-basic.sh                    |  14 +-
 t/t0203-gettext-setlocale-sanity.sh         |   4 +-
 t/t0204-gettext-reencode-sanity.sh          |   8 +-
 t/t0210-trace2-normal.sh                    |   6 +-
 t/t0211-trace2-perf.sh                      |  80 ++---
 t/t0212-trace2-event.sh                     |   8 +-
 t/t0300-credentials.sh                      |   4 +-
 t/t0410-partial-clone.sh                    |  82 ++---
 t/t0450-txt-doc-vs-help.sh                  |   2 +-
 t/t0500-progress-display.sh                 |  18 +-
 t/t0610-reftable-basics.sh                  |   8 +-
 t/t1004-read-tree-m-u-wf.sh                 |   8 +-
 t/t1006-cat-file.sh                         |  18 +-
 t/t1007-hash-object.sh                      |   8 +-
 t/t1011-read-tree-sparse-checkout.sh        |  10 +-
 t/t1050-large.sh                            |   6 +-
 t/t1091-sparse-checkout-builtin.sh          |  24 +-
 t/t1092-sparse-checkout-compatibility.sh    |  44 +--
 t/t1300-config.sh                           |  16 +-
 t/t1305-config-include.sh                   |   2 +-
 t/t1308-config-set.sh                       |   6 +-
 t/t1400-update-ref.sh                       | 170 +++++-----
 t/t1403-show-ref.sh                         |  18 +-
 t/t1410-reflog.sh                           |   4 +-
 t/t1415-worktree-refs.sh                    |   4 +-
 t/t1430-bad-ref-name.sh                     |  56 ++--
 t/t1450-fsck.sh                             |  12 +-
 t/t1451-fsck-buffer.sh                      |   6 +-
 t/t1460-refs-migrate.sh                     |   2 +-
 t/t1500-rev-parse.sh                        |   6 +-
 t/t1502-rev-parse-parseopt.sh               |   2 +-
 t/t1503-rev-parse-verify.sh                 |  10 +-
 t/t1510-repo-setup.sh                       |  10 +-
 t/t1512-rev-parse-disambiguation.sh         |   4 +-
 t/t1515-rev-parse-outside-repo.sh           |   2 +-
 t/t1800-hook.sh                             |  18 +-
 t/t2004-checkout-cache-temp.sh              |   4 +-
 t/t2019-checkout-ambiguous-ref.sh           |   4 +-
 t/t2024-checkout-dwim.sh                    |   8 +-
 t/t2030-unresolve-info.sh                   |   6 +-
 t/t2060-switch.sh                           |   6 +-
 t/t2070-restore.sh                          |   2 +-
 t/t2080-parallel-checkout-basics.sh         |  14 +-
 t/t2081-parallel-checkout-collisions.sh     |  24 +-
 t/t2082-parallel-checkout-attributes.sh     |  12 +-
 t/t2103-update-index-ignore-missing.sh      |   6 +-
 t/t2200-add-update.sh                       |   2 +-
 t/t2203-add-intent.sh                       |   6 +-
 t/t2400-worktree-add.sh                     |  24 +-
 t/t2402-worktree-list.sh                    |  16 +-
 t/t2403-worktree-move.sh                    |   6 +-
 t/t2405-worktree-submodule.sh               |   6 +-
 t/t2407-worktree-heads.sh                   |  26 +-
 t/t2500-untracked-overwriting.sh            |   8 +-
 t/t2501-cwd-empty.sh                        |   4 +-
 t/t3001-ls-files-others-exclude.sh          |   6 +-
 t/t3007-ls-files-recurse-submodules.sh      |   6 +-
 t/t3200-branch.sh                           |  12 +-
 t/t3202-show-branch.sh                      |  10 +-
 t/t3203-branch-output.sh                    |   4 +-
 t/t3206-range-diff.sh                       |  78 ++---
 t/t3207-branch-submodule.sh                 |   4 +-
 t/t3301-notes.sh                            |  32 +-
 t/t3310-notes-merge-manual-resolve.sh       |  16 +-
 t/t3320-notes-merge-worktrees.sh            |   2 +-
 t/t3400-rebase.sh                           |  16 +-
 t/t3402-rebase-merge.sh                     |  16 +-
 t/t3404-rebase-interactive.sh               |  72 ++--
 t/t3406-rebase-message.sh                   |   6 +-
 t/t3415-rebase-autosquash.sh                |  10 +-
 t/t3416-rebase-onto-threedots.sh            |   4 +-
 t/t3418-rebase-continue.sh                  |  10 +-
 t/t3420-rebase-autostash.sh                 |  26 +-
 t/t3422-rebase-incompatible-options.sh      |   4 +-
 t/t3429-rebase-edit-todo.sh                 |   2 +-
 t/t3430-rebase-merges.sh                    |  32 +-
 t/t3500-cherry.sh                           |   4 +-
 t/t3501-revert-cherry-pick.sh               |   6 +-
 t/t3504-cherry-pick-rerere.sh               |   6 +-
 t/t3510-cherry-pick-sequence.sh             |  24 +-
 t/t3602-rm-sparse-checkout.sh               |   4 +-
 t/t3705-add-sparse-checkout.sh              |  10 +-
 t/t3800-mktag.sh                            |   4 +-
 t/t3901-i18n-patch.sh                       |  16 +-
 t/t3903-stash.sh                            |  28 +-
 t/t3904-stash-patch.sh                      |   4 +-
 t/t3908-stash-in-worktree.sh                |   2 +-
 t/t4000-diff-format.sh                      |   2 +-
 t/t4001-diff-rename.sh                      |   4 +-
 t/t4011-diff-symlink.sh                     |   2 +-
 t/t4013-diff-various.sh                     |   2 +-
 t/t4014-format-patch.sh                     | 344 ++++++++++----------
 t/t4015-diff-whitespace.sh                  |  16 +-
 t/t4017-diff-retval.sh                      |   2 +-
 t/t4018-diff-funcname.sh                    |   2 +-
 t/t4019-diff-wserror.sh                     |   8 +-
 t/t4020-diff-external.sh                    |  18 +-
 t/t4021-format-patch-numbered.sh            |   4 +-
 t/t4022-diff-rewrite.sh                     |  14 +-
 t/t4028-format-patch-mime-headers.sh        |   6 +-
 t/t4031-diff-rewrite-binary.sh              |  18 +-
 t/t4033-diff-patience.sh                    |   2 +-
 t/t4036-format-patch-signer-mime.sh         |   6 +-
 t/t4038-diff-combined.sh                    |   6 +-
 t/t4051-diff-function-context.sh            |  38 +--
 t/t4053-diff-no-index.sh                    |   4 +-
 t/t4063-diff-blobs.sh                       |   2 +-
 t/t4065-diff-anchored.sh                    |  26 +-
 t/t4067-diff-partial-clone.sh               |  12 +-
 t/t4073-diff-stat-name-width.sh             |  24 +-
 t/t4103-apply-binary.sh                     |   2 +-
 t/t4120-apply-popt.sh                       |   2 +-
 t/t4124-apply-ws-rule.sh                    |  10 +-
 t/t4128-apply-root.sh                       |   2 +-
 t/t4140-apply-ita.sh                        |   4 +-
 t/t4141-apply-too-large.sh                  |   2 +-
 t/t4150-am.sh                               |  48 +--
 t/t4200-rerere.sh                           |   6 +-
 t/t4201-shortlog.sh                         |   2 +-
 t/t4202-log.sh                              |  84 ++---
 t/t4204-patch-id.sh                         |   2 +-
 t/t4205-log-pretty-formats.sh               |   2 +-
 t/t4209-log-pickaxe.sh                      |  10 +-
 t/t4211-line-log.sh                         |  72 ++--
 t/t4216-log-bloom.sh                        |  18 +-
 t/t4252-am-options.sh                       |  22 +-
 t/t4254-am-corrupt.sh                       |   6 +-
 t/t4258-am-quoted-cr.sh                     |   2 +-
 t/t4301-merge-tree-write-tree.sh            |  18 +-
 t/t5000-tar-tree.sh                         |  10 +-
 t/t5004-archive-corner-cases.sh             |   2 +-
 t/t5100-mailinfo.sh                         |   2 +-
 t/t5150-request-pull.sh                     |  18 +-
 t/t5300-pack-object.sh                      |  22 +-
 t/t5302-pack-index.sh                       |   6 +-
 t/t5304-prune.sh                            |   8 +-
 t/t5310-pack-bitmaps.sh                     |  14 +-
 t/t5317-pack-objects-filter-objects.sh      |  12 +-
 t/t5318-commit-graph.sh                     |   8 +-
 t/t5319-multi-pack-index.sh                 |  16 +-
 t/t5324-split-commit-graph.sh               |  10 +-
 t/t5325-reverse-index.sh                    |   2 +-
 t/t5326-multi-pack-bitmaps.sh               |  28 +-
 t/t5328-commit-graph-64bit-time.sh          |   2 +-
 t/t5329-pack-objects-cruft.sh               |   8 +-
 t/t5334-incremental-multi-pack-index.sh     |   2 +-
 t/t5335-compact-multi-pack-index.sh         |   4 +-
 t/t5351-unpack-large-objects.sh             |   2 +-
 t/t5402-post-merge-hook.sh                  |   4 +-
 t/t5403-post-checkout-hook.sh               |   2 +-
 t/t5404-tracking-branches.sh                |   2 +-
 t/t5406-remote-rejects.sh                   |   2 +-
 t/t5407-post-rewrite-hook.sh                |   8 +-
 t/t5409-colorize-remote-messages.sh         |  36 +-
 t/t5500-fetch-pack.sh                       |  38 +--
 t/t5504-fetch-receive-strict.sh             |  14 +-
 t/t5505-remote.sh                           |  20 +-
 t/t5510-fetch.sh                            |  10 +-
 t/t5512-ls-remote.sh                        |   8 +-
 t/t5514-fetch-multiple.sh                   |   2 +-
 t/t5516-fetch-push.sh                       |  20 +-
 t/t5520-pull.sh                             |   4 +-
 t/t5524-pull-msg.sh                         |   6 +-
 t/t5526-fetch-submodules.sh                 |  16 +-
 t/t5529-push-errors.sh                      |   4 +-
 t/t5530-upload-pack-error.sh                |  18 +-
 t/t5531-deep-submodule-push.sh              |   2 +-
 t/t5532-fetch-proxy.sh                      |   2 +-
 t/t5533-push-cas.sh                         |  12 +-
 t/t5534-push-signed.sh                      |  22 +-
 t/t5537-fetch-shallow.sh                    |   2 +-
 t/t5538-push-shallow.sh                     |   2 +-
 t/t5539-fetch-http-shallow.sh               |   4 +-
 t/t5541-http-push-smart.sh                  |  32 +-
 t/t5544-pack-objects-hook.sh                |  12 +-
 t/t5550-http-fetch-dumb.sh                  |   4 +-
 t/t5551-http-fetch-smart.sh                 |  46 +--
 t/t5552-skipping-fetch-negotiator.sh        |   6 +-
 t/t5554-noop-fetch-negotiator.sh            |   4 +-
 t/t5557-http-get.sh                         |   2 +-
 t/t5558-clone-bundle-uri.sh                 |  38 +--
 t/t5562-http-backend-content-length.sh      |   2 +-
 t/t5564-http-proxy.sh                       |  10 +-
 t/t5581-http-curl-verbose.sh                |   2 +-
 t/t5583-push-branches.sh                    |   8 +-
 t/t5601-clone.sh                            |  28 +-
 t/t5604-clone-reference.sh                  |   8 +-
 t/t5605-clone-local.sh                      |   2 +-
 t/t5606-clone-options.sh                    |   6 +-
 t/t5612-clone-refspec.sh                    |   2 +-
 t/t5616-partial-clone.sh                    |  60 ++--
 t/t5619-clone-local-ambiguous-transport.sh  |   2 +-
 t/t5620-backfill.sh                         |  12 +-
 t/t5700-protocol-v1.sh                      |  46 +--
 t/t5701-git-serve.sh                        |  14 +-
 t/t5702-protocol-v2.sh                      | 154 ++++-----
 t/t5703-upload-pack-ref-in-want.sh          |  22 +-
 t/t5705-session-id-in-capabilities.sh       |  12 +-
 t/t5750-bundle-uri-parse.sh                 |   8 +-
 t/t5801-remote-helpers.sh                   |   4 +-
 t/t5810-proto-disable-local.sh              |   2 +-
 t/t5813-proto-disable-ssh.sh                |   4 +-
 t/t6000-rev-list-misc.sh                    |  26 +-
 t/t6005-rev-list-count.sh                   |   8 +-
 t/t6006-rev-list-format.sh                  |   4 +-
 t/t6009-rev-list-parent.sh                  |   4 +-
 t/t6020-bundle-misc.sh                      |  12 +-
 t/t6022-rev-list-missing.sh                 |   4 +-
 t/t6030-bisect-porcelain.sh                 | 150 ++++-----
 t/t6040-tracking-info.sh                    |   2 +-
 t/t6112-rev-list-filters-objects.sh         |  24 +-
 t/t6115-rev-list-du.sh                      |   4 +-
 t/t6120-describe.sh                         |  14 +-
 t/t6200-fmt-merge-msg.sh                    |  82 ++---
 t/t6402-merge-rename.sh                     |   4 +-
 t/t6403-merge-file.sh                       |   6 +-
 t/t6404-recursive-merge.sh                  |   2 +-
 t/t6406-merge-attr.sh                       |  20 +-
 t/t6417-merge-ours-theirs.sh                |  30 +-
 t/t6418-merge-text-auto.sh                  |   2 +-
 t/t6422-merge-rename-corner-cases.sh        |   8 +-
 t/t6423-merge-rename-directories.sh         |  72 ++--
 t/t6424-merge-unrelated-index-changes.sh    |   6 +-
 t/t6427-diff3-conflict-markers.sh           |  10 +-
 t/t6432-merge-recursive-space-options.sh    |   4 +-
 t/t6436-merge-overwrite.sh                  |   6 +-
 t/t6437-submodule-merge.sh                  |  10 +-
 t/t6500-gc.sh                               |   8 +-
 t/t6600-test-reach.sh                       |   4 +-
 t/t7001-mv.sh                               |  16 +-
 t/t7002-mv-sparse-checkout.sh               |  38 +--
 t/t7003-filter-branch.sh                    |  16 +-
 t/t7004-tag.sh                              |   2 +-
 t/t7006-pager.sh                            |  16 +-
 t/t7012-skip-worktree-writing.sh            |   6 +-
 t/t7030-verify-tag.sh                       |  52 +--
 t/t7031-verify-tag-signed-ssh.sh            |  46 +--
 t/t7102-reset.sh                            |   2 +-
 t/t7110-reset-merge.sh                      |  40 +--
 t/t7201-co.sh                               |   6 +-
 t/t7300-clean.sh                            |   2 +-
 t/t7301-clean-interactive.sh                |   2 +-
 t/t7400-submodule-basic.sh                  |  32 +-
 t/t7402-submodule-rebase.sh                 |   2 +-
 t/t7406-submodule-update.sh                 |  26 +-
 t/t7416-submodule-dash-url.sh               |  20 +-
 t/t7417-submodule-path-url.sh               |   2 +-
 t/t7450-bad-git-dotfiles.sh                 |  14 +-
 t/t7501-commit-basic-functionality.sh       |  16 +-
 t/t7502-commit-porcelain.sh                 |   2 +-
 t/t7507-commit-verbose.sh                   |   6 +-
 t/t7508-status.sh                           |   6 +-
 t/t7510-signed-commit.sh                    |  68 ++--
 t/t7516-commit-races.sh                     |   4 +-
 t/t7519-status-fsmonitor.sh                 |  14 +-
 t/t7527-builtin-fsmonitor.sh                |  82 ++---
 t/t7528-signed-commit-ssh.sh                |  68 ++--
 t/t7600-merge.sh                            |  10 +-
 t/t7603-merge-reduce-heads.sh               |  20 +-
 t/t7606-merge-custom.sh                     |   2 +-
 t/t7607-merge-state.sh                      |   4 +-
 t/t7610-mergetool.sh                        |  18 +-
 t/t7700-repack.sh                           |  14 +-
 t/t7703-repack-geometric.sh                 |   4 +-
 t/t7704-repack-cruft.sh                     |  12 +-
 t/t7800-difftool.sh                         |  26 +-
 t/t7810-grep.sh                             |  22 +-
 t/t7814-grep-recurse-submodules.sh          |   2 +-
 t/t7900-maintenance.sh                      |  34 +-
 t/t8008-blame-formats.sh                    |   2 +-
 t/t8010-cat-file-filters.sh                 |   2 +-
 t/t8012-blame-colors.sh                     |   2 +-
 t/t9001-send-email.sh                       | 190 +++++------
 t/t9003-help-autocorrect.sh                 |   6 +-
 t/t9106-git-svn-commit-diff-clobber.sh      |   2 +-
 t/t9107-git-svn-migrate.sh                  |  30 +-
 t/t9110-git-svn-use-svm-props.sh            |  20 +-
 t/t9111-git-svn-use-svnsync-props.sh        |  18 +-
 t/t9114-git-svn-dcommit-merge.sh            |   6 +-
 t/t9116-git-svn-log.sh                      |   8 +-
 t/t9117-git-svn-init-clone.sh               |  12 +-
 t/t9119-git-svn-info.sh                     |  16 +-
 t/t9122-git-svn-author.sh                   |   8 +-
 t/t9130-git-svn-authors-file.sh             |   8 +-
 t/t9138-git-svn-authors-prog.sh             |  14 +-
 t/t9140-git-svn-reset.sh                    |   4 +-
 t/t9153-git-svn-rewrite-uuid.sh             |   4 +-
 t/t9200-git-cvsexportcommit.sh              |   2 +-
 t/t9210-scalar.sh                           |  34 +-
 t/t9211-scalar-clone.sh                     |  16 +-
 t/t9300-fast-import.sh                      |  10 +-
 t/t9350-fast-export.sh                      |  54 +--
 t/t9351-fast-export-anonymize.sh            |  36 +-
 t/t9400-git-cvsserver-server.sh             |   4 +-
 t/t9501-gitweb-standalone-http-status.sh    |  58 ++--
 t/t9502-gitweb-standalone-parse-output.sh   |  38 +--
 t/t9800-git-p4-basic.sh                     |  10 +-
 t/t9801-git-p4-branch.sh                    |  48 +--
 t/t9806-git-p4-options.sh                   |  10 +-
 t/t9807-git-p4-submit.sh                    |   2 +-
 t/t9810-git-p4-rcs.sh                       |   8 +-
 t/t9813-git-p4-preserve-users.sh            |   8 +-
 t/t9814-git-p4-rename.sh                    |   8 +-
 t/t9827-git-p4-change-filetype.sh           |   4 +-
 t/t9832-unshelve.sh                         |   6 +-
 t/t9833-errors.sh                           |   4 +-
 t/t9835-git-p4-metadata-encoding-python2.sh |  36 +-
 t/t9836-git-p4-metadata-encoding-python3.sh |  38 +--
 t/t9850-shell.sh                            |   2 +-
 t/t9902-completion.sh                       |  26 +-
 338 files changed, 2807 insertions(+), 2807 deletions(-)
t/for-each-ref-tests.sh +6 -6
index bd2d45c971..6b359d940e 100644
@@ -523,7 +523,7 @@ test_expect_success 'Verify descending sort' '
 
 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
+	test_grep "^usage: ${git_for_each_ref}" actual
 '
 
 cat >expected <<\EOF
@@ -622,7 +622,7 @@ test_expect_success 'Quoting style: tcl' '
 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 @@ test_expect_success "${git_for_each_ref} --stdin: fails if extra args" '
 	>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 "${git_for_each_ref} with nested tags" '
 
 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)"
t/lib-bitmap.sh +6 -6
index 62aa6744a6..2de6646128 100644
@@ -173,7 +173,7 @@ rev_list_tests_head () {
 
 	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 @@ basic_bitmap_tests () {
 		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 @@ test_rev_exists () {
 		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
 	'
 }
 
t/lib-bundle-uri-protocol.sh +13 -13
index de09b6b02e..ecf3284297 100644
@@ -63,9 +63,9 @@ test_expect_success "connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: no
 		>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 @@ test_expect_success "connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: hav
 		>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 @@ test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque
 		>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 @@ test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque
 		>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 @@ test_expect_success "clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque
 		>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
t/lib-httpd.sh +1 -1
index fc646447d5..a216e5376f 100644
@@ -275,7 +275,7 @@ test_http_push_nonff () {
 	'
 
 	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' '
t/pack-refs-tests.sh +1 -1
index d76b087b09..a800177446 100644
@@ -195,7 +195,7 @@ test_expect_success 'delete ref while another dangling packed ref' '
 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
 '
 
t/show-ref-exists-tests.sh +1 -1
index 36e8e9df33..1351f690eb 100644
@@ -19,7 +19,7 @@ test_expect_success '--exists with missing reference' '
 
 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' '
t/t0000-basic.sh +8 -8
index 2b63e1c86c..7bd1f4e7b7 100755
@@ -743,7 +743,7 @@ test_expect_success 'subtest: lazy prereqs do not turn off tracing' '
 	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 'subtest: test_atexit is run' '
 
 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_expect_success 'test_oid can look up data for SHA-1' '
 	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_expect_success 'test_oid can look up data for SHA-256' '
 	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_expect_success 'test_bool_env' '
 		# 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 on a failing git command with env' '
 
 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
t/t0001-init.sh +9 -9
index e4d32bb4d2..7dcd1e9d48 100755
@@ -278,9 +278,9 @@ test_expect_success POSIXPERM 'init creates a new deep directory (umask vs. shar
 		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 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage is not allowed wi
 	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 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back
 	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 @@ test_expect_success MINGW 'redirect std handles' '
 		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 @@ test_expect_success '--initial-branch' '
 	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_expect_success 'overridden default main branch name (env)' '
 	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' '
t/t0008-ignores.sh +4 -4
index d77a179bdd..ed95faf327 100755
@@ -790,8 +790,8 @@ test_expect_success 'existing file and directory' '
 	>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 @@ test_expect_success 'existing directory and file' '
 	>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)' '
t/t0009-git-dir-validation.sh +3 -3
index 33d21ed9ea..4cba478e50 100755
@@ -35,7 +35,7 @@ test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '
 		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 @@ test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejecte
 		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 @@ test_expect_success 'setup: .git with garbage content is rejected' '
 		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
 	)
 '
 
t/t0012-help.sh +2 -2
index c33501bdcd..48d128ad04 100755
@@ -131,8 +131,8 @@ test_expect_success 'git help succeeds without git.html' '
 
 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' '
t/t0013-sha1dc.sh +1 -1
index ce3d81227a..3ea3169d92 100755
@@ -16,7 +16,7 @@ fi
 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
t/t0017-env-helper.sh +2 -2
index 32fe848179..da02bce0ef 100755
@@ -88,7 +88,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '
 	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_expect_success 'test-tool env-helper reads config thanks to trace2' '
 		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
t/t0021-conversion.sh +9 -9
index f0d50d769e..50994a6ada 100755
@@ -731,7 +731,7 @@ test_expect_success 'process filter should restart after unexpected write failur
 		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 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '
 
 		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
 	)
 '
 
@@ -953,7 +953,7 @@ test_expect_success 'missing file in delayed checkout' '
 
 	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' '
@@ -974,7 +974,7 @@ test_expect_success 'invalid file in delayed checkout' '
 
 	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'
@@ -1015,7 +1015,7 @@ do
 
 		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
@@ -1053,7 +1053,7 @@ test_expect_success SYMLINKS,CASE_INSENSITIVE_FS \
 		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
 	)
 '
@@ -1144,9 +1144,9 @@ test_expect_success 'delayed checkout correctly reports the number of updated en
 
 		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
 	)
 '
 
t/t0029-core-unsetenvvars.sh +2 -2
index baa1b7e85b..975620b389 100755
@@ -20,10 +20,10 @@ test_expect_success 'core.unsetenvvars works' '
 	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
t/t0030-stripspace.sh +2 -2
index 43155f6bd8..d6e2f63c59 100755
@@ -407,12 +407,12 @@ test_expect_success 'strip comments with changed comment string' '
 
 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' '
t/t0031-lockfile-pid.sh +1 -1
index 8ef87addf5..e9e2f04049 100755
@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '
 		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
 	)
 '
 
t/t0040-parse-options.sh +26 -26

129 lines not on this page · raw message

t/t0041-usage.sh +1 -1
index a0f6f134c7..51af7cc030 100755
@@ -13,7 +13,7 @@ test_expect_success 'setup ' '
 
 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
 '
 
t/t0052-simple-ipc.sh +5 -5
index ff98be31a5..14cea84920 100755
@@ -33,19 +33,19 @@ test_expect_success 'servers cannot share the same path' '
 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 'slow response' '
 #
 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 @@ test_expect_success 'stress test threads' '
 		--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
t/t0061-run-command.sh +1 -1
index 60cfe65979..dc9d77b0e1 100755
@@ -97,7 +97,7 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '
 	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' '
t/t0066-dir-iterator.sh +1 -1
index df3e9f5fa5..9fbb41bac5 100755
@@ -127,7 +127,7 @@ test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default
 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
t/t0068-for-each-repo.sh +8 -8
index 80b163ea99..55b37d895a 100755
@@ -21,23 +21,23 @@ test_expect_success 'run based on configured value' '
 
 	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" &&
t/t0070-fundamental.sh +3 -3
index 6b9dcf984b..8f573c2a0e 100755
@@ -10,7 +10,7 @@ Verify wrappers and compatibility functions.
 
 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_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file
 	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 'check for a bug in the regex routines' '
 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' '
t/t0081-find-pack.sh +6 -6
index 26f017422d..ff9d56fbf1 100755
@@ -52,17 +52,17 @@ test_expect_success 'add more packfiles' '
 
 	# 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
 '
t/t0091-bugreport.sh +9 -9
index e38ca7a901..81ed41742c 100755
@@ -40,15 +40,15 @@ test_expect_success 'sanity check "System Info" section' '
 
 	# 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 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '
 	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_expect_success UNZIP '--diagnose creates diagnostics zip archive' '
 	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 @@ test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '
 
 	# 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/"
t/t0092-diagnose.sh +6 -6
index 6cabd6e67b..0bc2b1ed5c 100755
@@ -8,7 +8,7 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '
 	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_expect_success UNZIP 'creates diagnostics zip archive' '
 	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 @@ test_expect_success UNZIP 'counts loose objects' '
 	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 @@ test_expect_success UNZIP '--mode=stats excludes .git dir contents' '
 	# 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 @@ test_expect_success UNZIP '--mode=all includes .git dir contents' '
 	# 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/" &&
t/t0100-previous.sh +1 -1
index dd5d9b4e5e..87a9995ea4 100755
@@ -50,7 +50,7 @@ test_expect_success 'merge @{-1}~1' '
 	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' '
t/t0200-gettext-basic.sh +7 -7
index 8853d8afb9..8db26c1ada 100755
@@ -16,7 +16,7 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '
 '
 
 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' '
 '
 
 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 'sanity: Icelandic locale was compiled' '
 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 <[email protected]>" 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 <[email protected]>" 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' '
t/t0203-gettext-setlocale-sanity.sh +2 -2
index 0ce1f22eff..670082bf09 100755
@@ -12,7 +12,7 @@ test_expect_success 'git show a ISO-8859-1 commit under C locale' '
 	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_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 l
 	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
t/t0204-gettext-reencode-sanity.sh +4 -4
index 28d92bb9b7..48ccddaf7d 100755
@@ -66,22 +66,22 @@ test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' '
 # 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
t/t0210-trace2-normal.sh +3 -3
index 7e1e7af862..beed2b27a5 100755
@@ -333,12 +333,12 @@ test_expect_success 'unsafe URLs are redacted by default' '
 
 	GIT_TRACE2="$(pwd)/trace.normal" \
 		git clone https://user:[email protected]/ 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:[email protected]/ clone2 &&
-	grep "start .* clone https://user:[email protected]" unredacted.normal &&
-	grep "remote.origin.url=https://user:[email protected]" unredacted.normal
+	test_grep "start .* clone https://user:[email protected]" unredacted.normal &&
+	test_grep "remote.origin.url=https://user:[email protected]" unredacted.normal
 '
 
 test_done
t/t0211-trace2-perf.sh +40 -40

159 lines not on this page · raw message

t/t0212-trace2-event.sh +4 -4
index 1211db9f46..f5358a1dd4 100755
@@ -332,7 +332,7 @@ test_expect_success 'unsafe URLs are redacted by default in cmd_start events' '
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 300redact_start git clone https://user:[email protected]/ 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 @@ test_expect_success 'unsafe URLs are redacted by default in child_start events'
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 301redact_child_start git clone https://user:[email protected]/ 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 @@ test_expect_success 'unsafe URLs are redacted by default in exec events' '
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 302redact_exec git clone https://user:[email protected]/ 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 @@ test_expect_success 'unsafe URLs are redacted by default in def_param events' '
 
 	GIT_TRACE2_EVENT="$(pwd)/trace.event" \
 		test-tool trace2 303redact_def_param url https://user:[email protected]/ &&
-	! grep user:pwd trace.event
+	test_grep ! user:pwd trace.event
 '
 
 test_done
t/t0300-credentials.sh +2 -2
index 64ead1571a..ea11cdb4dc 100755
@@ -1014,7 +1014,7 @@ test_expect_success 'credential config with partial URLs' '
 	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 @@ test_expect_success 'credential config with partial URLs' '
 	do
 		git -c credential.$partial.helper=yep \
 			credential fill <stdin >stdout &&
-		! grep yep stdout ||
+		test_grep ! yep stdout ||
 		return 1
 	done &&
 
t/t0410-partial-clone.sh +41 -41

223 lines not on this page · raw message

t/t0450-txt-doc-vs-help.sh +1 -1
index 822b0d55a5..e7ba967223 100755
@@ -87,7 +87,7 @@ do
 	# -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" '
t/t0500-progress-display.sh +9 -9
index d1a498a216..fe2232cce0 100755
@@ -320,8 +320,8 @@ test_expect_success 'progress generates traces' '
 
 	# 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 @@ test_expect_success 'progress generates traces: start without stop' '
 	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 @@ test_expect_success 'progress generates traces: stop without start' '
 
 	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 @@ test_expect_success 'progress generates traces: start with active progress bar (
 	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
t/t0610-reftable-basics.sh +4 -4
index e19e036898..2f4c5e2a1b 100755
@@ -776,11 +776,11 @@ test_expect_success 'reflog: can delete separate reflog entries' '
 		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 @@ test_expect_success 'reflog: garbage collection deletes reflog entries' '
 		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 &&
t/t1004-read-tree-m-u-wf.sh +4 -4
index 11bf10424f..70ba3eea4d 100755
@@ -142,8 +142,8 @@ test_expect_success '3-way not overwriting local changes (our side)' '
 
 	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 @@ test_expect_success '3-way not overwriting local changes (their side)' '
 
 	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
 
 '
 
t/t1006-cat-file.sh +9 -9
index 8e2c52652c..762c77c351 100755
@@ -696,8 +696,8 @@ test_expect_success '%(deltabase) reports packed delta bases' '
 	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 +826,7 @@ test_expect_success 'cat-file -t and -s on corrupt loose object' '
 		# 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 +1318,37 @@ test_expect_success 'cat-file --batch-all-objects --batch-check ignores replace'
 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='
t/t1007-hash-object.sh +4 -4
index de076293b6..4782a6246a 100755
@@ -202,7 +202,7 @@ done
 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' '
@@ -210,7 +210,7 @@ test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '
 	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' '
@@ -218,7 +218,7 @@ test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '
 	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' '
@@ -229,7 +229,7 @@ test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '
 		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' '
t/t1011-read-tree-sparse-checkout.sh +5 -5
index 742f0fa909..93244ee134 100755
@@ -196,7 +196,7 @@ test_expect_success 'read-tree will not throw away dirty changes, non-sparse' '
 	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 @@ test_expect_success 'read-tree will not throw away dirty changes, sparse' '
 	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 @@ test_expect_success 'read-tree updates worktree, dirty case' '
 	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 @@ test_expect_success 'read-tree removes worktree, dirty case' '
 	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 @@ test_expect_success 'read-tree adds to worktree, dirty case' '
 	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' '
t/t1050-large.sh +3 -3
index 7d40d08521..d295c265c7 100755
@@ -8,7 +8,7 @@ test_description='adding and checking out large blobs'
 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 --stat' '
 
 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' '
t/t1091-sparse-checkout-builtin.sh +12 -12
index cd0aed9975..74b1761e0c 100755
@@ -129,7 +129,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '
 		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 @@ test_expect_success 'cone mode clears ignored subdirectories' '
 	# 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 @@ test_expect_success 'malformed cone-mode patterns' '
 	# 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 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '
 	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 @@ test_expect_success 'clean' '
 	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_expect_success 'sparse-checkout operations with merge conflicts' '
 		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 &&
t/t1092-sparse-checkout-compatibility.sh +22 -22

135 lines not on this page · raw message

t/t1300-config.sh +8 -8
index 11fc976f3a..b337e25d92 100755
@@ -800,7 +800,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		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
@@ -1615,9 +1615,9 @@ test_expect_success 'git --config-env=key=envvar support' '
 
 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' '
@@ -2048,7 +2048,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	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]
@@ -2068,7 +2068,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	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]
@@ -2599,7 +2599,7 @@ test_expect_success '--type rejects unknown specifiers' '
 
 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
 '
 
@@ -2911,12 +2911,12 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 
 	# 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' '
t/t1305-config-include.sh +1 -1
index f3892578e4..f6115269f9 100755
@@ -353,7 +353,7 @@ test_expect_success 'include cycles are detected' '
 	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' '
t/t1308-config-set.sh +3 -3
index e0e49053f0..de95161a83 100755
@@ -180,7 +180,7 @@ test_expect_success 'find integer if value is non parse-able' '
 
 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 @@ test_expect_success 'proper error on directory "files"' '
 	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_expect_success POSIXPERM,SANITY 'proper error on non-accessible files' '
 	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
 '
t/t1400-update-ref.sh +85 -85

625 lines not on this page · raw message

t/t1403-show-ref.sh +9 -9
index 36c903ca19..7ce5af4ab8 100755
@@ -213,19 +213,19 @@ test_expect_success 'show-ref --verify with dangling ref' '
 
 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
t/t1410-reflog.sh +2 -2
index ce71f9a30a..81de1d40eb 100755
@@ -108,12 +108,12 @@ test_expect_success setup '
 
 test_expect_success 'correct usage on sub-command -h' '
 	test_expect_code 129 git reflog expire -h >err &&
-	grep "git reflog expire" 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
+	test_grep -F "git reflog [show]" err
 '
 
 test_expect_success 'pass through -- to sub-command' '
t/t1415-worktree-refs.sh +2 -2
index 51d79bae83..6b2ad04aef 100755
@@ -32,7 +32,7 @@ test_expect_success 'ambiguous main-worktree/HEAD' '
 	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 @@ test_expect_success 'ambiguous worktrees/xx/HEAD' '
 	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' '
t/t1430-bad-ref-name.sh +28 -28

153 lines not on this page · raw message

t/t1450-fsck.sh +6 -6
index 54e81c2636..47d460a699 100755
@@ -68,7 +68,7 @@ test_expect_success 'object with hash mismatch' '
 		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_expect_success 'commit with multiple signatures is okay' '
 	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 @@ test_expect_success 'email without @ is okay' '
 	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' '
@@ -626,7 +626,7 @@ test_expect_success 'fsck notices excessively large tree entry name' '
 		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 +849,7 @@ test_expect_success 'fsck errors in packed objects' '
 	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 +1027,7 @@ test_expect_success 'bogus head does not fallback to all heads' '
 	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.
t/t1451-fsck-buffer.sh +3 -3
index 3a3d33f405..14151eabbd 100755
@@ -46,7 +46,7 @@ check () {
 			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 @@ ident_checks tag tagger
 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 @@ test_expect_success 'truncated tree (missing nul)' '
 	# 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
t/t1460-refs-migrate.sh +1 -1
index 5246468024..8f42697143 100755
@@ -212,7 +212,7 @@ do
 			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 &&
t/t1500-rev-parse.sh +3 -3
index 38067d95f7..4174ca40c3 100755
@@ -204,7 +204,7 @@ test_expect_success 'rev-parse --show-object-format in repo' '
 	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 @@ test_expect_success RUST 'rev-parse --show-object-format in repo with compat mod
 		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 @@ test_expect_success 'rev-parse --show-ref-format with invalid storage' '
 		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
 	)
 '
 
t/t1502-rev-parse-parseopt.sh +1 -1
index 3962f1d288..2cab204376 100755
@@ -331,7 +331,7 @@ test_expect_success 'ambiguous: --no matches both --noble and --no-noble' '
 	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
t/t1503-rev-parse-verify.sh +5 -5
index 75a708f9ba..87638a4a2c 100755
@@ -62,15 +62,15 @@ test_expect_success 'works with one good rev' '
 
 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' '
t/t1510-repo-setup.sh +5 -5
index bbfe05b8e4..d330b8df47 100755
@@ -604,7 +604,7 @@ test_expect_success '#20b/c: core.worktree and core.bare conflict' '
 		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 @@ test_expect_success '#22.2: core.worktree and core.bare conflict' '
 		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 @@ test_expect_success '#28: core.worktree and core.bare conflict (gitfile case)' '
 		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 @@ test_expect_success '#30: core.worktree and core.bare conflict (gitfile version)
 		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
t/t1512-rev-parse-disambiguation.sh +2 -2
index 1a380a4184..0e6b42ec78 100755
@@ -378,7 +378,7 @@ test_expect_success 'ambiguous 40-hex ref' '
 	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 @@ test_expect_success 'ambiguous short sha1 ref' '
 	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)' '
t/t1515-rev-parse-outside-repo.sh +1 -1
index 75e89c4b6e..2cb3c7cc9c 100755
@@ -32,7 +32,7 @@ test_expect_success 'rev-parse --local-env-vars' '
 	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' '
t/t1800-hook.sh +9 -9
index 0132e772e4..9aae3ff074 100755
@@ -79,7 +79,7 @@ test_expect_success 'git hook usage' '
 	test_expect_code 129 git hook run --unknown 2>err &&
 	test_expect_code 129 git hook list &&
 	test_expect_code 129 git hook list -h &&
-	grep "unknown option" err
+	test_grep "unknown option" err
 '
 
 test_expect_success 'git hook list: unknown hook name is rejected' '
@@ -361,9 +361,9 @@ test_expect_success 'hook can be configured for multiple events' '
 
 	# 'ghi' should be included in both 'pre-commit' and 'test-hook'
 	git hook list pre-commit >actual &&
-	grep "ghi" actual &&
+	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 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	# 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 @@ test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true'
 	# 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 @@ test_expect_success 'hook.jobs=-1 resolves to online_cpus()' '
 	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 @@ test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '
 	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 @@ test_expect_success 'git hook run -j-1 resolves to online_cpus()' '
 	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' '
t/t2004-checkout-cache-temp.sh +2 -2
index 0afe0ff7ca..db06c321ed 100755
@@ -132,8 +132,8 @@ test_expect_success 'overriding --stage=all resets implied --temp' '
 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 &&
[… more lines in the raw message]

Lines of this file are not shown. View the raw message.

t/t2019-checkout-ambiguous-ref.sh +2 -2
index 1fcef4be95..0ac35798c2 100755

Lines of this file are not shown. View the raw message.

t/t2024-checkout-dwim.sh +4 -4
index a3b1449ef1..752e7f0e61 100755

Lines of this file are not shown. View the raw message.

t/t2030-unresolve-info.sh +3 -3
index be3fcdde07..9af24b1204 100755

Lines of this file are not shown. View the raw message.

t/t2060-switch.sh +3 -3
index c91c4db936..cec2fe6876 100755

Lines of this file are not shown. View the raw message.

t/t2070-restore.sh +1 -1
index 16d6348b69..2c222fb934 100755

Lines of this file are not shown. View the raw message.

t/t2080-parallel-checkout-basics.sh +7 -7
index 5ffe1a41e2..88a6ec1826 100755

Lines of this file are not shown. View the raw message.

t/t2081-parallel-checkout-collisions.sh +12 -12
index f6fcfc0c1e..db45f5f30f 100755

Lines of this file are not shown. View the raw message.

t/t2082-parallel-checkout-attributes.sh +6 -6
index 79fb11f139..1f43c2a267 100755

Lines of this file are not shown. View the raw message.

t/t2103-update-index-ignore-missing.sh +3 -3
index 6938ecca86..fbfc86b1fa 100755

Lines of this file are not shown. View the raw message.

t/t2200-add-update.sh +1 -1
index 0a96655cfe..bb019d7a7c 100755

Lines of this file are not shown. View the raw message.

t/t2203-add-intent.sh +3 -3
index 44c1936e4d..69fef7e646 100755

Lines of this file are not shown. View the raw message.

t/t2400-worktree-add.sh +12 -12
index 58b4445cc4..87b926728a 100755

Lines of this file are not shown. View the raw message.

t/t2402-worktree-list.sh +8 -8
index 93f92e854a..eeaf800d74 100755

Lines of this file are not shown. View the raw message.

t/t2403-worktree-move.sh +3 -3
index 0bb33e8b1b..69768c1207 100755

Lines of this file are not shown. View the raw message.

t/t2405-worktree-submodule.sh +3 -3
index 11018f37c7..f5c94d490b 100755

Lines of this file are not shown. View the raw message.

t/t2407-worktree-heads.sh +13 -13
index 57c201869f..a6d7e0f83b 100755

Lines of this file are not shown. View the raw message.

t/t2500-untracked-overwriting.sh +4 -4
index 5c0bf4d21f..8e84f29e8d 100755

Lines of this file are not shown. View the raw message.

t/t2501-cwd-empty.sh +2 -2
index be9140bbaa..eb6451d71d 100755

Lines of this file are not shown. View the raw message.

t/t3001-ls-files-others-exclude.sh +3 -3
index 202fb8d9ea..29a0a25b30 100755

Lines of this file are not shown. View the raw message.

t/t3007-ls-files-recurse-submodules.sh +3 -3
index 61771eec83..218964edab 100755

Lines of this file are not shown. View the raw message.

t/t3200-branch.sh +6 -6
index e7829c2c4b..bd839998b1 100755

Lines of this file are not shown. View the raw message.

t/t3202-show-branch.sh +5 -5
index a1139f79e2..f402334936 100755

Lines of this file are not shown. View the raw message.

t/t3203-branch-output.sh +2 -2
index a6bd88a58d..7c44186341 100755

Lines of this file are not shown. View the raw message.

t/t3206-range-diff.sh +39 -39

78 lines not on this page · raw message

t/t3207-branch-submodule.sh +2 -2
index fe72b24716..4afb2d3198 100755

Lines of this file are not shown. View the raw message.

t/t3301-notes.sh +16 -16
index d6c50460d0..18a6000171 100755

Lines of this file are not shown. View the raw message.

t/t3310-notes-merge-manual-resolve.sh +8 -8
index 0bb366fdb8..1d61c997ff 100755

Lines of this file are not shown. View the raw message.

t/t3320-notes-merge-worktrees.sh +1 -1
index 96243b7222..9a2c3ac3f7 100755

Lines of this file are not shown. View the raw message.

t/t3400-rebase.sh +8 -8
index c0c00fbb7b..e62e07b894 100755

Lines of this file are not shown. View the raw message.

t/t3402-rebase-merge.sh +8 -8
index 761de63b6b..41ebcdec9f 100755

Lines of this file are not shown. View the raw message.

t/t3404-rebase-interactive.sh +36 -36

72 lines not on this page · raw message

t/t3406-rebase-message.sh +3 -3
index bc51a9d3a7..7b61188e1f 100755

Lines of this file are not shown. View the raw message.

t/t3415-rebase-autosquash.sh +5 -5
index 5033411a43..07a5a11678 100755

Lines of this file are not shown. View the raw message.

t/t3416-rebase-onto-threedots.sh +2 -2
index ea501f2b42..e7f725b8d0 100755

Lines of this file are not shown. View the raw message.

t/t3418-rebase-continue.sh +5 -5
index f9b8999db5..03e0714864 100755

Lines of this file are not shown. View the raw message.

t/t3420-rebase-autostash.sh +13 -13
index f0bbc476ff..f6cb3dd72e 100755

Lines of this file are not shown. View the raw message.

t/t3422-rebase-incompatible-options.sh +2 -2
index b9408f9ba1..d6830b82e0 100755

Lines of this file are not shown. View the raw message.

t/t3429-rebase-edit-todo.sh +1 -1
index abd66f3602..28e4bdd0d9 100755

Lines of this file are not shown. View the raw message.

t/t3430-rebase-merges.sh +16 -16
index 84b2d0e664..9bd61c58b3 100755

Lines of this file are not shown. View the raw message.

t/t3500-cherry.sh +2 -2
index 3e66827d76..03655c8a48 100755

Lines of this file are not shown. View the raw message.

t/t3501-revert-cherry-pick.sh +3 -3
index 8025a28cfd..939e7a16a6 100755

Lines of this file are not shown. View the raw message.

t/t3504-cherry-pick-rerere.sh +3 -3
index 18aeba161c..462973fa91 100755

Lines of this file are not shown. View the raw message.

t/t3510-cherry-pick-sequence.sh +12 -12
index 66ff9db270..5777dff496 100755

Lines of this file are not shown. View the raw message.

t/t3602-rm-sparse-checkout.sh +2 -2
index 02c7acd617..252df28bbf 100755

Lines of this file are not shown. View the raw message.

t/t3705-add-sparse-checkout.sh +5 -5
index 53a4782267..64ad7a2949 100755

Lines of this file are not shown. View the raw message.

t/t3800-mktag.sh +2 -2
index e3cf0ffbe5..d3ddf076c4 100755

Lines of this file are not shown. View the raw message.

t/t3901-i18n-patch.sh +8 -8
index ef7d7e1edc..b2f9301f49 100755

Lines of this file are not shown. View the raw message.

t/t3903-stash.sh +14 -14
index ecc35aae82..53328b8475 100755

Lines of this file are not shown. View the raw message.

t/t3904-stash-patch.sh +2 -2
index 90a4ff2c10..adc45c7073 100755

Lines of this file are not shown. View the raw message.

t/t3908-stash-in-worktree.sh +1 -1
index 2b2b366ef9..e7ae838bb2 100755

Lines of this file are not shown. View the raw message.

t/t4000-diff-format.sh +1 -1
index 32b14e3a71..74dec7d34a 100755

Lines of this file are not shown. View the raw message.

t/t4001-diff-rename.sh +2 -2
index 4f520d600d..ad474100af 100755

Lines of this file are not shown. View the raw message.

t/t4011-diff-symlink.sh +1 -1
index ac837b6c9e..4103bd6989 100755

Lines of this file are not shown. View the raw message.

t/t4013-diff-various.sh +1 -1
index d35695f5b0..b7a382c881 100755

Lines of this file are not shown. View the raw message.

t/t4014-format-patch.sh +172 -172

344 lines not on this page · raw message

t/t4015-diff-whitespace.sh +8 -8
index b691d29479..6e7ba8abf6 100755

Lines of this file are not shown. View the raw message.

t/t4017-diff-retval.sh +1 -1
index c2863c99b7..5ec6f5c612 100755

Lines of this file are not shown. View the raw message.

t/t4018-diff-funcname.sh +1 -1
index e026fac1f4..b538b715f5 100755

Lines of this file are not shown. View the raw message.

t/t4019-diff-wserror.sh +4 -4
index 4001dacee3..939ec8a0b4 100755

Lines of this file are not shown. View the raw message.

t/t4020-diff-external.sh +9 -9
index 7ec5854f74..93d37230c6 100755

Lines of this file are not shown. View the raw message.

t/t4021-format-patch-numbered.sh +2 -2
index 9be65fd444..204851f061 100755

Lines of this file are not shown. View the raw message.

t/t4022-diff-rewrite.sh +7 -7
index 6fed993ea0..b5c1b6868f 100755

Lines of this file are not shown. View the raw message.

t/t4028-format-patch-mime-headers.sh +3 -3
index a06a747926..39344cf761 100755

Lines of this file are not shown. View the raw message.

t/t4031-diff-rewrite-binary.sh +9 -9
index 15e012ccc7..5b31ca02fe 100755

Lines of this file are not shown. View the raw message.

t/t4033-diff-patience.sh +1 -1
index 113304dc59..8002c0f434 100755

Lines of this file are not shown. View the raw message.

t/t4036-format-patch-signer-mime.sh +3 -3
index 98d9713d8b..f57d3dc9fb 100755

Lines of this file are not shown. View the raw message.

t/t4038-diff-combined.sh +3 -3
index 2ce26e585c..e11b711388 100755

Lines of this file are not shown. View the raw message.

t/t4051-diff-function-context.sh +19 -19
index 4838a1df8b..117eaede8a 100755

Lines of this file are not shown. View the raw message.

t/t4053-diff-no-index.sh +2 -2
index 15076dfe0d..8e0394cf5a 100755

Lines of this file are not shown. View the raw message.

t/t4063-diff-blobs.sh +1 -1
index 50fdb5ea52..1318cceb19 100755

Lines of this file are not shown. View the raw message.

t/t4065-diff-anchored.sh +13 -13
index b3f510f040..9f06ac65be 100755

Lines of this file are not shown. View the raw message.

t/t4067-diff-partial-clone.sh +6 -6
index 30813109ac..142abdf74a 100755

Lines of this file are not shown. View the raw message.

t/t4073-diff-stat-name-width.sh +12 -12
index ec5d3c3c1f..2412420645 100755

Lines of this file are not shown. View the raw message.

t/t4103-apply-binary.sh +1 -1
index f2d41e06bc..bdd4ccf9e3 100755

Lines of this file are not shown. View the raw message.

t/t4120-apply-popt.sh +1 -1
index c960fdf622..c7801aec2c 100755

Lines of this file are not shown. View the raw message.

t/t4124-apply-ws-rule.sh +5 -5
index 205d86d05e..4c487fb576 100755

Lines of this file are not shown. View the raw message.

t/t4128-apply-root.sh +1 -1
index 5eba15fa66..c772faea62 100755

Lines of this file are not shown. View the raw message.

t/t4140-apply-ita.sh +2 -2
index 0b11a8aef4..49ebb51564 100755

Lines of this file are not shown. View the raw message.

t/t4141-apply-too-large.sh +1 -1
index eac6f7e151..b114a7adf7 100755

Lines of this file are not shown. View the raw message.

t/t4150-am.sh +24 -24

48 lines not on this page · raw message

t/t4200-rerere.sh +3 -3
index 1717f407c8..5466fb78bc 100755

Lines of this file are not shown. View the raw message.

t/t4201-shortlog.sh +1 -1
index 9f41d56d9a..023fbff546 100755

Lines of this file are not shown. View the raw message.

t/t4202-log.sh +42 -42

84 lines not on this page · raw message

t/t4204-patch-id.sh +1 -1
index 605faea0c7..90e661fe98 100755

Lines of this file are not shown. View the raw message.

t/t4205-log-pretty-formats.sh +1 -1
index 3865f6abc7..4be5c51489 100755

Lines of this file are not shown. View the raw message.

t/t4209-log-pickaxe.sh +5 -5
index 0e2f80a268..802597efc7 100755

Lines of this file are not shown. View the raw message.

t/t4211-line-log.sh +36 -36

72 lines not on this page · raw message

t/t4216-log-bloom.sh +9 -9
index 1064990de3..c66f25b932 100755

Lines of this file are not shown. View the raw message.

t/t4252-am-options.sh +11 -11
index bda8822b3d..c36c7e9017 100755

Lines of this file are not shown. View the raw message.

t/t4254-am-corrupt.sh +3 -3
index 96ddf3c53a..f68b5d3127 100755

Lines of this file are not shown. View the raw message.

t/t4258-am-quoted-cr.sh +1 -1
index 201915b45a..50e81b9330 100755

Lines of this file are not shown. View the raw message.

t/t4301-merge-tree-write-tree.sh +9 -9
index 6e117ee93c..b2684911d1 100755

Lines of this file are not shown. View the raw message.

t/t5000-tar-tree.sh +5 -5
index a8c28533dc..3ad600c793 100755

Lines of this file are not shown. View the raw message.

t/t5004-archive-corner-cases.sh +1 -1
index df513a4269..768b0ff85d 100755

Lines of this file are not shown. View the raw message.

t/t5100-mailinfo.sh +1 -1
index e57e1ae739..e01078abe7 100755

Lines of this file are not shown. View the raw message.

t/t5150-request-pull.sh +9 -9
index 270ce6ea48..67c5dc7e85 100755

Lines of this file are not shown. View the raw message.

t/t5300-pack-object.sh +11 -11
index 73445782e7..9dabb3615a 100755

Lines of this file are not shown. View the raw message.

t/t5302-pack-index.sh +3 -3
index 9697448cb2..735de1023e 100755

Lines of this file are not shown. View the raw message.

t/t5304-prune.sh +4 -4
index 2be7cd30de..db8e389c2c 100755

Lines of this file are not shown. View the raw message.

t/t5310-pack-bitmaps.sh +7 -7
index efeb71593b..33ccb579c5 100755

Lines of this file are not shown. View the raw message.

t/t5317-pack-objects-filter-objects.sh +6 -6
index dddb79ba62..2948a7cd2a 100755

Lines of this file are not shown. View the raw message.

t/t5318-commit-graph.sh +4 -4
index 1c40f904f8..6cecfbeaa1 100755

Lines of this file are not shown. View the raw message.

t/t5319-multi-pack-index.sh +8 -8
index fa0d4046f7..68143cb5b7 100755

Lines of this file are not shown. View the raw message.

t/t5324-split-commit-graph.sh +5 -5
index 49a057cc2e..89c0c4f901 100755

Lines of this file are not shown. View the raw message.

t/t5325-reverse-index.sh +1 -1
index 285c8b4a49..5493791938 100755

Lines of this file are not shown. View the raw message.

t/t5326-multi-pack-bitmaps.sh +14 -14
index 62bd973d92..86beab1dae 100755

Lines of this file are not shown. View the raw message.

t/t5328-commit-graph-64bit-time.sh +1 -1
index d8891e6a92..4e86aff509 100755

Lines of this file are not shown. View the raw message.

t/t5329-pack-objects-cruft.sh +4 -4
index 25ddda5cf3..12cda06373 100755

Lines of this file are not shown. View the raw message.

t/t5334-incremental-multi-pack-index.sh +1 -1
index 68a103d13d..f0b82b5f65 100755

Lines of this file are not shown. View the raw message.

t/t5335-compact-multi-pack-index.sh +2 -2
index ec1dafe89f..0f75ef2ccc 100755

Lines of this file are not shown. View the raw message.

t/t5351-unpack-large-objects.sh +1 -1
index d76eb4be93..7f621c5331 100755

Lines of this file are not shown. View the raw message.

t/t5402-post-merge-hook.sh +2 -2
index 915af2de95..c77aa56421 100755

Lines of this file are not shown. View the raw message.

t/t5403-post-checkout-hook.sh +1 -1
index cb0300b2d2..39009ce327 100755

Lines of this file are not shown. View the raw message.

t/t5404-tracking-branches.sh +1 -1
index cc07889667..a1661d34f2 100755

Lines of this file are not shown. View the raw message.

t/t5406-remote-rejects.sh +1 -1
index dcbeb42082..bb293b7010 100755

Lines of this file are not shown. View the raw message.

t/t5407-post-rewrite-hook.sh +4 -4
index ad7f8c6f00..ed9896e27a 100755

Lines of this file are not shown. View the raw message.

t/t5409-colorize-remote-messages.sh +18 -18
index 3010913bb1..5da8b0ad64 100755

Lines of this file are not shown. View the raw message.

t/t5500-fetch-pack.sh +19 -19
index 649a615ec9..6c1edf2117 100755

Lines of this file are not shown. View the raw message.

t/t5504-fetch-receive-strict.sh +7 -7
index 438250c75e..75b2b87999 100755

Lines of this file are not shown. View the raw message.

t/t5505-remote.sh +10 -10
index e592c0bcde..6f5e86dede 100755

Lines of this file are not shown. View the raw message.

t/t5510-fetch.sh +5 -5
index eca9a973b5..a0bbbce0f7 100755

Lines of this file are not shown. View the raw message.

t/t5512-ls-remote.sh +4 -4
index 5930f55186..fe44f39871 100755

Lines of this file are not shown. View the raw message.

t/t5514-fetch-multiple.sh +1 -1
index 523aff6268..5d79472c39 100755

Lines of this file are not shown. View the raw message.

t/t5516-fetch-push.sh +10 -10
index 1b986349a8..f3b3efc47f 100755

Lines of this file are not shown. View the raw message.

t/t5520-pull.sh +2 -2
index 0e0019347e..27f38ab3c8 100755

Lines of this file are not shown. View the raw message.

t/t5524-pull-msg.sh +3 -3
index b2be3605f5..493156a2b8 100755

Lines of this file are not shown. View the raw message.

t/t5526-fetch-submodules.sh +8 -8
index 1242ee9185..7b3b7359da 100755

Lines of this file are not shown. View the raw message.

t/t5529-push-errors.sh +2 -2
index 80b06a0cd2..35b1f212a8 100755

Lines of this file are not shown. View the raw message.

t/t5530-upload-pack-error.sh +9 -9
index d40292cfb7..25d409fc94 100755

Lines of this file are not shown. View the raw message.

t/t5531-deep-submodule-push.sh +1 -1
index 05debd1134..7d239dd31f 100755

Lines of this file are not shown. View the raw message.

t/t5532-fetch-proxy.sh +1 -1
index 95d0f33b29..ccbca7c2f2 100755

Lines of this file are not shown. View the raw message.

t/t5533-push-cas.sh +6 -6
index cba26a872d..2e14aecaa2 100755

Lines of this file are not shown. View the raw message.

t/t5534-push-signed.sh +11 -11
index 2a782214ee..21f0262cbd 100755

Lines of this file are not shown. View the raw message.

t/t5537-fetch-shallow.sh +1 -1
index 9982dd2aa6..2e80512773 100755

Lines of this file are not shown. View the raw message.

t/t5538-push-shallow.sh +1 -1
index dc0e972943..afab456b32 100755

Lines of this file are not shown. View the raw message.

t/t5539-fetch-http-shallow.sh +2 -2
index 3ea75d34ca..44e6f609df 100755

Lines of this file are not shown. View the raw message.

t/t5541-http-push-smart.sh +16 -16
index 538b603f03..8c34833027 100755

Lines of this file are not shown. View the raw message.

t/t5544-pack-objects-hook.sh +6 -6
index 89147a052e..908d3b8ebe 100755

Lines of this file are not shown. View the raw message.

t/t5550-http-fetch-dumb.sh +2 -2
index b0080bf204..f00eeae48f 100755

Lines of this file are not shown. View the raw message.

t/t5551-http-fetch-smart.sh +23 -23

46 lines not on this page · raw message

t/t5552-skipping-fetch-negotiator.sh +3 -3
index eeddb85b1d..96e224168e 100755

Lines of this file are not shown. View the raw message.

t/t5554-noop-fetch-negotiator.sh +2 -2
index 17e73b606d..d50ffe55b6 100755

Lines of this file are not shown. View the raw message.

t/t5557-http-get.sh +1 -1
index 67fcc23f11..e8ffe3f8e9 100755

Lines of this file are not shown. View the raw message.

t/t5558-clone-bundle-uri.sh +19 -19
index 7a0943bd36..0d5be0d56e 100755

Lines of this file are not shown. View the raw message.

t/t5562-http-backend-content-length.sh +1 -1
index b6ee06f5c8..59c6a50dd2 100755

Lines of this file are not shown. View the raw message.

t/t5564-http-proxy.sh +5 -5
index 817cd1f984..866e2b4e93 100755

Lines of this file are not shown. View the raw message.

t/t5581-http-curl-verbose.sh +1 -1
index cded79c16b..6788cf8171 100755

Lines of this file are not shown. View the raw message.

t/t5583-push-branches.sh +4 -4
index e7e1b6dab6..1ccfaee75a 100755

Lines of this file are not shown. View the raw message.

t/t5601-clone.sh +14 -14
index 3dd229c186..b6167582a1 100755

Lines of this file are not shown. View the raw message.

t/t5604-clone-reference.sh +4 -4
index c232ab8c15..39a0c318df 100755

Lines of this file are not shown. View the raw message.

t/t5605-clone-local.sh +1 -1
index 2397f8fa61..156362f145 100755

Lines of this file are not shown. View the raw message.

t/t5606-clone-options.sh +3 -3
index 8a15237736..1b48da496b 100755

Lines of this file are not shown. View the raw message.

t/t5612-clone-refspec.sh +1 -1
index 3126cfd7e9..dd1fe0abb4 100755

Lines of this file are not shown. View the raw message.

t/t5616-partial-clone.sh +30 -30

60 lines not on this page · raw message

t/t5619-clone-local-ambiguous-transport.sh +1 -1
index cce62bf78d..e4218e5c7f 100755

Lines of this file are not shown. View the raw message.

t/t5620-backfill.sh +6 -6
index d2ea68e065..7462280470 100755

Lines of this file are not shown. View the raw message.

t/t5700-protocol-v1.sh +23 -23

46 lines not on this page · raw message

t/t5701-git-serve.sh +7 -7
index d4c28bae39..108eb30945 100755

Lines of this file are not shown. View the raw message.

t/t5702-protocol-v2.sh +77 -77

154 lines not on this page · raw message

t/t5703-upload-pack-ref-in-want.sh +11 -11
index 249137b467..330d049b2c 100755

Lines of this file are not shown. View the raw message.

t/t5705-session-id-in-capabilities.sh +6 -6
index ed38c76c29..ef0db57d1f 100755

Lines of this file are not shown. View the raw message.

t/t5750-bundle-uri-parse.sh +4 -4
index 294f9d9c64..70ae94c4d0 100755

Lines of this file are not shown. View the raw message.

t/t5801-remote-helpers.sh +2 -2
index d21877150e..5fafc857b8 100755

Lines of this file are not shown. View the raw message.

t/t5810-proto-disable-local.sh +1 -1
index 96a2c46e7a..cb3b13f8d3 100755

Lines of this file are not shown. View the raw message.

t/t5813-proto-disable-ssh.sh +2 -2
index 045e2fe6ce..1800d27f89 100755

Lines of this file are not shown. View the raw message.

t/t6000-rev-list-misc.sh +13 -13
index a95ba576fa..ce968f685e 100755

Lines of this file are not shown. View the raw message.

t/t6005-rev-list-count.sh +4 -4
index 6cde997e13..0bef5838df 100755

Lines of this file are not shown. View the raw message.

t/t6006-rev-list-format.sh +2 -2
index 581984467d..d2648e1619 100755

Lines of this file are not shown. View the raw message.

t/t6009-rev-list-parent.sh +2 -2
index 9c9a8459af..fd6ee2eff8 100755

Lines of this file are not shown. View the raw message.

t/t6020-bundle-misc.sh +6 -6
index 500c81b8a1..939d4214f4 100755

Lines of this file are not shown. View the raw message.

t/t6022-rev-list-missing.sh +2 -2
index 08e92dd002..1e472a45af 100755

Lines of this file are not shown. View the raw message.

t/t6030-bisect-porcelain.sh +75 -75

150 lines not on this page · raw message

t/t6040-tracking-info.sh +1 -1
index 0242b5bf7a..ddedce5442 100755

Lines of this file are not shown. View the raw message.

t/t6112-rev-list-filters-objects.sh +12 -12
index e0a825bccf..a1c244c32d 100755

Lines of this file are not shown. View the raw message.

t/t6115-rev-list-du.sh +2 -2
index 04c577dad6..b9650b80a4 100755

Lines of this file are not shown. View the raw message.

t/t6120-describe.sh +7 -7
index 8ee3d2c37d..12b4fb307b 100755

Lines of this file are not shown. View the raw message.

t/t6200-fmt-merge-msg.sh +41 -41

82 lines not on this page · raw message

t/t6402-merge-rename.sh +2 -2
index ff00b74e9c..fb1595ae53 100755

Lines of this file are not shown. View the raw message.

t/t6403-merge-file.sh +3 -3
index 801284cf8f..dcc5c2ac99 100755

Lines of this file are not shown. View the raw message.

t/t6404-recursive-merge.sh +1 -1
index 346f3608b9..ce420fe1ac 100755

Lines of this file are not shown. View the raw message.

t/t6406-merge-attr.sh +10 -10
index 8f6fbef002..7f49c5908a 100755

Lines of this file are not shown. View the raw message.

t/t6417-merge-ours-theirs.sh +15 -15
index 62d1406119..9486251399 100755

Lines of this file are not shown. View the raw message.

t/t6418-merge-text-auto.sh +1 -1
index 41288a60ce..c86a1bedd5 100755

Lines of this file are not shown. View the raw message.

t/t6422-merge-rename-corner-cases.sh +4 -4
index e18d5a227d..d1f1de14e7 100755

Lines of this file are not shown. View the raw message.

t/t6423-merge-rename-directories.sh +36 -36

72 lines not on this page · raw message

t/t6424-merge-unrelated-index-changes.sh +3 -3
index ddc7524f6c..12e58bc335 100755

Lines of this file are not shown. View the raw message.

t/t6427-diff3-conflict-markers.sh +5 -5
index 57569c4f4b..818550bc95 100755

Lines of this file are not shown. View the raw message.

t/t6432-merge-recursive-space-options.sh +2 -2
index db4b77e63d..110b48cb67 100755

Lines of this file are not shown. View the raw message.

t/t6436-merge-overwrite.sh +3 -3
index 70b5d2d694..5e39a15fac 100755

Lines of this file are not shown. View the raw message.

t/t6437-submodule-merge.sh +5 -5
index a564758f52..107e13afbc 100755

Lines of this file are not shown. View the raw message.

t/t6500-gc.sh +4 -4
index ea9aaad470..3ee4b18d88 100755

Lines of this file are not shown. View the raw message.

t/t6600-test-reach.sh +2 -2
index b5b314e570..ee46d66b84 100755

Lines of this file are not shown. View the raw message.

t/t7001-mv.sh +8 -8
index 920479e925..7cf4aa5ba1 100755

Lines of this file are not shown. View the raw message.

t/t7002-mv-sparse-checkout.sh +19 -19
index 4d3f221224..9c0e82ba31 100755

Lines of this file are not shown. View the raw message.

t/t7003-filter-branch.sh +8 -8
index c475769858..86011e7b1f 100755

Lines of this file are not shown. View the raw message.

t/t7004-tag.sh +1 -1
index d918005dd9..2ad30040df 100755

Lines of this file are not shown. View the raw message.

t/t7006-pager.sh +8 -8
index 9717e825f0..75ca7a25da 100755

Lines of this file are not shown. View the raw message.

t/t7012-skip-worktree-writing.sh +3 -3
index cd5c20fe51..526cc4ec83 100755

Lines of this file are not shown. View the raw message.

t/t7030-verify-tag.sh +26 -26

52 lines not on this page · raw message

t/t7031-verify-tag-signed-ssh.sh +23 -23

46 lines not on this page · raw message

t/t7102-reset.sh +1 -1
index 0503a64d3f..298272cb13 100755

Lines of this file are not shown. View the raw message.

t/t7110-reset-merge.sh +20 -20
index 9a335071af..a241ecfc0b 100755

Lines of this file are not shown. View the raw message.

t/t7201-co.sh +3 -3
index 7613b1d2a4..0ddd1ad7aa 100755

Lines of this file are not shown. View the raw message.

t/t7300-clean.sh +1 -1
index 00d4070156..13ad3eec77 100755

Lines of this file are not shown. View the raw message.

t/t7301-clean-interactive.sh +1 -1
index f743e5b8f4..de59ba3724 100755

Lines of this file are not shown. View the raw message.

t/t7400-submodule-basic.sh +16 -16
index 65fcfae93a..eefdecb0bd 100755

Lines of this file are not shown. View the raw message.

t/t7402-submodule-rebase.sh +1 -1
index 06cee3432f..872eb2d3ab 100755

Lines of this file are not shown. View the raw message.

t/t7406-submodule-update.sh +13 -13
index 6abb00876a..9554720152 100755

Lines of this file are not shown. View the raw message.

t/t7416-submodule-dash-url.sh +10 -10
index 3d944a00e0..9f4af88ed5 100755

Lines of this file are not shown. View the raw message.

t/t7417-submodule-path-url.sh +1 -1
index 5e3051da8b..7a6f61585b 100755

Lines of this file are not shown. View the raw message.

t/t7450-bad-git-dotfiles.sh +7 -7
index 8cc86522b2..105c8d6cbf 100755

Lines of this file are not shown. View the raw message.

t/t7501-commit-basic-functionality.sh +8 -8
index a37509f004..7794babe46 100755

Lines of this file are not shown. View the raw message.

t/t7502-commit-porcelain.sh +1 -1
index 05f6da4ad9..62e3970e3f 100755

Lines of this file are not shown. View the raw message.

t/t7507-commit-verbose.sh +3 -3
index acdb6b1455..8e9d8ff3ae 100755

Lines of this file are not shown. View the raw message.

t/t7508-status.sh +3 -3
index c2057bc94c..c37ffbe444 100755

Lines of this file are not shown. View the raw message.

t/t7510-signed-commit.sh +34 -34

68 lines not on this page · raw message

t/t7516-commit-races.sh +2 -2
index de7c4ca790..29669f64ae 100755

Lines of this file are not shown. View the raw message.

t/t7519-status-fsmonitor.sh +7 -7
index 7ee69ecdd4..93973ed25a 100755

Lines of this file are not shown. View the raw message.

t/t7527-builtin-fsmonitor.sh +41 -41

82 lines not on this page · raw message

t/t7528-signed-commit-ssh.sh +34 -34

68 lines not on this page · raw message

t/t7600-merge.sh +5 -5
index f877d9a433..a9c3c3920d 100755

Lines of this file are not shown. View the raw message.

t/t7603-merge-reduce-heads.sh +10 -10
index 1f8c3b7ccb..92007dc501 100755

Lines of this file are not shown. View the raw message.

t/t7606-merge-custom.sh +1 -1
index 81fb7c474c..0ae2f59b1d 100755

Lines of this file are not shown. View the raw message.

t/t7607-merge-state.sh +2 -2
index 89a62ac53b..bfe782d89e 100755

Lines of this file are not shown. View the raw message.

t/t7610-mergetool.sh +9 -9
index 957f8e20ba..0128b14452 100755

Lines of this file are not shown. View the raw message.

t/t7700-repack.sh +7 -7
index c6ff3aed30..f0a390e3c6 100755

Lines of this file are not shown. View the raw message.

t/t7703-repack-geometric.sh +2 -2
index 04d5d8fc33..f3a0650cfe 100755

Lines of this file are not shown. View the raw message.

t/t7704-repack-cruft.sh +6 -6
index 9e03b04315..b342e82447 100755

Lines of this file are not shown. View the raw message.

t/t7800-difftool.sh +13 -13
index 8a91ff3603..02e47f466a 100755

Lines of this file are not shown. View the raw message.

t/t7810-grep.sh +11 -11
index 1b195bee59..d314393fce 100755

Lines of this file are not shown. View the raw message.

t/t7814-grep-recurse-submodules.sh +1 -1
index 167fe66150..e1cf53dc9e 100755

Lines of this file are not shown. View the raw message.

t/t7900-maintenance.sh +17 -17
index d7f82e1bec..8ccaac8b50 100755

Lines of this file are not shown. View the raw message.

t/t8008-blame-formats.sh +1 -1
index c12a4196d6..016a837f14 100755

Lines of this file are not shown. View the raw message.

t/t8010-cat-file-filters.sh +1 -1
index b3be2aa387..a4afb33c3e 100755

Lines of this file are not shown. View the raw message.

t/t8012-blame-colors.sh +1 -1
index 5562eba436..d2653b3c31 100755

Lines of this file are not shown. View the raw message.

t/t9001-send-email.sh +95 -95

190 lines not on this page · raw message

t/t9003-help-autocorrect.sh +3 -3
index 8da318d2b5..87b438fcac 100755

Lines of this file are not shown. View the raw message.

t/t9106-git-svn-commit-diff-clobber.sh +1 -1
index bca496c40e..376100fd7a 100755

Lines of this file are not shown. View the raw message.

t/t9107-git-svn-migrate.sh +15 -15
index aa908bbc2f..6d7d2aa491 100755

Lines of this file are not shown. View the raw message.

t/t9110-git-svn-use-svm-props.sh +10 -10
index ad37d980c9..9dc4a31a44 100755

Lines of this file are not shown. View the raw message.

t/t9111-git-svn-use-svnsync-props.sh +9 -9
index 6c93073551..e45d81bfc4 100755

Lines of this file are not shown. View the raw message.

t/t9114-git-svn-dcommit-merge.sh +3 -3
index e06538b1c8..2240b58d4d 100755

Lines of this file are not shown. View the raw message.

t/t9116-git-svn-log.sh +4 -4
index d74d7b2de6..b2e1248651 100755

Lines of this file are not shown. View the raw message.

t/t9117-git-svn-init-clone.sh +6 -6
index 3b038c338f..b2815b6894 100755

Lines of this file are not shown. View the raw message.

t/t9119-git-svn-info.sh +8 -8
index 088d1c57a8..0dbb2e4a97 100755

Lines of this file are not shown. View the raw message.

t/t9122-git-svn-author.sh +4 -4
index 0fc289ae0f..21401ad2b1 100755

Lines of this file are not shown. View the raw message.

t/t9130-git-svn-authors-file.sh +4 -4
index 90325db909..67a4d889fd 100755

Lines of this file are not shown. View the raw message.

t/t9138-git-svn-authors-prog.sh +7 -7
index 5bb38cb23a..5fdf324111 100755

Lines of this file are not shown. View the raw message.

t/t9140-git-svn-reset.sh +2 -2
index a420b2a87a..c70a3b36a4 100755

Lines of this file are not shown. View the raw message.

t/t9153-git-svn-rewrite-uuid.sh +2 -2
index 8cb2b5c69c..2387c7d4f2 100755

Lines of this file are not shown. View the raw message.

t/t9200-git-cvsexportcommit.sh +1 -1
index 581cf3d28f..8e0272df20 100755

Lines of this file are not shown. View the raw message.

t/t9210-scalar.sh +17 -17
index f2a6df77ce..c8463d0ac4 100755

Lines of this file are not shown. View the raw message.

t/t9211-scalar-clone.sh +8 -8
index 2043f48a1a..002d6ecdc1 100755

Lines of this file are not shown. View the raw message.

t/t9300-fast-import.sh +5 -5
index 479437760b..fe6c2617ac 100755

Lines of this file are not shown. View the raw message.

t/t9350-fast-export.sh +27 -27

54 lines not on this page · raw message

t/t9351-fast-export-anonymize.sh +18 -18
index 156a647484..ed784a56b0 100755

Lines of this file are not shown. View the raw message.

t/t9400-git-cvsserver-server.sh +2 -2
index 4b45398bab..b2379e4671 100755

Lines of this file are not shown. View the raw message.

t/t9501-gitweb-standalone-http-status.sh +29 -29

58 lines not on this page · raw message

t/t9502-gitweb-standalone-parse-output.sh +19 -19
index 81d5625557..9587ce212a 100755

Lines of this file are not shown. View the raw message.

t/t9800-git-p4-basic.sh +5 -5
index 0816763e46..140a3a7f24 100755

Lines of this file are not shown. View the raw message.

t/t9801-git-p4-branch.sh +24 -24

48 lines not on this page · raw message

t/t9806-git-p4-options.sh +5 -5
index c26d297433..dce710ea56 100755

Lines of this file are not shown. View the raw message.

t/t9807-git-p4-submit.sh +1 -1
index 6ae7ced51b..adbdf9323a 100755

Lines of this file are not shown. View the raw message.

t/t9810-git-p4-rcs.sh +4 -4
index 5fe83315ec..70355a32a3 100755

Lines of this file are not shown. View the raw message.

t/t9813-git-p4-preserve-users.sh +4 -4
index fd018c87a8..838ddd324c 100755

Lines of this file are not shown. View the raw message.

t/t9814-git-p4-rename.sh +4 -4
index 2a9838f37f..6c5ecb9bea 100755

Lines of this file are not shown. View the raw message.

t/t9827-git-p4-change-filetype.sh +2 -2
index d3670bd7a2..9a8231a788 100755

Lines of this file are not shown. View the raw message.

t/t9832-unshelve.sh +3 -3
index 6b3cb0414a..f85a60a184 100755

Lines of this file are not shown. View the raw message.

t/t9833-errors.sh +2 -2
index e22369ccdf..b312669ef2 100755

Lines of this file are not shown. View the raw message.

t/t9835-git-p4-metadata-encoding-python2.sh +18 -18
index b969c7e0d5..7eb0c8ecd1 100755

Lines of this file are not shown. View the raw message.

t/t9836-git-p4-metadata-encoding-python3.sh +19 -19
index da6669bf71..16118495cd 100755

Lines of this file are not shown. View the raw message.

t/t9850-shell.sh +1 -1
index 21c3af48bd..27cc49bbd1 100755

Lines of this file are not shown. View the raw message.

t/t9902-completion.sh +13 -13
index 28f61f08fb..a50639ee89 100755

Lines of this file are not shown. View the raw message.

The rest of this diff is not shown. View the raw message.

Signature · 1 line
gitgitgadget

Part of the diff is not shown here. View the raw message.

#17 lore raw

Re: [PATCH v2 5/6] t: convert grep assertions to test_grep

Quoted 8 lines · Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Replace bare grep with test_grep in test assertions across the suite, including sourced test helpers (lib-*.sh, *-tests.sh). test_grep prints the contents of the file being searched on failure, making debugging easier than a bare grep which fails silently.

Quoted 5 lines

Only assertion-style greps are converted: grep used as a filter in pipelines, command substitutions, conditionals, or with redirected I/O is left as-is with a "# lint-ok" annotation. Existing '! test_grep' calls are rewritten to 'test_grep !' so that the diagnostic output is preserved on failure.

Thanks for taking the effort for cleaning up all those negated '! grep' and '! test_grep' callsites and turning them into 'test_grep !'.

Quoted 9 lines

The conversion was generated using a grep-assertion linter (greplint.pl, added in the following commit) to identify bare grep calls at command position. To reproduce:

    # Step 1: mark bare greps that should not be converted
    sed -i '/! grep "$m" \.git\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \
        t/t1400-update-ref.sh
    sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \
        t/t3420-rebase-autostash.sh

I think in this case checking the file3's contents is wrong, because at this point file3 should not exist in the first place. I've sent a patch to fix this long ago, but apparently didn't manage to follow through back then.

https://lore.kernel.org/git/[email protected]/

Quoted 95 lines
t/t3420-rebase-autostash.sh +13 -13
index f0bbc476ff..f6cb3dd72e 100755
@@ -141,8 +141,8 @@ testrebase () {
 		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 @@ testrebase () {
 		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 @@ testrebase () {
 		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 @@ testrebase () {
 		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 @@ testrebase () {
 		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
 	'
 
@@ -244,7 +244,7 @@ testrebase () {
 		git rebase --quit &&
 		test_when_finished git stash drop &&
 		test_path_is_missing $dotest/autostash &&
-		! grep dirty file3 &&
+		! grep dirty file3 && # lint-ok: file may not exist after --quit
 		git stash show -p >actual &&
 		test_cmp expect actual &&
 		git reset --hard &&
@@ -260,11 +260,11 @@ testrebase () {
 		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_expect_success "rebase: fast-forward rebase" '
 	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_expect_success "rebase: noop rebase" '
 	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
 '
 
#18 lore raw

Re: [PATCH v2 5/6] t: convert grep assertions to test_grep

Quoted 10 lines · SZEDER Gábor
Quoted 9 lines

The conversion was generated using a grep-assertion linter (greplint.pl, added in the following commit) to identify bare grep calls at command position. To reproduce:

    # Step 1: mark bare greps that should not be converted
    sed -i '/! grep "$m" \.git\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \
        t/t1400-update-ref.sh
    sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \
        t/t3420-rebase-autostash.sh
Quoted 6 lines

I think in this case checking the file3's contents is wrong, because at this point file3 should not exist in the first place. I've sent a patch to fix this long ago, but apparently didn't manage to follow through back then.

https://lore.kernel.org/git/[email protected]/

Thanks. I guess the test_grep can be extended to catch this case, where

test_grep ! -e pattern1 -e pattern2 file

does not find any hits, but only because 'file' is missing, as an error, just like "test_must_fail git foo" that segfaults is flagged as "yes, it fails but that is not the kind of failure we expect".

#19 lore raw

Re: [PATCH v2 5/6] t: convert grep assertions to test_grep

Quoted 9 lines · Junio C Hamano
Quoted 6 lines · SZEDER Gábor

I think in this case checking the file3's contents is wrong, because at this point file3 should not exist in the first place. I've sent a patch to fix this long ago, but apparently didn't manage to follow through back then.

https://lore.kernel.org/git/[email protected]/

Quoted 7 lines

Thanks. I guess the test_grep can be extended to catch this case, where

test_grep ! -e pattern1 -e pattern2 file

does not find any hits, but only because 'file' is missing, as an error, ...

Wait. The necessary check is already there, isn't it?

        test_grep () {
                eval "last_arg=\${$#}"

                test -f "$last_arg" ||
                BUG "test_grep requires a file to read as the last parameter"

So why don't we see it every time we run that test that inspects file3's contents with Michael's series merged in? Puzzled...

#20 lore raw

Re: [PATCH v2 5/6] t: convert grep assertions to test_grep

Quoted 21 lines · Junio C Hamano
Quoted 9 lines · Junio C Hamano
Quoted 6 lines · SZEDER Gábor

I think in this case checking the file3's contents is wrong, because at this point file3 should not exist in the first place. I've sent a patch to fix this long ago, but apparently didn't manage to follow through back then.

https://lore.kernel.org/git/[email protected]/

Quoted 7 lines

Thanks. I guess the test_grep can be extended to catch this case, where

test_grep ! -e pattern1 -e pattern2 file

does not find any hits, but only because 'file' is missing, as an error, ...

Wait. The necessary check is already there, isn't it?

Quoted 8 lines
        test_grep () {
                eval "last_arg=\${$#}"

                test -f "$last_arg" ||
                BUG "test_grep requires a file to read as the last parameter"

So why don't we see it every time we run that test that inspects file3's contents with Michael's series merged in? Puzzled...

Ah, of course. Michael sidesteps this mechanism by not using "test_grep !", with

       ! grep dirty file3 && # lint-ok: file may not exist after --quit

and if we realize that "may not exist" is actually "never exists", then your other patch from 5 years ago would become the most sensible fix for this line.

It may not be a bad idea to go through "# lint-ok:" introduced by Michael's series with finer toothed comb (there are only a handful of them) and see if there are similar "look, the file we are grepping in never exists with correctly running Git" gotchas.

Thanks.

#21 lore raw

Re: [PATCH v2 5/6] t: convert grep assertions to test_grep

Quoted 5 lines · Junio C Hamano

Ah, of course. Michael sidesteps this mechanism by not using "test_grep !", with

       ! grep dirty file3 && # lint-ok: file may not exist after --quit
Quoted 8 lines

and if we realize that "may not exist" is actually "never exists", then your other patch from 5 years ago would become the most sensible fix for this line.

It may not be a bad idea to go through "# lint-ok:" introduced by Michael's series with finer toothed comb (there are only a handful of them) and see if there are similar "look, the file we are grepping in never exists with correctly running Git" gotchas.

In any case, I think SZEDER's fix to stop grepping in the file but instead insisting on its absense does make sense and it is now in 'next'. So perhaps this topic can have a small and final reroll v3 that omits change to this particular line (and possibly fix other lines that punts with "# lint-ok" if needed) and we can declare victory after that?

Thanks, all.

#22 lore raw

Re: [PATCH v2 5/6] t: convert grep assertions to test_grep

Quoted 6 lines · Junio C Hamano
Quoted 5 lines

It may not be a bad idea to go through "# lint-ok:" introduced by Michael's series with finer toothed comb (there are only a handful of them) and see if there are similar "look, the file we are grepping in never exists with correctly running Git" gotchas.

Quoted 8 lines

In any case, I think SZEDER's fix to stop grepping in the file but instead insisting on its absense does make sense and it is now in 'next'. So perhaps this topic can have a small and final reroll v3 that omits change to this particular line (and possibly fix other lines that punts with "# lint-ok" if needed) and we can declare victory after that?

Thanks, all.

Thank you, SZEDER, for the nice catch.

I will apply the suggested fix to the series locally, and go through the other #lint-ok's with a fine toothed comb as Junio suggests.

Appreciate the eyes on the series, will send a reroll soon.

#23 lore raw

PATCH v2 6/6 t: add greplint to detect bare grep assertions

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Without a lint guard, bare grep assertions will creep back into tests over time, defeating the previous commit's conversion.

Add greplint.pl to catch bare 'grep' used as a test assertion (where 'test_grep' should be used) and '! test_grep' (where 'test_grep !' should be used).

greplint.pl reuses the shared shell parser from lib-shell-parser.pl to tokenize test bodies. The Lexer collapses heredocs, command substitutions, and quoted strings into single tokens, so 'grep' appearing inside these contexts is not flagged. A flat walk over the token stream tracks command position and pipeline state to distinguish assertion greps from filter greps.

For double-quoted test bodies, a source-line walk counts backslash-continuation lines that the Lexer consumes without emitting into the body text, adjusting the reported line number accordingly.

Add test fixtures in greplint/ (modeled on chainlint/) covering detection of bare grep assertions, correct skipping of filters, pipelines, redirects, command substitutions, and lint-ok annotations.

Wire into the Makefile as:
- test-greplint: runs greplint.pl on $(T) $(THELPERS) $(TPERF)
- check-greplint: runs greplint.pl on fixtures, diffs against expected
- clean-greplint: removes temp dir

Add eol=lf entries in t/.gitattributes for greplint fixtures, matching chainlint, so that check-greplint passes on Windows where core.autocrlf would otherwise cause CRLF mismatches between expected and actual output.

---
 t/.gitattributes                              |   2 +
 t/Makefile                                    |  29 ++-
 t/greplint-cat.pl                             |  27 ++
 t/greplint.pl                                 | 241 ++++++++++++++++++
 t/greplint/bare-grep-after-and.expect         |   1 +
 t/greplint/bare-grep-after-and.test           |   4 +
 t/greplint/bare-grep-after-semicolon.expect   |   1 +
 t/greplint/bare-grep-after-semicolon.test     |   4 +
 t/greplint/bare-grep-compound-body.expect     |   3 +
 t/greplint/bare-grep-compound-body.test       |  17 ++
 t/greplint/bare-grep-count-mode.expect        |   1 +
 t/greplint/bare-grep-count-mode.test          |   3 +
 t/greplint/bare-grep-explicit-pattern.expect  |   1 +
 t/greplint/bare-grep-explicit-pattern.test    |   3 +
 t/greplint/bare-grep-flags.expect             |   1 +
 t/greplint/bare-grep-flags.test               |   3 +
 t/greplint/bare-grep-lint-ok.expect           |   0
 t/greplint/bare-grep-lint-ok.test             |   4 +
 t/greplint/bare-grep-negated.expect           |   1 +
 t/greplint/bare-grep-negated.test             |   3 +
 t/greplint/bare-grep-pattern-file.expect      |   1 +
 t/greplint/bare-grep-pattern-file.test        |   3 +
 t/greplint/bare-grep-simple.expect            |   1 +
 t/greplint/bare-grep-simple.test              |   3 +
 t/greplint/bare-grep-subshell.expect          |   1 +
 t/greplint/bare-grep-subshell.test            |   5 +
 .../dqstring-continuation-offset.expect       |   1 +
 t/greplint/dqstring-continuation-offset.test  |  11 +
 t/greplint/filter-command-substitution.expect |   0
 t/greplint/filter-command-substitution.test   |   3 +
 t/greplint/filter-pipe-input.expect           |   0
 t/greplint/filter-pipe-input.test             |   3 +
 t/greplint/filter-pipe-output.expect          |   0
 t/greplint/filter-pipe-output.test            |   3 +
 t/greplint/filter-redirect-output.expect      |   0
 t/greplint/filter-redirect-output.test        |   3 +
 t/greplint/filter-stdin-redirect.expect       |   0
 t/greplint/filter-stdin-redirect.test         |   3 +
 t/greplint/grep-as-argument.expect            |   0
 t/greplint/grep-as-argument.test              |   3 +
 t/greplint/grep-as-value.expect               |   0
 t/greplint/grep-as-value.test                 |   6 +
 t/greplint/wrong-negation.expect              |   1 +
 t/greplint/wrong-negation.test                |   3 +
 44 files changed, 398 insertions(+), 5 deletions(-)
 create mode 100644 t/greplint-cat.pl
 create mode 100644 t/greplint.pl
 create mode 100644 t/greplint/bare-grep-after-and.expect
 create mode 100644 t/greplint/bare-grep-after-and.test
 create mode 100644 t/greplint/bare-grep-after-semicolon.expect
 create mode 100644 t/greplint/bare-grep-after-semicolon.test
 create mode 100644 t/greplint/bare-grep-compound-body.expect
 create mode 100644 t/greplint/bare-grep-compound-body.test
 create mode 100644 t/greplint/bare-grep-count-mode.expect
 create mode 100644 t/greplint/bare-grep-count-mode.test
 create mode 100644 t/greplint/bare-grep-explicit-pattern.expect
 create mode 100644 t/greplint/bare-grep-explicit-pattern.test
 create mode 100644 t/greplint/bare-grep-flags.expect
 create mode 100644 t/greplint/bare-grep-flags.test
 create mode 100644 t/greplint/bare-grep-lint-ok.expect
 create mode 100644 t/greplint/bare-grep-lint-ok.test
 create mode 100644 t/greplint/bare-grep-negated.expect
 create mode 100644 t/greplint/bare-grep-negated.test
 create mode 100644 t/greplint/bare-grep-pattern-file.expect
 create mode 100644 t/greplint/bare-grep-pattern-file.test
 create mode 100644 t/greplint/bare-grep-simple.expect
 create mode 100644 t/greplint/bare-grep-simple.test
 create mode 100644 t/greplint/bare-grep-subshell.expect
 create mode 100644 t/greplint/bare-grep-subshell.test
 create mode 100644 t/greplint/dqstring-continuation-offset.expect
 create mode 100644 t/greplint/dqstring-continuation-offset.test
 create mode 100644 t/greplint/filter-command-substitution.expect
 create mode 100644 t/greplint/filter-command-substitution.test
 create mode 100644 t/greplint/filter-pipe-input.expect
 create mode 100644 t/greplint/filter-pipe-input.test
 create mode 100644 t/greplint/filter-pipe-output.expect
 create mode 100644 t/greplint/filter-pipe-output.test
 create mode 100644 t/greplint/filter-redirect-output.expect
 create mode 100644 t/greplint/filter-redirect-output.test
 create mode 100644 t/greplint/filter-stdin-redirect.expect
 create mode 100644 t/greplint/filter-stdin-redirect.test
 create mode 100644 t/greplint/grep-as-argument.expect
 create mode 100644 t/greplint/grep-as-argument.test
 create mode 100644 t/greplint/grep-as-value.expect
 create mode 100644 t/greplint/grep-as-value.test
 create mode 100644 t/greplint/wrong-negation.expect
 create mode 100644 t/greplint/wrong-negation.test
t/.gitattributes +2 -0
index 7664c6e027..e867f38c71 100644
@@ -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
t/Makefile +24 -5
index ab8a5b54aa..e3249879c4 100644
@@ -27,9 +27,11 @@ TEST_LINT ?= test-lint
 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 @@ TEST_SHELL_PATH_SQ = $(subst ','\'',$(TEST_SHELL_PATH))
 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 @@ TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh
 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 @@ test: pre-clean check-meson $(TEST_LINT)
 	$(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 @@ unit-tests-test-tool:
 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 @@ check-chainlint:
 	{ $(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 @@ check-meson:
 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-executable:
 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 @@ perf:
 	$(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:
t/greplint-cat.pl new file +27 -0
new file mode 100644
index 0000000000..fa9c3b8aab
@@ -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;
t/greplint.pl new file +241 -0

241 lines not on this page · raw message

t/greplint/bare-grep-after-and.expect new file +1 -0
new file mode 100644
index 0000000000..7da1b21aa8
@@ -0,0 +1 @@
+3: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-after-and.test new file +4 -0
new file mode 100644
index 0000000000..c5b26e4572
@@ -0,0 +1,4 @@
+test_expect_success 'grep after && is flagged' '
+	cmd &&
+	grep pattern file
+'
t/greplint/bare-grep-after-semicolon.expect new file +1 -0
new file mode 100644
index 0000000000..7da1b21aa8
@@ -0,0 +1 @@
+3: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-after-semicolon.test new file +4 -0
new file mode 100644
index 0000000000..c1e468ddf8
@@ -0,0 +1,4 @@
+test_expect_success 'grep after semicolon is flagged' '
+	echo hello;
+	grep pattern file
+'
t/greplint/bare-grep-compound-body.expect new file +3 -0
new file mode 100644
index 0000000000..eb6210f0c9
@@ -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)
t/greplint/bare-grep-compound-body.test new file +17 -0
new file mode 100644
index 0000000000..72472edcf6
@@ -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
+'
t/greplint/bare-grep-count-mode.expect new file +1 -0
new file mode 100644
index 0000000000..8922d35b42
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-count-mode.test new file +3 -0
new file mode 100644
index 0000000000..b9c0f18783
@@ -0,0 +1,3 @@
+test_expect_success 'grep -c is flagged (not special-cased)' '
+	grep -c pattern file
+'
t/greplint/bare-grep-explicit-pattern.expect new file +1 -0
new file mode 100644
index 0000000000..8922d35b42
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-explicit-pattern.test new file +3 -0
new file mode 100644
index 0000000000..69d8651019
@@ -0,0 +1,3 @@
+test_expect_success 'grep -e is flagged' '
+	grep -e pattern file
+'
t/greplint/bare-grep-flags.expect new file +1 -0
new file mode 100644
index 0000000000..8922d35b42
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-flags.test new file +3 -0
new file mode 100644
index 0000000000..9ca0e10233
@@ -0,0 +1,3 @@
+test_expect_success 'grep -E is flagged' '
+	grep -E "pat+ern" file
+'
t/greplint/bare-grep-lint-ok.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/bare-grep-lint-ok.test new file +4 -0
new file mode 100644
index 0000000000..335b902a42
@@ -0,0 +1,4 @@
+test_expect_success 'grep with lint-ok annotation is not flagged' '
+	grep pattern file && # lint-ok
+	echo done
+'
t/greplint/bare-grep-negated.expect new file +1 -0
new file mode 100644
index 0000000000..8922d35b42
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-negated.test new file +3 -0
new file mode 100644
index 0000000000..ef789bc8b7
@@ -0,0 +1,3 @@
+test_expect_success 'negated grep is flagged' '
+	! grep pattern file
+'
t/greplint/bare-grep-pattern-file.expect new file +1 -0
new file mode 100644
index 0000000000..8922d35b42
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-pattern-file.test new file +3 -0
new file mode 100644
index 0000000000..d70035e449
@@ -0,0 +1,3 @@
+test_expect_success 'grep -f is flagged' '
+	grep -f patterns.txt file
+'
t/greplint/bare-grep-simple.expect new file +1 -0
new file mode 100644
index 0000000000..8922d35b42
@@ -0,0 +1 @@
+2: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-simple.test new file +3 -0
new file mode 100644
index 0000000000..3a8d5f9bbd
@@ -0,0 +1,3 @@
+test_expect_success 'bare grep is flagged' '
+	grep pattern file
+'
t/greplint/bare-grep-subshell.expect new file +1 -0
new file mode 100644
index 0000000000..7da1b21aa8
@@ -0,0 +1 @@
+3: error: bare grep outside pipeline (use test_grep)
t/greplint/bare-grep-subshell.test new file +5 -0
new file mode 100644
index 0000000000..1fab2d3363
@@ -0,0 +1,5 @@
+test_expect_success 'grep in subshell is flagged' '
+	(
+		grep pattern file
+	)
+'
t/greplint/dqstring-continuation-offset.expect new file +1 -0
new file mode 100644
index 0000000000..8185bfdca4
@@ -0,0 +1 @@
+10: error: bare grep outside pipeline (use test_grep)
t/greplint/dqstring-continuation-offset.test new file +11 -0
new file mode 100644
index 0000000000..b332bd8f3b
@@ -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
+"
t/greplint/filter-command-substitution.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/filter-command-substitution.test new file +3 -0
new file mode 100644
index 0000000000..bd9a100a5d
@@ -0,0 +1,3 @@
+test_expect_success 'grep in command substitution is not flagged' '
+	x=$(grep pattern file)
+'
t/greplint/filter-pipe-input.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/filter-pipe-input.test new file +3 -0
new file mode 100644
index 0000000000..cb5c0d9e23
@@ -0,0 +1,3 @@
+test_expect_success 'grep receiving pipe input is not flagged' '
+	cmd | grep pattern
+'
t/greplint/filter-pipe-output.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/filter-pipe-output.test new file +3 -0
new file mode 100644
index 0000000000..01340086d7
@@ -0,0 +1,3 @@
+test_expect_success 'grep piping to another command is not flagged' '
+	grep pattern file | wc -l
+'
t/greplint/filter-redirect-output.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/filter-redirect-output.test new file +3 -0
new file mode 100644
index 0000000000..ee70ba7cde
@@ -0,0 +1,3 @@
+test_expect_success 'grep with output redirect is not flagged' '
+	grep pattern file >output
+'
t/greplint/filter-stdin-redirect.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/filter-stdin-redirect.test new file +3 -0
new file mode 100644
index 0000000000..292db7766e
@@ -0,0 +1,3 @@
+test_expect_success 'grep reading from stdin redirect is not flagged' '
+	grep pattern <input
+'
t/greplint/grep-as-argument.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/grep-as-argument.test new file +3 -0
new file mode 100644
index 0000000000..7157228b60
@@ -0,0 +1,3 @@
+test_expect_success 'grep as argument to another command is not flagged' '
+	test_must_fail grep pattern file
+'
t/greplint/grep-as-value.expect new file +0 -0
new file mode 100644
index 0000000000..e69de29bb2
t/greplint/grep-as-value.test new file +6 -0
new file mode 100644
index 0000000000..92926f6378
@@ -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
+'
t/greplint/wrong-negation.expect new file +1 -0
new file mode 100644
index 0000000000..7dc52f65a8
@@ -0,0 +1 @@
+2: error: use "test_grep !" instead of "! test_grep"
t/greplint/wrong-negation.test new file +3 -0
new file mode 100644
index 0000000000..542fbd9b28
@@ -0,0 +1,3 @@
+test_expect_success 'wrong negation of test_grep is flagged' '
+	! test_grep pattern file
+'
Signature · 1 line
gitgitgadget
#24 lore raw

v3 cover t: add greplint.pl and convert grep to test_grep

In reply to Michael Montalbo via GitGitGadget

test_grep is a wrapper around grep for test assertions that prints the file contents on failure for easier debugging. Bare grep fails silently, making it hard to diagnose what went wrong.

This series converts existing bare grep assertions to test_grep and adds greplint.pl to prevent new ones from being introduced.

Patch 1 documents test_grep in t/README.

Patch 2 fixes three greps missing file arguments (t2402, t7507, t7700). They were reading empty stdin and passing vacuously.

Patch 3 extracts chainlint's Lexer, ShellParser, and ScriptParser into a shared module (lib-shell-parser.pl) so greplint.pl can reuse the same tokenizer. No functional change to chainlint.

Patch 4 fixes a latent line-counting bug in scan_dqstring where newlines from $() bodies inside double-quoted strings were counted twice. This does not affect chainlint (which uses byte offsets) but matters for greplint.pl's line-number reporting.

Patch 5 converts existing assertion greps to test_grep, including sourced test helpers. Greps used as data filters or on files that may not exist are left unconverted with lint-ok annotations.

Patch 6 adds greplint.pl with test fixtures (modeled on chainlint/) and wires it into the Makefile as test-greplint and check-greplint.

Changes since v2:

* t3420-rebase-autostash: dropped the change to the '! grep dirty file3' line under 'rebase --quit'. As SZEDER pointed out, file3 never exists in the conflicted state, so that grep was passing only because it could not open the file. SZEDER's fix (sg/t3420-do-not-grep-in-missing-file, now in 'next') replaces the line with 'test_path_is_missing file3', which is the right check; this series simply leaves that line to his fix.

* Audited the remaining '# lint-ok' annotations for the same "grep a file that never exists with correctly running Git" gotcha, as Junio suggested. The rule the audit applies: 'grep' becomes 'test_grep' only where its exit code is the assertion; grep that produces data (a filter) or that reads a file whose presence is conditional stays a plain 'grep', because test_grep BUGs on a missing file.

* t5537 (.git/shallow): the file is still present after the repack (the client stays shallow), so the assertion is converted to 'test_grep !' like any other; the "may not exist" note was wrong.

* t1400 (.git/packed-refs): the file exists only with the files backend. Guarded the packed-refs check with a REFFILES prerequisite; the backend-agnostic 'git show-ref' check that follows still runs under every backend.

* t7450 (squatting-clone/d/a/git~2): kept as '! grep' with an improved '# lint-ok'. 'git~2' is the NTFS 8.3 short name of a planted '..git' decoy and only exists when 8.3 short-name generation is enabled. Verified on a Windows VM: with 8.3 disabled (the modern default) the short name is absent, the '! grep' correctly tolerates it, and a plain test_grep would BUG. So this one deliberately stays a missing-file-tolerant grep.

* t5326 and t5702 remain annotated: these are genuine data filters (grep produces data that is redirected/captured, not an assertion).

* Rebased onto master. The only conflict was a one-line codespell typo fix that upstream applied to chainlint.pl in the meantime; it now lives in the extracted lib-shell-parser.pl.

Known limitation / follow-up:

Assertions like grep pattern file >/dev/null and grep pattern <file are not converted because greplint.pl treats any redirect as a filter, so it does not flag them. The former could be converted, but test_grep prints the matching line on success just as grep does, so the >/dev/null would have to be kept or dropped as a judgment call. The latter requires turning the <file redirect into a positional file argument, since test_grep reads a named file rather than stdin. Both are left as bare grep. A follow-up series can address these once a convention is established.

Michael Montalbo (6):
t/README: document test_grep helper
t: fix grep assertions missing file arguments
t: extract chainlint's parser into shared module
t: fix Lexer line count for $() inside double-quoted strings
t: convert grep assertions to test_grep
t: add greplint to detect bare grep assertions

 t/.gitattributes                              |   2 +
 t/Makefile                                    |  29 +-
 t/README                                      |  26 +
 t/chainlint.pl                                | 529 +----------------
 t/for-each-ref-tests.sh                       |  12 +-
 t/greplint-cat.pl                             |  27 +
 t/greplint.pl                                 | 241 ++++++++
 t/greplint/bare-grep-after-and.expect         |   1 +
 t/greplint/bare-grep-after-and.test           |   4 +
 t/greplint/bare-grep-after-semicolon.expect   |   1 +
 t/greplint/bare-grep-after-semicolon.test     |   4 +
 t/greplint/bare-grep-compound-body.expect     |   3 +
 t/greplint/bare-grep-compound-body.test       |  17 +
 t/greplint/bare-grep-count-mode.expect        |   1 +
 t/greplint/bare-grep-count-mode.test          |   3 +
 t/greplint/bare-grep-explicit-pattern.expect  |   1 +
 t/greplint/bare-grep-explicit-pattern.test    |   3 +
 t/greplint/bare-grep-flags.expect             |   1 +
 t/greplint/bare-grep-flags.test               |   3 +
 t/greplint/bare-grep-lint-ok.expect           |   0
 t/greplint/bare-grep-lint-ok.test             |   4 +
 t/greplint/bare-grep-negated.expect           |   1 +
 t/greplint/bare-grep-negated.test             |   3 +
 t/greplint/bare-grep-pattern-file.expect      |   1 +
 t/greplint/bare-grep-pattern-file.test        |   3 +
 t/greplint/bare-grep-simple.expect            |   1 +
 t/greplint/bare-grep-simple.test              |   3 +
 t/greplint/bare-grep-subshell.expect          |   1 +
 t/greplint/bare-grep-subshell.test            |   5 +
 .../dqstring-continuation-offset.expect       |   1 +
 t/greplint/dqstring-continuation-offset.test  |  11 +
 t/greplint/filter-command-substitution.expect |   0
 t/greplint/filter-command-substitution.test   |   3 +
 t/greplint/filter-pipe-input.expect           |   0
 t/greplint/filter-pipe-input.test             |   3 +
 t/greplint/filter-pipe-output.expect          |   0
 t/greplint/filter-pipe-output.test            |   3 +
 t/greplint/filter-redirect-output.expect      |   0
 t/greplint/filter-redirect-output.test        |   3 +
 t/greplint/filter-stdin-redirect.expect       |   0
 t/greplint/filter-stdin-redirect.test         |   3 +
 t/greplint/grep-as-argument.expect            |   0
 t/greplint/grep-as-argument.test              |   3 +
 t/greplint/grep-as-value.expect               |   0
 t/greplint/grep-as-value.test                 |   6 +
 t/greplint/wrong-negation.expect              |   1 +
 t/greplint/wrong-negation.test                |   3 +
 t/lib-bitmap.sh                               |  12 +-
 t/lib-bundle-uri-protocol.sh                  |  26 +-
 t/lib-httpd.sh                                |   2 +-
 t/lib-shell-parser.pl                         | 534 ++++++++++++++++++
 t/pack-refs-tests.sh                          |   2 +-
 t/show-ref-exists-tests.sh                    |   2 +-
 t/t0000-basic.sh                              |  16 +-
 t/t0001-init.sh                               |  18 +-
 t/t0008-ignores.sh                            |   8 +-
 t/t0009-git-dir-validation.sh                 |   6 +-
 t/t0012-help.sh                               |   4 +-
 t/t0013-sha1dc.sh                             |   2 +-
 t/t0017-env-helper.sh                         |   4 +-
 t/t0021-conversion.sh                         |  18 +-
 t/t0029-core-unsetenvvars.sh                  |   4 +-
 t/t0030-stripspace.sh                         |   4 +-
 t/t0031-lockfile-pid.sh                       |   2 +-
 t/t0040-parse-options.sh                      |  52 +-
 t/t0041-usage.sh                              |   2 +-
 t/t0052-simple-ipc.sh                         |  10 +-
 t/t0061-run-command.sh                        |   2 +-
 t/t0066-dir-iterator.sh                       |   2 +-
 t/t0068-for-each-repo.sh                      |  16 +-
 t/t0070-fundamental.sh                        |   6 +-
 t/t0081-find-pack.sh                          |  12 +-
 t/t0091-bugreport.sh                          |  18 +-
 t/t0092-diagnose.sh                           |  12 +-
 t/t0100-previous.sh                           |   2 +-
 t/t0200-gettext-basic.sh                      |  14 +-
 t/t0203-gettext-setlocale-sanity.sh           |   4 +-
 t/t0204-gettext-reencode-sanity.sh            |   8 +-
 t/t0210-trace2-normal.sh                      |   6 +-
 t/t0211-trace2-perf.sh                        |  80 +--
 t/t0212-trace2-event.sh                       |   8 +-
 t/t0300-credentials.sh                        |   4 +-
 t/t0410-partial-clone.sh                      |  82 +--
 t/t0450-txt-doc-vs-help.sh                    |   2 +-
 t/t0500-progress-display.sh                   |  18 +-
 t/t0610-reftable-basics.sh                    |   8 +-
 t/t1004-read-tree-m-u-wf.sh                   |   8 +-
 t/t1006-cat-file.sh                           |  18 +-
 t/t1007-hash-object.sh                        |   8 +-
 t/t1011-read-tree-sparse-checkout.sh          |  10 +-
 t/t1050-large.sh                              |   6 +-
 t/t1091-sparse-checkout-builtin.sh            |  24 +-
 t/t1092-sparse-checkout-compatibility.sh      |  44 +-
 t/t1300-config.sh                             |  16 +-
 t/t1305-config-include.sh                     |   2 +-
 t/t1308-config-set.sh                         |   6 +-
 t/t1400-update-ref.sh                         | 173 +++---
 t/t1403-show-ref.sh                           |  18 +-
 t/t1410-reflog.sh                             |   4 +-
 t/t1415-worktree-refs.sh                      |   4 +-
 t/t1430-bad-ref-name.sh                       |  56 +-
 t/t1450-fsck.sh                               |  12 +-
 t/t1451-fsck-buffer.sh                        |   6 +-
 t/t1460-refs-migrate.sh                       |   2 +-
 t/t1500-rev-parse.sh                          |   6 +-
 t/t1502-rev-parse-parseopt.sh                 |   2 +-
 t/t1503-rev-parse-verify.sh                   |  10 +-
 t/t1510-repo-setup.sh                         |  10 +-
 t/t1512-rev-parse-disambiguation.sh           |   4 +-
 t/t1515-rev-parse-outside-repo.sh             |   2 +-
 t/t1800-hook.sh                               |  18 +-
 t/t2004-checkout-cache-temp.sh                |   4 +-
 t/t2019-checkout-ambiguous-ref.sh             |   4 +-
 t/t2024-checkout-dwim.sh                      |   8 +-
 t/t2030-unresolve-info.sh                     |   6 +-
 t/t2060-switch.sh                             |   6 +-
 t/t2070-restore.sh                            |   2 +-
 t/t2080-parallel-checkout-basics.sh           |  14 +-
 t/t2081-parallel-checkout-collisions.sh       |  24 +-
 t/t2082-parallel-checkout-attributes.sh       |  12 +-
 t/t2103-update-index-ignore-missing.sh        |   6 +-
 t/t2200-add-update.sh                         |   2 +-
 t/t2203-add-intent.sh                         |   6 +-
 t/t2400-worktree-add.sh                       |  24 +-
 t/t2402-worktree-list.sh                      |  16 +-
 t/t2403-worktree-move.sh                      |   6 +-
 t/t2405-worktree-submodule.sh                 |   6 +-
 t/t2407-worktree-heads.sh                     |  26 +-
 t/t2500-untracked-overwriting.sh              |   8 +-
 t/t2501-cwd-empty.sh                          |   4 +-
 t/t3001-ls-files-others-exclude.sh            |   6 +-
 t/t3007-ls-files-recurse-submodules.sh        |   6 +-
 t/t3200-branch.sh                             |  12 +-
 t/t3202-show-branch.sh                        |  10 +-
 t/t3203-branch-output.sh                      |   4 +-
 t/t3206-range-diff.sh                         |  78 +--
 t/t3207-branch-submodule.sh                   |   4 +-
 t/t3301-notes.sh                              |  32 +-
 t/t3310-notes-merge-manual-resolve.sh         |  16 +-
 t/t3320-notes-merge-worktrees.sh              |   2 +-
 t/t3400-rebase.sh                             |  16 +-
 t/t3402-rebase-merge.sh                       |  16 +-
 t/t3404-rebase-interactive.sh                 |  72 +--
 t/t3406-rebase-message.sh                     |   6 +-
 t/t3415-rebase-autosquash.sh                  |  10 +-
 t/t3416-rebase-onto-threedots.sh              |   4 +-
 t/t3418-rebase-continue.sh                    |  10 +-
 t/t3420-rebase-autostash.sh                   |  24 +-
 t/t3422-rebase-incompatible-options.sh        |   4 +-
 t/t3429-rebase-edit-todo.sh                   |   2 +-
 t/t3430-rebase-merges.sh                      |  32 +-
 t/t3500-cherry.sh                             |   4 +-
 t/t3501-revert-cherry-pick.sh                 |   6 +-
 t/t3504-cherry-pick-rerere.sh                 |   6 +-
 t/t3510-cherry-pick-sequence.sh               |  24 +-
 t/t3602-rm-sparse-checkout.sh                 |   4 +-
 t/t3705-add-sparse-checkout.sh                |  10 +-
 t/t3800-mktag.sh                              |   4 +-
 t/t3901-i18n-patch.sh                         |  16 +-
 t/t3903-stash.sh                              |  28 +-
 t/t3904-stash-patch.sh                        |   4 +-
 t/t3908-stash-in-worktree.sh                  |   2 +-
 t/t4000-diff-format.sh                        |   2 +-
 t/t4001-diff-rename.sh                        |   4 +-
 t/t4011-diff-symlink.sh                       |   2 +-
 t/t4013-diff-various.sh                       |   2 +-
 t/t4014-format-patch.sh                       | 344 +++++------
 t/t4015-diff-whitespace.sh                    |  16 +-
 t/t4017-diff-retval.sh                        |   2 +-
 t/t4018-diff-funcname.sh                      |   2 +-
 t/t4019-diff-wserror.sh                       |   8 +-
 t/t4020-diff-external.sh                      |  18 +-
 t/t4021-format-patch-numbered.sh              |   4 +-
 t/t4022-diff-rewrite.sh                       |  14 +-
 t/t4028-format-patch-mime-headers.sh          |   6 +-
 t/t4031-diff-rewrite-binary.sh                |  18 +-
 t/t4033-diff-patience.sh                      |   2 +-
 t/t4036-format-patch-signer-mime.sh           |   6 +-
 t/t4038-diff-combined.sh                      |   6 +-
 t/t4051-diff-function-context.sh              |  38 +-
 t/t4053-diff-no-index.sh                      |   4 +-
 t/t4063-diff-blobs.sh                         |   2 +-
 t/t4065-diff-anchored.sh                      |  26 +-
 t/t4067-diff-partial-clone.sh                 |  12 +-
 t/t4073-diff-stat-name-width.sh               |  24 +-
 t/t4103-apply-binary.sh                       |   2 +-
 t/t4120-apply-popt.sh                         |   2 +-
 t/t4124-apply-ws-rule.sh                      |  10 +-
 t/t4128-apply-root.sh                         |   2 +-
 t/t4140-apply-ita.sh                          |   4 +-
 t/t4141-apply-too-large.sh                    |   2 +-
 t/t4150-am.sh                                 |  48 +-
 t/t4200-rerere.sh                             |   6 +-
 t/t4201-shortlog.sh                           |   2 +-
 t/t4202-log.sh                                |  84 +--
 t/t4204-patch-id.sh                           |   2 +-
 t/t4205-log-pretty-formats.sh                 |   2 +-
 t/t4209-log-pickaxe.sh                        |  10 +-
 t/t4211-line-log.sh                           |  72 +--
 t/t4216-log-bloom.sh                          |  18 +-
 t/t4252-am-options.sh                         |  22 +-
 t/t4254-am-corrupt.sh                         |   6 +-
 t/t4258-am-quoted-cr.sh                       |   2 +-
 t/t4301-merge-tree-write-tree.sh              |  18 +-
 t/t5000-tar-tree.sh                           |  10 +-
 t/t5004-archive-corner-cases.sh               |   2 +-
 t/t5100-mailinfo.sh                           |   2 +-
 t/t5150-request-pull.sh                       |  18 +-
 t/t5300-pack-object.sh                        |  22 +-
 t/t5302-pack-index.sh                         |   6 +-
 t/t5304-prune.sh                              |   8 +-
 t/t5310-pack-bitmaps.sh                       |  14 +-
 t/t5317-pack-objects-filter-objects.sh        |  12 +-
 t/t5318-commit-graph.sh                       |   8 +-
 t/t5319-multi-pack-index.sh                   |  16 +-
 t/t5324-split-commit-graph.sh                 |  10 +-
 t/t5325-reverse-index.sh                      |   2 +-
 t/t5326-multi-pack-bitmaps.sh                 |  28 +-
 t/t5328-commit-graph-64bit-time.sh            |   2 +-
 t/t5329-pack-objects-cruft.sh                 |   8 +-
 t/t5334-incremental-multi-pack-index.sh       |   2 +-
 t/t5335-compact-multi-pack-index.sh           |   4 +-
 t/t5351-unpack-large-objects.sh               |   2 +-
 t/t5402-post-merge-hook.sh                    |   4 +-
 t/t5403-post-checkout-hook.sh                 |   2 +-
 t/t5404-tracking-branches.sh                  |   2 +-
 t/t5406-remote-rejects.sh                     |   2 +-
 t/t5407-post-rewrite-hook.sh                  |   8 +-
 t/t5409-colorize-remote-messages.sh           |  36 +-
 t/t5500-fetch-pack.sh                         |  38 +-
 t/t5504-fetch-receive-strict.sh               |  14 +-
 t/t5505-remote.sh                             |  20 +-
 t/t5510-fetch.sh                              |  10 +-
 t/t5512-ls-remote.sh                          |   8 +-
 t/t5514-fetch-multiple.sh                     |   2 +-
 t/t5516-fetch-push.sh                         |  20 +-
 t/t5520-pull.sh                               |   4 +-
 t/t5524-pull-msg.sh                           |   6 +-
 t/t5526-fetch-submodules.sh                   |  16 +-
 t/t5529-push-errors.sh                        |   4 +-
 t/t5530-upload-pack-error.sh                  |  18 +-
 t/t5531-deep-submodule-push.sh                |   2 +-
 t/t5532-fetch-proxy.sh                        |   2 +-
 t/t5533-push-cas.sh                           |  12 +-
 t/t5534-push-signed.sh                        |  22 +-
 t/t5537-fetch-shallow.sh                      |   2 +-
 t/t5538-push-shallow.sh                       |   2 +-
 t/t5539-fetch-http-shallow.sh                 |   4 +-
 t/t5541-http-push-smart.sh                    |  32 +-
 t/t5544-pack-objects-hook.sh                  |  12 +-
 t/t5550-http-fetch-dumb.sh                    |   4 +-
 t/t5551-http-fetch-smart.sh                   |  46 +-
 t/t5552-skipping-fetch-negotiator.sh          |   6 +-
 t/t5554-noop-fetch-negotiator.sh              |   4 +-
 t/t5557-http-get.sh                           |   2 +-
 t/t5558-clone-bundle-uri.sh                   |  38 +-
 t/t5562-http-backend-content-length.sh        |   2 +-
 t/t5564-http-proxy.sh                         |  10 +-
 t/t5581-http-curl-verbose.sh                  |   2 +-
 t/t5583-push-branches.sh                      |   8 +-
 t/t5601-clone.sh                              |  28 +-
 t/t5604-clone-reference.sh                    |   8 +-
 t/t5605-clone-local.sh                        |   2 +-
 t/t5606-clone-options.sh                      |   6 +-
 t/t5612-clone-refspec.sh                      |   2 +-
 t/t5616-partial-clone.sh                      |  60 +-
 t/t5619-clone-local-ambiguous-transport.sh    |   2 +-
 t/t5620-backfill.sh                           |  12 +-
 t/t5700-protocol-v1.sh                        |  46 +-
 t/t5701-git-serve.sh                          |  14 +-
 t/t5702-protocol-v2.sh                        | 154 ++---
 t/t5703-upload-pack-ref-in-want.sh            |  22 +-
 t/t5705-session-id-in-capabilities.sh         |  12 +-
 t/t5750-bundle-uri-parse.sh                   |   8 +-
 t/t5801-remote-helpers.sh                     |   4 +-
 t/t5810-proto-disable-local.sh                |   2 +-
 t/t5813-proto-disable-ssh.sh                  |   4 +-
 t/t6000-rev-list-misc.sh                      |  26 +-
 t/t6005-rev-list-count.sh                     |   8 +-
 t/t6006-rev-list-format.sh                    |   4 +-
 t/t6009-rev-list-parent.sh                    |   4 +-
 t/t6020-bundle-misc.sh                        |  12 +-
 t/t6022-rev-list-missing.sh                   |   4 +-
 t/t6030-bisect-porcelain.sh                   | 150 ++---
 t/t6040-tracking-info.sh                      |   2 +-
 t/t6112-rev-list-filters-objects.sh           |  24 +-
 t/t6115-rev-list-du.sh                        |   4 +-
 t/t6120-describe.sh                           |  14 +-
 t/t6200-fmt-merge-msg.sh                      |  82 +--
 t/t6402-merge-rename.sh                       |   4 +-
 t/t6403-merge-file.sh                         |   6 +-
 t/t6404-recursive-merge.sh                    |   2 +-
 t/t6406-merge-attr.sh                         |  20 +-
 t/t6417-merge-ours-theirs.sh                  |  30 +-
 t/t6418-merge-text-auto.sh                    |   2 +-
 t/t6422-merge-rename-corner-cases.sh          |   8 +-
 t/t6423-merge-rename-directories.sh           |  72 +--
 t/t6424-merge-unrelated-index-changes.sh      |   6 +-
 t/t6427-diff3-conflict-markers.sh             |  10 +-
 t/t6432-merge-recursive-space-options.sh      |   4 +-
 t/t6436-merge-overwrite.sh                    |   6 +-
 t/t6437-submodule-merge.sh                    |  10 +-
 t/t6500-gc.sh                                 |   8 +-
 t/t6600-test-reach.sh                         |   4 +-
 t/t7001-mv.sh                                 |  16 +-
 t/t7002-mv-sparse-checkout.sh                 |  38 +-
 t/t7003-filter-branch.sh                      |  16 +-
 t/t7004-tag.sh                                |   2 +-
 t/t7006-pager.sh                              |  16 +-
 t/t7012-skip-worktree-writing.sh              |   6 +-
 t/t7030-verify-tag.sh                         |  52 +-
 t/t7031-verify-tag-signed-ssh.sh              |  46 +-
 t/t7102-reset.sh                              |   2 +-
 t/t7110-reset-merge.sh                        |  40 +-
 t/t7201-co.sh                                 |   6 +-
 t/t7300-clean.sh                              |   2 +-
 t/t7301-clean-interactive.sh                  |   2 +-
 t/t7400-submodule-basic.sh                    |  32 +-
 t/t7402-submodule-rebase.sh                   |   2 +-
 t/t7406-submodule-update.sh                   |  26 +-
 t/t7416-submodule-dash-url.sh                 |  20 +-
 t/t7417-submodule-path-url.sh                 |   2 +-
 t/t7450-bad-git-dotfiles.sh                   |  18 +-
 t/t7501-commit-basic-functionality.sh         |  16 +-
 t/t7502-commit-porcelain.sh                   |   2 +-
 t/t7507-commit-verbose.sh                     |   6 +-
 t/t7508-status.sh                             |   6 +-
 t/t7510-signed-commit.sh                      |  68 +--
 t/t7516-commit-races.sh                       |   4 +-
 t/t7519-status-fsmonitor.sh                   |  14 +-
 t/t7527-builtin-fsmonitor.sh                  |  82 +--
 t/t7528-signed-commit-ssh.sh                  |  68 +--
 t/t7600-merge.sh                              |  10 +-
 t/t7603-merge-reduce-heads.sh                 |  20 +-
 t/t7606-merge-custom.sh                       |   2 +-
 t/t7607-merge-state.sh                        |   4 +-
 t/t7610-mergetool.sh                          |  18 +-
 t/t7700-repack.sh                             |  14 +-
 t/t7703-repack-geometric.sh                   |   4 +-
 t/t7704-repack-cruft.sh                       |  12 +-
 t/t7800-difftool.sh                           |  26 +-
 t/t7810-grep.sh                               |  22 +-
 t/t7814-grep-recurse-submodules.sh            |   2 +-
 t/t7900-maintenance.sh                        |  34 +-
 t/t8008-blame-formats.sh                      |   2 +-
 t/t8010-cat-file-filters.sh                   |   2 +-
 t/t8012-blame-colors.sh                       |   2 +-
 t/t9001-send-email.sh                         | 190 +++----
 t/t9003-help-autocorrect.sh                   |   6 +-
 t/t9106-git-svn-commit-diff-clobber.sh        |   2 +-
 t/t9107-git-svn-migrate.sh                    |  30 +-
 t/t9110-git-svn-use-svm-props.sh              |  20 +-
 t/t9111-git-svn-use-svnsync-props.sh          |  18 +-
 t/t9114-git-svn-dcommit-merge.sh              |   6 +-
 t/t9116-git-svn-log.sh                        |   8 +-
 t/t9117-git-svn-init-clone.sh                 |  12 +-
 t/t9119-git-svn-info.sh                       |  16 +-
 t/t9122-git-svn-author.sh                     |   8 +-
 t/t9130-git-svn-authors-file.sh               |   8 +-
 t/t9138-git-svn-authors-prog.sh               |  14 +-
 t/t9140-git-svn-reset.sh                      |   4 +-
 t/t9153-git-svn-rewrite-uuid.sh               |   4 +-
 t/t9200-git-cvsexportcommit.sh                |   2 +-
 t/t9210-scalar.sh                             |  34 +-
 t/t9211-scalar-clone.sh                       |  16 +-
 t/t9300-fast-import.sh                        |  10 +-
 t/t9350-fast-export.sh                        |  54 +-
 t/t9351-fast-export-anonymize.sh              |  36 +-
 t/t9400-git-cvsserver-server.sh               |   4 +-
 t/t9501-gitweb-standalone-http-status.sh      |  58 +-
 t/t9502-gitweb-standalone-parse-output.sh     |  38 +-
 t/t9800-git-p4-basic.sh                       |  10 +-
 t/t9801-git-p4-branch.sh                      |  48 +-
 t/t9806-git-p4-options.sh                     |  10 +-
 t/t9807-git-p4-submit.sh                      |   2 +-
 t/t9810-git-p4-rcs.sh                         |   8 +-
 t/t9813-git-p4-preserve-users.sh              |   8 +-
 t/t9814-git-p4-rename.sh                      |   8 +-
 t/t9827-git-p4-change-filetype.sh             |   4 +-
 t/t9832-unshelve.sh                           |   6 +-
 t/t9833-errors.sh                             |   4 +-
 t/t9835-git-p4-metadata-encoding-python2.sh   |  36 +-
 t/t9836-git-p4-metadata-encoding-python3.sh   |  38 +-
 t/t9850-shell.sh                              |   2 +-
 t/t9902-completion.sh                         |  26 +-
 385 files changed, 3783 insertions(+), 3328 deletions(-)
 create mode 100644 t/greplint-cat.pl
 create mode 100644 t/greplint.pl
 create mode 100644 t/greplint/bare-grep-after-and.expect
 create mode 100644 t/greplint/bare-grep-after-and.test
 create mode 100644 t/greplint/bare-grep-after-semicolon.expect
 create mode 100644 t/greplint/bare-grep-after-semicolon.test
 create mode 100644 t/greplint/bare-grep-compound-body.expect
 create mode 100644 t/greplint/bare-grep-compound-body.test
 create mode 100644 t/greplint/bare-grep-count-mode.expect
 create mode 100644 t/greplint/bare-grep-count-mode.test
 create mode 100644 t/greplint/bare-grep-explicit-pattern.expect
 create mode 100644 t/greplint/bare-grep-explicit-pattern.test
 create mode 100644 t/greplint/bare-grep-flags.expect
 create mode 100644 t/greplint/bare-grep-flags.test
 create mode 100644 t/greplint/bare-grep-lint-ok.expect
 create mode 100644 t/greplint/bare-grep-lint-ok.test
 create mode 100644 t/greplint/bare-grep-negated.expect
 create mode 100644 t/greplint/bare-grep-negated.test
 create mode 100644 t/greplint/bare-grep-pattern-file.expect
 create mode 100644 t/greplint/bare-grep-pattern-file.test
 create mode 100644 t/greplint/bare-grep-simple.expect
 create mode 100644 t/greplint/bare-grep-simple.test
 create mode 100644 t/greplint/bare-grep-subshell.expect
 create mode 100644 t/greplint/bare-grep-subshell.test
 create mode 100644 t/greplint/dqstring-continuation-offset.expect
 create mode 100644 t/greplint/dqstring-continuation-offset.test
 create mode 100644 t/greplint/filter-command-substitution.expect
 create mode 100644 t/greplint/filter-command-substitution.test
 create mode 100644 t/greplint/filter-pipe-input.expect
 create mode 100644 t/greplint/filter-pipe-input.test
 create mode 100644 t/greplint/filter-pipe-output.expect
 create mode 100644 t/greplint/filter-pipe-output.test
 create mode 100644 t/greplint/filter-redirect-output.expect
 create mode 100644 t/greplint/filter-redirect-output.test
 create mode 100644 t/greplint/filter-stdin-redirect.expect
 create mode 100644 t/greplint/filter-stdin-redirect.test
 create mode 100644 t/greplint/grep-as-argument.expect
 create mode 100644 t/greplint/grep-as-argument.test
 create mode 100644 t/greplint/grep-as-value.expect
 create mode 100644 t/greplint/grep-as-value.test
 create mode 100644 t/greplint/wrong-negation.expect
 create mode 100644 t/greplint/wrong-negation.test
 create mode 100644 t/lib-shell-parser.pl

base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2135

Range-diff vs v2:

1: 5959cab258 ! 1: ec6b478c4c t/README: document test_grep helper
@@ t/README: see test-lib-functions.sh for the full list and their options.
+ 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.
++ 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), keep a plain guarded 'grep' instead.
+
- test_path_is_file <path>
test_path_is_dir <path>

 2:  f3e8e19e6e = 2:  7a837b77cd t: fix grep assertions missing file arguments
 3:  460461b5fe ! 3:  5894ca1724 t: extract chainlint's parser into shared module
     @@ t/chainlint.pl: my $jobs = -1;
      -#
      -# 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 whitepace, is scanned as three token in shell, as well.
     +-# `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
     @@ t/lib-shell-parser.pl (new)
      +#
      +# 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 whitepace, is scanned as three token in shell, as well.
     ++# `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
 4:  c1b86748d1 = 4:  416c95fee5 t: fix Lexer line count for $() inside double-quoted strings
 5:  3a589ef738 ! 5:  7518445733 t: convert grep assertions to test_grep

@@ Commit message
Existing '! test_grep' calls are rewritten to 'test_grep !' so that the diagnostic output is preserved on failure.

+ test_grep requires the file it reads to exist, so '! grep'
+ assertions that inspect a file whose presence is conditional need
+ care. In t5537 the '.git/shallow' file is still present after the
+ repack (the client remains shallow), so the assertion is
+ converted like any other. In t1400 the '.git/packed-refs' file
+ exists only with the files backend, so its check is guarded with a
+ REFFILES prerequisite; the backend-agnostic 'git show-ref' check
+ that follows still runs under every backend. In t7450 'git~2' is
+ the NTFS 8.3 short name of a decoy '..git' file and only exists
+ when 8.3 short-name generation is enabled; that '! grep' tolerates
+ the missing file on purpose, so it is left as-is with a
+ "# lint-ok" annotation rather than converted (a plain test_grep
+ would BUG when the short name is absent).
+
The conversion was generated using a grep-assertion linter (greplint.pl, added in the following commit) to identify bare
- grep calls at command position. To reproduce:
+ grep calls at command position. To reproduce, from the t/
+ directory:

- # Step 1: mark bare greps that should not be converted
- sed -i '/! grep "$m" \.git\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \
- t/t1400-update-ref.sh
- sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \
- t/t3420-rebase-autostash.sh
+ # Step 1: annotate the two data-filter greps (grep produces
+ # data, not a verdict) so the linter skips them.
sed -i '/grep -vf before commits\.raw/s/$/ # lint-ok: data filter/' \
- t/t5326-multi-pack-bitmaps.sh
- sed -i '/! grep $d shallow-client\/\.git\/shallow/s/$/ # lint-ok: file may not exist after repack/' \
- t/t5537-fetch-shallow.sh
+ t5326-multi-pack-bitmaps.sh
sed -i '/grep -E "^\[0-9a-f\].*|| :/s/$/ # lint-ok: data filter/' \
- t/t5702-protocol-v2.sh
- sed -i '/! grep gitdir squatting-clone/s/$/ # lint-ok: file may not exist after failed clone/' \
- t/t7450-bad-git-dotfiles.sh
+ t5702-protocol-v2.sh
+
+ # Step 1b: two '! grep' assertions need more than a mechanical
+ # conversion; handle them by hand before the linter-driven steps
+ # below so it leaves them alone.
+ #
+ # t1400: '.git/packed-refs' is absent under reftable, so guard the
+ # check with REFFILES (a plain test_grep would BUG on the missing
+ # file):
+ #
+ # 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
+ #
+ # t7450: git~2 is an NTFS 8.3 short name that exists only when
+ # short-name generation is enabled, so this stays a missing-file-
+ # tolerant '! grep'; add a comment plus "# lint-ok" so the linter
+ # skips it.

# Step 2: reorder pre-existing '! test_grep' to 'test_grep !' # (must come before steps 3-4 so greplint does not see them)
- sed -i 's/! test_grep/test_grep !/' t/t0031-lockfile-pid.sh
- sed -i 's/! test_grep/test_grep !/' t/t5300-pack-object.sh
- sed -i 's/! test_grep/test_grep !/' t/t5319-multi-pack-index.sh
+ sed -i 's/! test_grep/test_grep !/' t0031-lockfile-pid.sh
+ sed -i 's/! test_grep/test_grep !/' t5300-pack-object.sh
+ sed -i 's/! test_grep/test_grep !/' t5319-multi-pack-index.sh

# Step 3: convert '! grep' -> 'test_grep !'
- perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |
+ perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |
while IFS=: read f l; do
sed -i "${l}s/! *grep/test_grep !/" "$f"
done

# Step 4: convert remaining 'grep' -> 'test_grep'
- perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |
+ perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |
while IFS=: read f l; do
sed -i "${l}s/grep/test_grep/" "$f"
done
@@ t/t1400-update-ref.sh: test_expect_success "move $m (by HEAD)" '
test_when_finished "git update-ref -d $m" &&
git update-ref -d HEAD $B &&
- ! grep "$m" .git/packed-refs &&
-+ ! grep "$m" .git/packed-refs && # lint-ok: file may not exist (reftable)
++ if test_have_prereq REFFILES
++ then
++ test_grep ! "$m" .git/packed-refs
++ fi &&
test_must_fail git show-ref --verify -q $m
'

@@ t/t3420-rebase-autostash.sh: testrebase () {
git checkout feature-branch
'

-@@ t/t3420-rebase-autostash.sh: testrebase () {
- git rebase --quit &&
- test_when_finished git stash drop &&
- test_path_is_missing $dotest/autostash &&
-- ! grep dirty file3 &&
-+ ! grep dirty file3 && # lint-ok: file may not exist after --quit
- git stash show -p >actual &&
- test_cmp expect actual &&
- git reset --hard &&
@@ t/t3420-rebase-autostash.sh: testrebase () {
git rebase$type unrelated-onto-branch >actual 2>&1 && test_path_is_missing $dotest &&
@@ t/t4211-line-log.sh: test_expect_success '-L with -G filters to diff-text matche
+ test_grep "F2 + 2" actual
'

- test_done
+ test_expect_success '-L with --diff-filter=M excludes root commit' '

## t/t4216-log-bloom.sh ##
@@ t/t4216-log-bloom.sh: test_expect_success 'persist filter settings' '
@@ t/t5537-fetch-shallow.sh: test_expect_success '.git/shallow is edited by repack'
git -C shallow-client repack -adfl &&
test_must_fail git -C shallow-client rev-parse --verify $d^0 &&
- ! grep $d shallow-client/.git/shallow &&
-+ ! grep $d shallow-client/.git/shallow && # lint-ok: file may not exist after repack
++ test_grep ! $d shallow-client/.git/shallow &&

git -C shallow-server branch branch-orig $d &&
git -C shallow-client fetch --prune --depth=2 \
@@ t/t7450-bad-git-dotfiles.sh: test_expect_success WINDOWS 'prevent git~1 squattin
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
-+ ! grep gitdir squatting-clone/d/a/git~2 # lint-ok: file may not exist after failed clone
++ # git~2 is the 8.3 short name of the ..git decoy, present
++ # only when 8.3 name generation is on; a missing git~2 is
++ # fine because the "directory not empty" check above is the
++ # real assertion.
++ ! grep gitdir squatting-clone/d/a/git~2 # lint-ok: 8.3 short name git~2 may not exist
fi
'

6: e5ecb37401 = 6: 3b12a959fd t: add greplint to detect bare grep assertions

Signature · 1 line
gitgitgadget
#25 lore raw

PATCH v3 1/6 t/README: document test_grep helper

From: Michael Montalbo <[email protected]>

test_grep is a wrapper around grep for test assertions that prints the file contents on failure for easier debugging. It also accepts '!' as its first argument for negation, which preserves the diagnostic output that '! test_grep' would suppress.

Despite being widely used (and the preferred replacement for bare grep in assertions), test_grep has no entry in t/README alongside the other documented helpers like test_cmp and test_line_count. Add one.

---
 t/README | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)
t/README +26 -0
index 085921be4b..928331fc7d 100644
@@ -1039,6 +1039,32 @@ see test-lib-functions.sh for the full list and their options.
 
    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.
+   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), keep a plain guarded 'grep' instead.
+
  - test_path_is_file <path>
    test_path_is_dir <path>
    test_path_is_missing <path>
Signature · 1 line
gitgitgadget
#26 lore raw

PATCH v3 2/6 t: fix grep assertions missing file arguments

In reply to Michael Montalbo via GitGitGadget

From: Michael Montalbo <[email protected]>

Three grep assertions were missing their file arguments, causing them to read from empty stdin instead of the intended file:

- t2402: '! grep ...' should read from 'out', matching the grep on the preceding line.
- t7507: the closing quote is in the wrong place, making the entire 'diff --git actual' a single pattern with no file argument instead of pattern 'diff --git' and file 'actual'.
- t7700: '! grep ...' should read from 'packlist', matching the redirect on the preceding line.

Without file arguments these greps always succeed (empty stdin matches nothing), so the assertions were not actually checking anything. All three tests pass with the corrected file arguments, confirming the intended behavior is sound.

---
 t/t2402-worktree-list.sh  | 2 +-
 t/t7507-commit-verbose.sh | 2 +-
 t/t7700-repack.sh         | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)
t/t2402-worktree-list.sh +1 -1
index e0c6abd2f5..93f92e854a 100755
@@ -144,7 +144,7 @@ test_expect_success '"list" all worktrees with prunable annotation' '
 	rm -rf prunable &&
 	git worktree list >out &&
 	grep "/prunable  *[0-9a-f].* prunable$" out &&
-	! grep "/unprunable  *[0-9a-f].* prunable$"
+	! grep "/unprunable  *[0-9a-f].* prunable$" out
 '
 
 test_expect_success '"list" all worktrees --porcelain with prunable' '
t/t7507-commit-verbose.sh +1 -1
index b53d71c086..acdb6b1455 100755
@@ -163,7 +163,7 @@ done
 
 test_expect_success "status ignores commit.verbose=true" '
 	git -c commit.verbose=true status >actual &&
-	! grep "^diff --git actual"
+	! grep "^diff --git" actual
 '
 
 test_done
t/t7700-repack.sh +1 -1
index 63ef63fc50..c6ff3aed30 100755
@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar
 	rm -f .git/objects/pack/* &&
 	mv pack-* .git/objects/pack/ &&
 	git verify-pack -v -- .git/objects/pack/*.idx >packlist &&
-	! grep "^$coid " &&
+	! grep "^$coid " packlist &&
 	echo >.git/objects/info/alternates &&
 	test_must_fail git show $coid
 '
Signature · 1 line
gitgitgadget