Skip to content

add blocked-string rewrite validation fixtures#8

Merged
bschellenberger2600 merged 3 commits into
mainfrom
feat/gmt-watch-sanitize-fixtures
Apr 16, 2026
Merged

add blocked-string rewrite validation fixtures#8
bschellenberger2600 merged 3 commits into
mainfrom
feat/gmt-watch-sanitize-fixtures

Conversation

@bschellenberger2600
Copy link
Copy Markdown
Member

@bschellenberger2600 bschellenberger2600 commented Apr 16, 2026

Summary

  • add reusable sanitize/rewrite fixtures that seed blocked strings across file contents, commit messages, refs, and paths
  • add scan/assert helpers for full-surface detection and removal checks
  • document fixture usage and focused test commands in README/developer docs

Test plan

  • go test ./... -run 'RewriteValidation|BlockedStringCoverage'
  • go test ./...

Made with Cursor


Note

Medium Risk
Adds new exported helpers that execute git commands and parse repo history/refs, so edge cases and performance of scans could affect downstream tests; changes are otherwise additive and well-covered by new tests.

Overview
Adds a new sanitize/rewrite validation fixture (CreateRewriteValidationFixture) that seeds a repo with a configurable blocked token across file content, commit messages, refs (branch/tag), and object paths.

Introduces scan/validate/assert helpers (ScanBlockedStringSurfaces*, ValidateBlockedStringCoverage*, AssertBlockedStringCoverage*) to verify blocked-string detection coverage pre-rewrite and confirm complete removal post-rewrite.

Updates README.md, DEVELOPER_GUIDE.md, and package docs with usage examples and a focused test command, and adds sanitize_fixtures_test.go to exercise fixture creation, invalid token rejection, and a simulated history rewrite cleanup flow.

Reviewed by Cursor Bugbot for commit dc8e18b. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Documentation

    • Updated developer guide with sanitize/rewrite testing workflow examples and targeted test commands
    • Added sanitize/rewrite validation helpers to README with usage guidance
  • New Features

    • Added test fixture creation helper for seeding blocked-strings across Git surfaces (contents, history, refs, paths)
    • Added blocked-string detection and assertion utilities for validation testing
  • Tests

    • Added comprehensive test coverage for sanitize/rewrite validation scenarios

Add reusable fixtures and assertions for coverage across file contents, commit messages, refs, and paths so sanitize/rewrite workflows can be tested consistently.

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

This pull request adds testing utilities for sanitize/rewrite validation in the testutil package. It introduces a RewriteValidationFixture for creating test Git repositories with blocked strings embedded across multiple surfaces (file contents, commits, refs, paths), and scanning/assertion helpers to verify blocked-string detection and removal.

Changes

Cohort / File(s) Summary
Documentation Updates
DEVELOPER_GUIDE.md, README.md, doc.go
Documentation-only updates referencing new CreateRewriteValidationFixture helper and blocked-string assertions, along with example test commands for sanitize/rewrite validation workflows.
Sanitize/Rewrite Validation Fixtures
sanitize_fixtures.go
New utility module introducing RewriteValidationFixture and RewriteValidationFixtureOptions for test repository creation; BlockedStringSurfaceMatches struct with IsClean() and MissingSurfaces() methods; and fixture/scanning functions (CreateRewriteValidationFixture, ScanBlockedStringSurfaces, ValidateBlockedStringCoverageDetectedE, ValidateBlockedStringCoverageRemovedE, AssertBlockedStringCoverageDetected, AssertBlockedStringCoverageRemoved) that embed blocked strings across Git surfaces and validate their presence/removal via Git commands.
Test Coverage
sanitize_fixtures_test.go
Test cases validating fixture creation, blocked-string detection across file contents/commit messages/refs/paths, history rewriting, and validation error handling; includes helpers for substring and exact-match assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A new fixture hops into testutil's warren,
Embedding blocked strings through git's domains—
Contents, commits, branches, and paths declared,
Then scanned with git commands in careful chains,
To verify the sanitize rewrite's marvelous care! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'add blocked-string rewrite validation fixtures' directly and specifically describes the main change: introducing new fixture utilities for sanitize/rewrite validation across multiple Git surfaces.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gmt-watch-sanitize-fixtures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bschellenberger2600
Copy link
Copy Markdown
Member Author

bugbot-review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Duplicated commit hash prefix in file content matches
    • File content match keys now use each git grep line as-is, since output already begins with commit:path:line:content.
  • ✅ Fixed: Missing dash-prefix validation causes silent false negatives
    • validateBlockedRefPathToken rejects leading hyphens and runGitGrepForCommit passes the pattern with -e so git never treats it as flags.
Preview (47551d10f8)
diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md
--- a/DEVELOPER_GUIDE.md
+++ b/DEVELOPER_GUIDE.md
@@ -31,9 +31,11 @@
 ## Project structure
 
 - `fixtures.go`: base repo creation and command helpers.
+- `sanitize_fixtures.go`: reusable sanitize/rewrite fixtures plus blocked-string scan/assert helpers.
 - `scenarios.go`: fluent scenario builder and predefined multi-repo scenarios.
 - `snapshots.go`: snapshot/restore utilities for expensive test setup reuse.
 - `fixtures_test.go`: external package tests (`testutil_test`) that validate public API usage.
+- `sanitize_fixtures_test.go`: coverage tests for blocked-string detection/removal across content/history/refs/paths.
 - `usb_fixtures.go` / `usb_fixtures_test.go`: USB volume root and `.git-fire` config helpers for backup-mode style tests.
 - `scenarios_test.go`: package-internal tests for scenario/snapshot behavior.
 
@@ -74,7 +76,19 @@
 - `CreateTestRepo`: fast setup for single-repo state.
 - `NewScenario`: multi-repo topologies and fluent setup.
 - `SnapshotRepo`/`RestoreSnapshot`: performance optimization for repeated expensive setups.
+- `CreateRewriteValidationFixture` + blocked-string assertions: sanitize/rewrite validation against file contents, commit messages, refs, and paths.
 
+Sanitize/rewrite focused validation loop:
+
+```bash
+# Run only sanitize/rewrite fixture and assertion tests.
+go test ./... -run 'RewriteValidation|BlockedStringCoverage'
+
+# Full required local gate.
+go vet ./...
+go test ./...
+```
+
 ## Adding new helpers
 
 When adding new exported helpers:

diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@
 - Repository fixtures (`CreateTestRepo`, `CreateBareRemote`, `RunGitCmd`)
 - Scenario builders for common multi-repo states (`NewScenario`, conflict/worktree helpers)
 - Snapshot helpers for capturing and restoring repository state in tests
+- Sanitize/rewrite validation helpers for blocked-string coverage across content/history/refs/paths
 
 ## API overview
 
@@ -38,6 +39,8 @@
 - `NewScenario(t)` returns a fluent builder for multi-repo test topologies.
 - `SnapshotRepo(t, path)` and `RestoreSnapshot(t, snap)` speed up repeated fixture setup.
 - `IsDirty`, `GetCurrentSHA`, `GetBranches`, `GetRemotes` provide common assertions/helpers.
+- `CreateRewriteValidationFixture(t, opts)` seeds blocked strings across file contents, commit messages, refs, and paths.
+- `AssertBlockedStringCoverageDetected` and `AssertBlockedStringCoverageRemoved` validate sanitize/rewrite coverage before/after rewriting.
 
 ## Quickstart for using in tests
 
@@ -113,6 +116,33 @@
 }

+### Validate sanitize/rewrite coverage
+
+```go
+func TestRewriteRemovesBlockedString(t *testing.T) {

  • fixture := testutil.CreateRewriteValidationFixture(t, testutil.RewriteValidationFixtureOptions{
  •   Name:          "rewrite-fixture",
    
  •   BlockedString: "blockedtoken",
    
  • })
  • // Confirm fixture coverage before running your rewrite logic.
  • testutil.AssertBlockedStringCoverageDetected(t, fixture.RepoPath, fixture.BlockedString)
  • // Run your sanitizer/rewrite command against fixture.RepoPath here.
  • // Example:
  • // testutil.RunGitCmd(t, fixture.RepoPath, "filter-branch", ...)
  • // Validate the blocked string is removed from all tracked surfaces.
  • testutil.AssertBlockedStringCoverageRemoved(t, fixture.RepoPath, fixture.BlockedString)
    +}
    +```

+Run just sanitize/rewrite-focused tests:
+
+bash +go test ./... -run 'RewriteValidation|BlockedStringCoverage' +
+

Notes

  • Snapshots are intended for deterministic test fixtures and only restore regular files/directories.

diff --git a/doc.go b/doc.go
--- a/doc.go
+++ b/doc.go
@@ -11,6 +11,8 @@
// remote, diverged clones, worktrees, conflict states).
// - [SnapshotRepo] / [RestoreSnapshot] for capturing expensive fixture state
// once and restoring it cheaply across subtests.
+// - [CreateRewriteValidationFixture] plus blocked-string assertions for
+// sanitize/rewrite validation across content, commit history, refs, and paths.
//
// All helpers integrate with Go's testing.T: repositories are created inside
// t.TempDir() and cleaned up automatically, and setup failures call t.Fatalf

diff --git a/sanitize_fixtures.go b/sanitize_fixtures.go
new file mode 100644
--- /dev/null
+++ b/sanitize_fixtures.go
@@ -1,0 +1,465 @@
+package testutil
+
+import (

  • "bytes"
  • "fmt"
  • "os"
  • "os/exec"
  • "path/filepath"
  • "regexp"
  • "sort"
  • "strings"
  • "testing"
    +)

+const defaultRewriteValidationFixtureName = "rewrite-validation-fixture"
+
+var blockedRefPathTokenPattern = regexp.MustCompile(^[A-Za-z0-9._-]+$)
+
+// RewriteValidationFixtureOptions configures creation of a sanitize/rewrite fixture repository.
+type RewriteValidationFixtureOptions struct {

  • // Name controls the repository directory name under the fixture root.
  • // Defaults to "rewrite-validation-fixture".
  • Name string
  • // BlockedString is the token intentionally seeded across file contents,
  • // commit messages, refs, and object paths.
  • //
  • // The value must be ref/path safe (letters, numbers, dot, underscore, dash)
  • // so it can be embedded in branch/tag names and file paths.
  • BlockedString string
    +}

+// RewriteValidationFixture captures important fixture metadata for rewrite tests.
+type RewriteValidationFixture struct {

  • RepoPath string
  • DefaultBranch string
  • BlockedString string
  • ContentFile string
  • PathWithBlocked string
  • BranchWithBlocked string
  • TagWithBlocked string
    +}

+// BlockedStringSurfaceMatches reports where a blocked string was found.
+type BlockedStringSurfaceMatches struct {

  • BlockedString string
  • FileContents []string
  • CommitMessages []string
  • Refs []string
  • Paths []string
    +}

+// IsClean reports whether no blocked-string matches were found.
+func (m BlockedStringSurfaceMatches) IsClean() bool {

  • return len(m.FileContents) == 0 &&
  •   len(m.CommitMessages) == 0 &&
    
  •   len(m.Refs) == 0 &&
    
  •   len(m.Paths) == 0
    

+}
+
+// MissingSurfaces reports which required surfaces did not contain a match.
+func (m BlockedStringSurfaceMatches) MissingSurfaces() []string {

  • var missing []string
  • if len(m.FileContents) == 0 {
  •   missing = append(missing, "file-contents")
    
  • }
  • if len(m.CommitMessages) == 0 {
  •   missing = append(missing, "commit-messages")
    
  • }
  • if len(m.Refs) == 0 {
  •   missing = append(missing, "refs")
    
  • }
  • if len(m.Paths) == 0 {
  •   missing = append(missing, "paths")
    
  • }
  • return missing
    +}

+// CreateRewriteValidationFixture creates a repository seeded for sanitize/rewrite validation.
+func CreateRewriteValidationFixture(t *testing.T, opts RewriteValidationFixtureOptions) RewriteValidationFixture {

  • t.Helper()
  • fixture, err := CreateRewriteValidationFixtureInDir(t.TempDir(), opts)
  • if err != nil {
  •   t.Fatalf("Failed to create rewrite validation fixture: %v", err)
    
  • }
  • return fixture
    +}

+// CreateRewriteValidationFixtureInDir creates a repository seeded for sanitize/rewrite validation.
+func CreateRewriteValidationFixtureInDir(baseDir string, opts RewriteValidationFixtureOptions) (RewriteValidationFixture, error) {

  • blocked := strings.TrimSpace(opts.BlockedString)
  • if err := validateBlockedRefPathToken(blocked); err != nil {
  •   return RewriteValidationFixture{}, fmt.Errorf("invalid blocked string %q: %w", opts.BlockedString, err)
    
  • }
  • name := strings.TrimSpace(opts.Name)
  • if name == "" {
  •   name = defaultRewriteValidationFixtureName
    
  • }
  • repoPath, err := CreateTestRepoInDir(baseDir, RepoOptions{Name: name})
  • if err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • defaultBranch, err := RunGitCmdE(repoPath, "rev-parse", "--abbrev-ref", "HEAD")
  • if err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • contentFile := filepath.ToSlash(filepath.Join("fixtures", "blocked-content.txt"))
  • pathWithBlocked := filepath.ToSlash(filepath.Join("fixtures", blocked+"-path.txt"))
  • if err := writeFixtureFile(repoPath, contentFile, "fixture blocked content: "+blocked+"\n"); err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • if err := writeFixtureFile(repoPath, pathWithBlocked, "fixture path marker\n"); err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • if _, err := RunGitCmdE(repoPath, "add", "--", contentFile, pathWithBlocked); err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • if _, err := RunGitCmdE(repoPath, "commit", "-m", "seed blocked string "+blocked+" for rewrite coverage"); err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • branchWithBlocked := "rewrite/" + blocked + "-branch"
  • tagWithBlocked := "rewrite-" + blocked + "-tag"
  • if _, err := RunGitCmdE(repoPath, "branch", branchWithBlocked); err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • if _, err := RunGitCmdE(repoPath, "tag", tagWithBlocked); err != nil {
  •   return RewriteValidationFixture{}, err
    
  • }
  • return RewriteValidationFixture{
  •   RepoPath:          repoPath,
    
  •   DefaultBranch:     defaultBranch,
    
  •   BlockedString:     blocked,
    
  •   ContentFile:       contentFile,
    
  •   PathWithBlocked:   pathWithBlocked,
    
  •   BranchWithBlocked: branchWithBlocked,
    
  •   TagWithBlocked:    tagWithBlocked,
    
  • }, nil
    +}

+// ScanBlockedStringSurfaces scans file contents, commit messages, refs, and object paths.
+func ScanBlockedStringSurfaces(t *testing.T, repoPath, blockedString string) BlockedStringSurfaceMatches {

  • t.Helper()
  • matches, err := ScanBlockedStringSurfacesE(repoPath, blockedString)
  • if err != nil {
  •   t.Fatalf("Failed to scan blocked-string surfaces: %v", err)
    
  • }
  • return matches
    +}

+// ScanBlockedStringSurfacesE scans file contents, commit messages, refs, and object paths.
+func ScanBlockedStringSurfacesE(repoPath, blockedString string) (BlockedStringSurfaceMatches, error) {

  • blocked := strings.TrimSpace(blockedString)
  • if blocked == "" {
  •   return BlockedStringSurfaceMatches{}, fmt.Errorf("blocked string cannot be empty")
    
  • }
  • commits, err := listAllCommits(repoPath)
  • if err != nil {
  •   return BlockedStringSurfaceMatches{}, err
    
  • }
  • fileMatches, err := collectBlockedFileContentMatches(repoPath, commits, blocked)
  • if err != nil {
  •   return BlockedStringSurfaceMatches{}, err
    
  • }
  • commitMatches, err := collectBlockedCommitMessageMatches(repoPath, blocked)
  • if err != nil {
  •   return BlockedStringSurfaceMatches{}, err
    
  • }
  • refMatches, err := collectBlockedRefMatches(repoPath, blocked)
  • if err != nil {
  •   return BlockedStringSurfaceMatches{}, err
    
  • }
  • pathMatches, err := collectBlockedPathMatches(repoPath, blocked)
  • if err != nil {
  •   return BlockedStringSurfaceMatches{}, err
    
  • }
  • sort.Strings(fileMatches)
  • sort.Strings(commitMatches)
  • sort.Strings(refMatches)
  • sort.Strings(pathMatches)
  • return BlockedStringSurfaceMatches{
  •   BlockedString:  blocked,
    
  •   FileContents:   fileMatches,
    
  •   CommitMessages: commitMatches,
    
  •   Refs:           refMatches,
    
  •   Paths:          pathMatches,
    
  • }, nil
    +}

+// ValidateBlockedStringCoverageDetectedE verifies blocked-string detection covers all surfaces.
+func ValidateBlockedStringCoverageDetectedE(repoPath, blockedString string) error {

  • matches, err := ScanBlockedStringSurfacesE(repoPath, blockedString)
  • if err != nil {
  •   return err
    
  • }
  • missing := matches.MissingSurfaces()
  • if len(missing) > 0 {
  •   return fmt.Errorf(
    
  •   	"blocked string %q missing expected surfaces: %s",
    
  •   	blockedString,
    
  •   	strings.Join(missing, ", "),
    
  •   )
    
  • }
  • return nil
    +}

+// ValidateBlockedStringCoverageRemovedE verifies blocked-string matches are gone from all surfaces.
+func ValidateBlockedStringCoverageRemovedE(repoPath, blockedString string) error {

  • matches, err := ScanBlockedStringSurfacesE(repoPath, blockedString)
  • if err != nil {
  •   return err
    
  • }
  • if matches.IsClean() {
  •   return nil
    
  • }
  • return fmt.Errorf("blocked string %q still present: %s", blockedString, summarizeBlockedMatches(matches))
    +}

+// AssertBlockedStringCoverageDetected fails the test when any required surface is missing.
+func AssertBlockedStringCoverageDetected(t *testing.T, repoPath, blockedString string) BlockedStringSurfaceMatches {

  • t.Helper()
  • matches, err := ScanBlockedStringSurfacesE(repoPath, blockedString)
  • if err != nil {
  •   t.Fatalf("scan blocked-string surfaces: %v", err)
    
  • }
  • if missing := matches.MissingSurfaces(); len(missing) > 0 {
  •   t.Fatalf("blocked string %q missing expected surfaces: %s", blockedString, strings.Join(missing, ", "))
    
  • }
  • return matches
    +}

+// AssertBlockedStringCoverageRemoved fails the test when any blocked-string matches remain.
+func AssertBlockedStringCoverageRemoved(t *testing.T, repoPath, blockedString string) {

  • t.Helper()
  • matches, err := ScanBlockedStringSurfacesE(repoPath, blockedString)
  • if err != nil {
  •   t.Fatalf("scan blocked-string surfaces: %v", err)
    
  • }
  • if !matches.IsClean() {
  •   t.Fatalf("blocked string %q still present: %s", blockedString, summarizeBlockedMatches(matches))
    
  • }
    +}

+func validateBlockedRefPathToken(token string) error {

  • if token == "" {
  •   return fmt.Errorf("value cannot be empty")
    
  • }
  • if !blockedRefPathTokenPattern.MatchString(token) {
  •   return fmt.Errorf("must contain only letters, numbers, dot, underscore, or dash")
    
  • }
  • if strings.HasPrefix(token, "-") {
  •   return fmt.Errorf("cannot start with hyphen")
    
  • }
  • if strings.HasPrefix(token, ".") || strings.HasSuffix(token, ".") {
  •   return fmt.Errorf("cannot start or end with dot")
    
  • }
  • if strings.HasSuffix(token, ".lock") {
  •   return fmt.Errorf("cannot end with .lock")
    
  • }
  • if strings.Contains(token, "..") {
  •   return fmt.Errorf("cannot contain consecutive dots")
    
  • }
  • return nil
    +}

+func writeFixtureFile(repoPath, relPath, content string) error {

  • absPath := filepath.Join(repoPath, filepath.FromSlash(relPath))
  • if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
  •   return fmt.Errorf("failed to create directory for %s: %w", relPath, err)
    
  • }
  • if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
  •   return fmt.Errorf("failed to write file %s: %w", relPath, err)
    
  • }
  • return nil
    +}

+func listAllCommits(repoPath string) ([]string, error) {

  • output, err := RunGitCmdE(repoPath, "rev-list", "--all")
  • if err != nil {
  •   return nil, err
    
  • }
  • return splitNonEmptyLines(output), nil
    +}

+func collectBlockedFileContentMatches(repoPath string, commits []string, blocked string) ([]string, error) {

  • matchSet := make(map[string]struct{})
  • for _, commit := range commits {
  •   lines, err := runGitGrepForCommit(repoPath, commit, blocked)
    
  •   if err != nil {
    
  •   	return nil, err
    
  •   }
    
  •   for _, line := range lines {
    
  •   	matchSet[line] = struct{}{}
    
  •   }
    
  • }
  • return sortedKeys(matchSet), nil
    +}

+func runGitGrepForCommit(repoPath, commit, blocked string) ([]string, error) {

  • cmd := exec.Command("git", "grep", "-n", "-I", "-F", "-e", blocked, commit, "--")
  • cmd.Dir = repoPath
  • var stderr bytes.Buffer
  • cmd.Stderr = &stderr
  • output, err := cmd.Output()
  • if err != nil {
  •   if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
    
  •   	return nil, nil
    
  •   }
    
  •   return nil, fmt.Errorf(
    
  •   	"git command failed: git %v\nStdout: %s\nStderr: %s\nError: %w",
    
  •   	[]string{"grep", "-n", "-I", "-F", "-e", blocked, commit, "--"},
    
  •   	strings.TrimSpace(string(output)),
    
  •   	strings.TrimSpace(stderr.String()),
    
  •   	err,
    
  •   )
    
  • }
  • return splitNonEmptyLines(string(output)), nil
    +}

+func collectBlockedCommitMessageMatches(repoPath, blocked string) ([]string, error) {

  • output, err := RunGitCmdE(repoPath, "log", "--all", "--format=%H%x09%B%x00")
  • if err != nil {
  •   return nil, err
    
  • }
  • var matches []string
  • for _, entry := range strings.Split(output, "\x00") {
  •   entry = strings.TrimSpace(entry)
    
  •   if entry == "" {
    
  •   	continue
    
  •   }
    
  •   sha, message, ok := strings.Cut(entry, "\t")
    
  •   if !ok {
    
  •   	continue
    
  •   }
    
  •   if strings.Contains(message, blocked) {
    
  •   	firstLine := firstNonEmptyLine(message)
    
  •   	if firstLine == "" {
    
  •   		firstLine = "<empty-message>"
    
  •   	}
    
  •   	matches = append(matches, sha+"\t"+firstLine)
    
  •   }
    
  • }
  • return matches, nil
    +}

+func collectBlockedRefMatches(repoPath, blocked string) ([]string, error) {

  • output, err := RunGitCmdE(repoPath, "for-each-ref", "--format=%(refname)")
  • if err != nil {
  •   return nil, err
    
  • }
  • var matches []string
  • for _, line := range splitNonEmptyLines(output) {
  •   if strings.Contains(line, blocked) {
    
  •   	matches = append(matches, line)
    
  •   }
    
  • }
  • return matches, nil
    +}

+func collectBlockedPathMatches(repoPath, blocked string) ([]string, error) {

  • output, err := RunGitCmdE(repoPath, "rev-list", "--all", "--objects")
  • if err != nil {
  •   return nil, err
    
  • }
  • matchSet := make(map[string]struct{})
  • for _, line := range splitNonEmptyLines(output) {
  •   _, path, ok := strings.Cut(line, " ")
    
  •   if !ok {
    
  •   	continue
    
  •   }
    
  •   path = strings.TrimSpace(path)
    
  •   if path == "" {
    
  •   	continue
    
  •   }
    
  •   if strings.Contains(path, blocked) {
    
  •   	matchSet[path] = struct{}{}
    
  •   }
    
  • }
  • return sortedKeys(matchSet), nil
    +}

+func summarizeBlockedMatches(matches BlockedStringSurfaceMatches) string {

  • var parts []string
  • if len(matches.FileContents) > 0 {
  •   parts = append(parts, fmt.Sprintf("file-contents=%d [%s]", len(matches.FileContents), sampleEntries(matches.FileContents)))
    
  • }
  • if len(matches.CommitMessages) > 0 {
  •   parts = append(parts, fmt.Sprintf("commit-messages=%d [%s]", len(matches.CommitMessages), sampleEntries(matches.CommitMessages)))
    
  • }
  • if len(matches.Refs) > 0 {
  •   parts = append(parts, fmt.Sprintf("refs=%d [%s]", len(matches.Refs), sampleEntries(matches.Refs)))
    
  • }
  • if len(matches.Paths) > 0 {
  •   parts = append(parts, fmt.Sprintf("paths=%d [%s]", len(matches.Paths), sampleEntries(matches.Paths)))
    
  • }
  • return strings.Join(parts, "; ")
    +}

+func sampleEntries(values []string) string {

  • const max = 3
  • if len(values) == 0 {
  •   return ""
    
  • }
  • limit := max
  • if len(values) < limit {
  •   limit = len(values)
    
  • }
  • sample := strings.Join(values[:limit], ", ")
  • if len(values) > limit {
  •   return sample + ", ..."
    
  • }
  • return sample
    +}

+func firstNonEmptyLine(text string) string {

  • for _, line := range strings.Split(text, "\n") {
  •   line = strings.TrimSpace(line)
    
  •   if line != "" {
    
  •   	return line
    
  •   }
    
  • }
  • return ""
    +}

+func splitNonEmptyLines(output string) []string {

  • rawLines := strings.Split(output, "\n")
  • lines := make([]string, 0, len(rawLines))
  • for _, line := range rawLines {
  •   line = strings.TrimSpace(line)
    
  •   if line != "" {
    
  •   	lines = append(lines, line)
    
  •   }
    
  • }
  • return lines
    +}

+func sortedKeys(set map[string]struct{}) []string {

  • items := make([]string, 0, len(set))
  • for key := range set {
  •   items = append(items, key)
    
  • }
  • sort.Strings(items)
  • return items
    +}

diff --git a/sanitize_fixtures_test.go b/sanitize_fixtures_test.go
new file mode 100644
--- /dev/null
+++ b/sanitize_fixtures_test.go
@@ -1,0 +1,112 @@
+package testutil_test
+
+import (

  • "os"
  • "path/filepath"
  • "strings"
  • "testing"
  • testutil "github.com/git-fire/git-testkit"
    +)

+func TestCreateRewriteValidationFixtureAndCoverageDetection(t *testing.T) {

  • fixture := testutil.CreateRewriteValidationFixture(t, testutil.RewriteValidationFixtureOptions{
  •   Name:          "rewrite-detection",
    
  •   BlockedString: "blockedtoken",
    
  • })
  • matches := testutil.AssertBlockedStringCoverageDetected(t, fixture.RepoPath, fixture.BlockedString)
  • if len(matches.FileContents) == 0 {
  •   t.Fatal("expected blocked string in file contents")
    
  • }
  • if len(matches.CommitMessages) == 0 {
  •   t.Fatal("expected blocked string in commit messages")
    
  • }
  • if len(matches.Refs) == 0 {
  •   t.Fatal("expected blocked string in refs")
    
  • }
  • if len(matches.Paths) == 0 {
  •   t.Fatal("expected blocked string in paths")
    
  • }
  • if !containsSubstring(matches.Refs, fixture.BranchWithBlocked) {
  •   t.Fatalf("expected blocked branch ref %q in scan results", fixture.BranchWithBlocked)
    
  • }
  • if !containsSubstring(matches.Refs, fixture.TagWithBlocked) {
  •   t.Fatalf("expected blocked tag ref %q in scan results", fixture.TagWithBlocked)
    
  • }
  • if !containsString(matches.Paths, fixture.PathWithBlocked) {
  •   t.Fatalf("expected blocked path %q in scan results", fixture.PathWithBlocked)
    
  • }
    +}

+func TestAssertBlockedStringCoverageRemovedAfterHistoryRewrite(t *testing.T) {

  • fixture := testutil.CreateRewriteValidationFixture(t, testutil.RewriteValidationFixtureOptions{
  •   Name:          "rewrite-clean",
    
  •   BlockedString: "leaktoken",
    
  • })
  • repoPath := fixture.RepoPath
  • testutil.RunGitCmd(t, repoPath, "checkout", "--orphan", "sanitized-history")
  • testutil.RunGitCmd(t, repoPath, "rm", "-rf", "--", ".")
  • cleanFile := filepath.Join(repoPath, "sanitized.txt")
  • if err := os.WriteFile(cleanFile, []byte("sanitized fixture\n"), 0644); err != nil {
  •   t.Fatalf("failed writing clean file: %v", err)
    
  • }
  • testutil.RunGitCmd(t, repoPath, "add", "--", "sanitized.txt")
  • testutil.RunGitCmd(t, repoPath, "commit", "-m", "sanitize fixture history")
  • testutil.RunGitCmd(t, repoPath, "branch", "-D", fixture.DefaultBranch)
  • testutil.RunGitCmd(t, repoPath, "branch", "-m", fixture.DefaultBranch)
  • testutil.RunGitCmd(t, repoPath, "branch", "-D", fixture.BranchWithBlocked)
  • testutil.RunGitCmd(t, repoPath, "tag", "-d", fixture.TagWithBlocked)
  • testutil.AssertBlockedStringCoverageRemoved(t, repoPath, fixture.BlockedString)
  • if err := testutil.ValidateBlockedStringCoverageRemovedE(repoPath, fixture.BlockedString); err != nil {
  •   t.Fatalf("expected removed coverage validation to pass: %v", err)
    
  • }
    +}

+func TestCreateRewriteValidationFixtureInDir_RejectsInvalidBlockedString(t *testing.T) {

  • _, err := testutil.CreateRewriteValidationFixtureInDir(t.TempDir(), testutil.RewriteValidationFixtureOptions{
  •   Name:          "bad-blocked",
    
  •   BlockedString: "blocked token with spaces",
    
  • })
  • if err == nil {
  •   t.Fatal("expected invalid blocked string to be rejected")
    
  • }
    +}

+func TestValidateBlockedStringCoverageDetectedE_FailsWhenCoverageMissing(t *testing.T) {

  • repoPath := testutil.CreateTestRepo(t, testutil.RepoOptions{Name: "missing-coverage"})
  • err := testutil.ValidateBlockedStringCoverageDetectedE(repoPath, "blockedtoken")
  • if err == nil {
  •   t.Fatal("expected missing coverage validation to fail")
    
  • }
  • if !strings.Contains(err.Error(), "missing expected surfaces") {
  •   t.Fatalf("expected missing coverage error, got: %v", err)
    
  • }
    +}

+func containsString(values []string, target string) bool {

  • for _, value := range values {
  •   if value == target {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

+func containsSubstring(values []string, target string) bool {

  • for _, value := range values {
  •   if strings.Contains(value, target) {
    
  •   	return true
    
  •   }
    
  • }
  • return false
    +}

</details>


<sub>You can send follow-ups to the cloud agent <a href="https://cursor.com/agents/bc-fb1fd048-5e3c-4dc6-9d0b-638f1be21801">here</a>.</sub>
<!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_END -->

<sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7ba5e52ea58db8753bacb9bffb50b6004ea24089. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>

Comment thread sanitize_fixtures.go
Comment thread sanitize_fixtures.go
Stop prepending the commit hash to git grep lines, which already use
commit:path:line:content. Reject blocked strings that start with a hyphen
so they cannot be misparsed as git grep flags, and pass patterns with -e
for correct argument handling.

Co-authored-by: Ben Schellenberger <TBRX103@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
sanitize_fixtures_test.go (1)

74-82: Consider adding explicit test for leading-hyphen rejection.

The PR commits mention "rejecting blocked strings that start with a hyphen (to avoid being parsed as git grep flags)" as a hardening measure. This test covers spaces (invalid characters), but an explicit test for leading hyphen would ensure this security-relevant validation remains intact.

🧪 Suggested additional test case
func TestCreateRewriteValidationFixtureInDir_RejectsLeadingHyphen(t *testing.T) {
	_, err := testutil.CreateRewriteValidationFixtureInDir(t.TempDir(), testutil.RewriteValidationFixtureOptions{
		Name:          "hyphen-blocked",
		BlockedString: "-blockedtoken",
	})
	if err == nil {
		t.Fatal("expected leading-hyphen blocked string to be rejected")
	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sanitize_fixtures_test.go` around lines 74 - 82, Add an explicit unit test
that asserts CreateRewriteValidationFixtureInDir rejects blocked strings that
begin with a hyphen: create a new test function (e.g.,
TestCreateRewriteValidationFixtureInDir_RejectsLeadingHyphen) that calls
testutil.CreateRewriteValidationFixtureInDir with
RewriteValidationFixtureOptions{ Name: "hyphen-blocked", BlockedString:
"-blockedtoken" } and fails the test if err == nil; this ensures the
leading-hyphen validation added to CreateRewriteValidationFixtureInDir is
covered and won't regress.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@sanitize_fixtures_test.go`:
- Around line 74-82: Add an explicit unit test that asserts
CreateRewriteValidationFixtureInDir rejects blocked strings that begin with a
hyphen: create a new test function (e.g.,
TestCreateRewriteValidationFixtureInDir_RejectsLeadingHyphen) that calls
testutil.CreateRewriteValidationFixtureInDir with
RewriteValidationFixtureOptions{ Name: "hyphen-blocked", BlockedString:
"-blockedtoken" } and fails the test if err == nil; this ensures the
leading-hyphen validation added to CreateRewriteValidationFixtureInDir is
covered and won't regress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c3a0a25-8f33-4848-a685-67ba6316801e

📥 Commits

Reviewing files that changed from the base of the PR and between ad81ae5 and 47551d1.

📒 Files selected for processing (5)
  • DEVELOPER_GUIDE.md
  • README.md
  • doc.go
  • sanitize_fixtures.go
  • sanitize_fixtures_test.go

@bschellenberger2600
Copy link
Copy Markdown
Member Author

@cursoragent address the nit feedback from coderabbit Verify each finding against the current code and only fix it if needed.

In @sanitize_fixtures_test.go around lines 74 - 82, Add an explicit unit test
that asserts CreateRewriteValidationFixtureInDir rejects blocked strings that
begin with a hyphen: create a new test function (e.g.,
TestCreateRewriteValidationFixtureInDir_RejectsLeadingHyphen) that calls
testutil.CreateRewriteValidationFixtureInDir with
RewriteValidationFixtureOptions{ Name: "hyphen-blocked", BlockedString:
"-blockedtoken" } and fails the test if err == nil; this ensures the
leading-hyphen validation added to CreateRewriteValidationFixtureInDir is
covered and won't regress.

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 16, 2026

Verification: CreateRewriteValidationFixtureInDir already routes the blocked string through validateBlockedRefPathToken, which returns cannot start with hyphen when strings.HasPrefix(token, "-") (see sanitize_fixtures.go around 259–268). The CodeRabbit item was about coverage, not a missing rule.

Change: Added TestCreateRewriteValidationFixtureInDir_RejectsLeadingHyphen next to the existing invalid-token test so a leading hyphen cannot regress without failing CI.

func TestCreateRewriteValidationFixtureInDir_RejectsLeadingHyphen(t *testing.T) {
	_, err := testutil.CreateRewriteValidationFixtureInDir(t.TempDir(), testutil.RewriteValidationFixtureOptions{
		Name:          "hyphen-blocked",
		BlockedString: "-blockedtoken",
	})
	if err == nil {
		t.Fatal("expected leading-hyphen blocked string to be rejected")
	}
}

Checks: gofmt, go vet ./..., and go test ./... -run 'TestCreateRewriteValidationFixtureInDir_Rejects' all succeeded. Updates are committed, pushed to feat/gmt-watch-sanitize-fixtures, and PR #8 was updated.

Open in Web Open in Cursor 

Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
@bschellenberger2600 bschellenberger2600 merged commit fc792bb into main Apr 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants