diff --git a/.gitignore b/.gitignore index 4658e73..df82e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ # Local dev files /go.work /go.work.sum + +# Claude Code local settings +.claude/settings.local.json diff --git a/build/ci/dangerfile.go b/build/ci/dangerfile.go index 42bc6a3..706598f 100644 --- a/build/ci/dangerfile.go +++ b/build/ci/dangerfile.go @@ -8,6 +8,6 @@ import ( // Run is invoked by danger-go func Run(d *danger.T, pr danger.DSL) { - d.Message(fmt.Sprintf("%d new files added!", len(pr.Git.CreateFiles)), "", 0) - d.Message(fmt.Sprintf("%d files modified!", len(pr.Git.ModifiedFiles)), "", 0) + d.Message(fmt.Sprintf("%d new files added!", len(pr.Git.GetCreatedFiles())), "", 0) + d.Message(fmt.Sprintf("%d files modified!", len(pr.Git.GetModifiedFiles())), "", 0) } diff --git a/cmd/danger-go/runner/runner.go b/cmd/danger-go/runner/runner.go index 401e168..23a2b8c 100644 --- a/cmd/danger-go/runner/runner.go +++ b/cmd/danger-go/runner/runner.go @@ -38,7 +38,7 @@ func Run() { } var jsonData struct { - Danger dangerJs.DSL `json:"danger"` + Danger dangerJs.DSLData `json:"danger"` } err = json.Unmarshal(jsonBytes, &jsonData) if err != nil { @@ -62,7 +62,7 @@ func Run() { } d := danger.New() - fn(d, jsonData.Danger) + fn(d, jsonData.Danger.ToInterface()) respJSON, err := d.Results() if err != nil { log.Fatalf("marshalling response: %s", err.Error()) diff --git a/danger-js/danger-js.go b/danger-js/danger-js.go index 9ede2c7..118d518 100644 --- a/danger-js/danger-js.go +++ b/danger-js/danger-js.go @@ -37,11 +37,11 @@ func GetPR(url string, dangerBin string) (DSL, error) { return DSL{}, fmt.Errorf("could not download DSL JSON with danger-js: %w", err) } - var pr DSL - if err = json.Unmarshal(prJSON, &pr); err != nil { + var prData DSLData + if err = json.Unmarshal(prJSON, &prData); err != nil { return DSL{}, err } - return pr, nil + return prData.ToInterface(), nil } func Process(command string, args []string) error { diff --git a/danger-js/types_danger.go b/danger-js/types_danger.go index 05a3ad9..e9f7929 100644 --- a/danger-js/types_danger.go +++ b/danger-js/types_danger.go @@ -1,24 +1,282 @@ package dangerJs +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +var ( + // Compiled regex patterns for diff parsing + addedLineRe = regexp.MustCompile(`^\+([^+].*|$)`) + removedLineRe = regexp.MustCompile(`^-([^-].*|$)`) + hunkHeaderRe = regexp.MustCompile(`^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@`) + + // Shell metacharacters that could be used for command injection + shellMetaChars = []string{";", "|", "&", "$", "`", "(", ")", "{", "}", "[", "]", "*", "?", "<", ">", "'", "\""} + + // Whitespace characters that should be rejected in git refs + whitespaceChars = []string{" ", "\t", "\n", "\r"} +) + +type GitHub interface { + GetIssue() GitHubIssue + GetPR() GitHubPR + GetThisPR() GitHubAPIPR + GetCommits() []GitHubCommit + GetReviews() []GitHubReview + GetRequestedReviewers() GitHubReviewers +} + +type GitLab interface { + GetMetadata() RepoMetaData + GetMR() GitLabMR + GetCommits() []GitLabMRCommit + GetApprovals() GitLabApproval +} + +type Settings interface { + GetGitHubAccessToken() string + GetGitHubBaseURL() string + GetGitHubAdditionalHeaders() any + GetCLIArgs() CLIArgs +} + +type Git interface { + GetModifiedFiles() []FilePath + GetCreatedFiles() []FilePath + GetDeletedFiles() []FilePath + GetCommits() []GitCommit + DiffForFile(filePath string) (FileDiff, error) + DiffForFileWithRefs(filePath, baseRef, headRef string) (FileDiff, error) +} + +// DSL is the main Danger context, with all fields as interfaces for testability. type DSL struct { - Git Git `json:"git"` - GitHub GitHub `json:"github,omitempty"` - GitLab GitLab `json:"gitlab,omitempty"` - // TODO: bitbucket_server - // TODO: bitbucket_cloud + Git Git `json:"git"` + GitHub GitHub `json:"github,omitempty"` + GitLab GitLab `json:"gitlab,omitempty"` Settings Settings `json:"settings"` } type FilePath = string -type Git struct { +// gitImpl is the internal implementation of the Git interface +type gitImpl struct { ModifiedFiles []FilePath `json:"modified_files"` - CreateFiles []FilePath `json:"created_files"` + CreatedFiles []FilePath `json:"created_files"` DeletedFiles []FilePath `json:"deleted_files"` Commits []GitCommit `json:"commits"` } -type Settings struct { +func (g gitImpl) GetModifiedFiles() []FilePath { + return g.ModifiedFiles +} + +func (g gitImpl) GetCreatedFiles() []FilePath { + return g.CreatedFiles +} + +func (g gitImpl) GetDeletedFiles() []FilePath { + return g.DeletedFiles +} + +func (g gitImpl) GetCommits() []GitCommit { + return g.Commits +} + +// FileDiff represents the changes in a file. +type FileDiff struct { + AddedLines []DiffLine + RemovedLines []DiffLine +} + +// DiffLine represents a single line in a file diff. +type DiffLine struct { + Content string + Line int +} + +// DiffForFile executes a git diff command for a specific file and parses its output. +// Uses HEAD^ and HEAD as the base and head references by default. +func (g gitImpl) DiffForFile(filePath string) (FileDiff, error) { + return g.DiffForFileWithRefs(filePath, "HEAD^", "HEAD") +} + +// validateFilePath validates that the file path doesn't contain dangerous characters +func validateFilePath(path string) bool { + // Empty paths are invalid + if path == "" { + return false + } + + // Clean the path and check for dangerous patterns + cleaned := filepath.Clean(path) + + // Reject paths that try to escape the repository + if strings.Contains(cleaned, "..") { + return false + } + + // Reject absolute paths as they could access files outside the repository + if filepath.IsAbs(cleaned) { + return false + } + + // Reject paths with shell metacharacters that could be used for command injection + for _, char := range shellMetaChars { + if strings.Contains(path, char) { + return false + } + } + + // Reject paths with whitespace characters that could cause issues in shell commands + for _, char := range whitespaceChars { + if strings.Contains(path, char) { + return false + } + } + + return true +} + +// validateGitRef validates that the git ref name doesn't contain dangerous characters +func validateGitRef(ref string) bool { + // Git refs must not contain certain characters and must not be empty + if ref == "" { + return false + } + // Disallow shell metacharacters + for _, char := range shellMetaChars { + if strings.Contains(ref, char) { + return false + } + } + // Disallow whitespace characters + for _, char := range whitespaceChars { + if strings.Contains(ref, char) { + return false + } + } + // Disallow path traversal + if strings.Contains(ref, "..") { + return false + } + // Disallow slashes at the start or end, or consecutive slashes + if strings.HasPrefix(ref, "/") || strings.HasSuffix(ref, "/") || strings.Contains(ref, "//") { + return false + } + // Disallow ref names with ASCII control characters + for _, r := range ref { + if r < 32 || r == 127 { + return false + } + } + return true +} + +// DiffForFileWithRefs executes a git diff command for a specific file with configurable references. +func (g gitImpl) DiffForFileWithRefs(filePath, baseRef, headRef string) (FileDiff, error) { + // Validate file path to prevent command injection + if !validateFilePath(filePath) { + return FileDiff{}, fmt.Errorf("invalid file path: %s", filePath) + } + // Validate baseRef and headRef to prevent command injection + if !validateGitRef(baseRef) { + return FileDiff{}, fmt.Errorf("invalid base ref: %s", baseRef) + } + if !validateGitRef(headRef) { + return FileDiff{}, fmt.Errorf("invalid head ref: %s", headRef) + } + + cmd := exec.Command("git", "diff", "--unified=0", baseRef, headRef, filePath) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return FileDiff{}, err + } + + return parseDiffContent(out.String()), nil +} + +// parseHunkHeader extracts line number information from a hunk header +func parseHunkHeader(line string) (removedStart, addedStart int, isHunkHeader bool) { + if matches := hunkHeaderRe.FindStringSubmatch(line); len(matches) > 3 { + var err error + removedStart, err = strconv.Atoi(matches[1]) + if err != nil { + return 0, 0, false + } + addedStart, err = strconv.Atoi(matches[3]) + if err != nil { + return 0, 0, false + } + return removedStart, addedStart, true + } + return 0, 0, false +} + +// parseAddedLine extracts content from an added line and returns whether it's an added line +func parseAddedLine(line string) (content string, isAdded bool) { + if matches := addedLineRe.FindStringSubmatch(line); len(matches) > 1 { + return matches[1], true + } + return "", false +} + +// parseRemovedLine extracts content from a removed line and returns whether it's a removed line +func parseRemovedLine(line string) (content string, isRemoved bool) { + if matches := removedLineRe.FindStringSubmatch(line); len(matches) > 1 { + return matches[1], true + } + return "", false +} + +// parseDiffContent parses git diff output and extracts added and removed lines with line numbers +func parseDiffContent(diffContent string) FileDiff { + var fileDiff FileDiff + + lines := strings.Split(diffContent, "\n") + // Initialize line numbers to -1 to indicate no hunk header has been found yet + currentRemovedLine := -1 + currentAddedLine := -1 + + for _, line := range lines { + // Check for hunk header to track line numbers + if removedStart, addedStart, isHunk := parseHunkHeader(line); isHunk { + currentRemovedLine = removedStart + currentAddedLine = addedStart + } else if content, isAdded := parseAddedLine(line); isAdded { + // Only add line if we have a valid line number from a hunk header + if currentAddedLine >= 0 { + fileDiff.AddedLines = append(fileDiff.AddedLines, DiffLine{ + Content: content, + Line: currentAddedLine, + }) + currentAddedLine++ + } + } else if content, isRemoved := parseRemovedLine(line); isRemoved { + // Only add line if we have a valid line number from a hunk header + if currentRemovedLine >= 0 { + fileDiff.RemovedLines = append(fileDiff.RemovedLines, DiffLine{ + Content: content, + Line: currentRemovedLine, + }) + currentRemovedLine++ + } + } + } + + return fileDiff +} + +// settingsImpl is the internal implementation of the Settings interface +type settingsImpl struct { GitHub struct { AccessToken string `json:"accessToken"` BaseURL string `json:"baseURL"` @@ -27,6 +285,99 @@ type Settings struct { CLIArgs CLIArgs `json:"cliArgs"` } +// GetGitHubAccessToken returns the GitHub access token +func (s settingsImpl) GetGitHubAccessToken() string { + return s.GitHub.AccessToken +} + +func (s settingsImpl) GetGitHubBaseURL() string { + return s.GitHub.BaseURL +} + +func (s settingsImpl) GetGitHubAdditionalHeaders() any { + return s.GitHub.AdditionalHeaders +} + +func (s settingsImpl) GetCLIArgs() CLIArgs { + return s.CLIArgs +} + +// gitHubImpl is the internal implementation of the GitHub interface +type gitHubImpl struct { + Issue GitHubIssue `json:"issue"` + PR GitHubPR `json:"pr"` + ThisPR GitHubAPIPR `json:"thisPR"` + Commits []GitHubCommit `json:"commits"` + Reviews []GitHubReview `json:"reviews"` + RequestedReviewers GitHubReviewers `json:"requested_reviewers"` +} + +func (g gitHubImpl) GetIssue() GitHubIssue { + return g.Issue +} + +func (g gitHubImpl) GetPR() GitHubPR { + return g.PR +} + +func (g gitHubImpl) GetThisPR() GitHubAPIPR { + return g.ThisPR +} + +func (g gitHubImpl) GetCommits() []GitHubCommit { + return g.Commits +} + +func (g gitHubImpl) GetReviews() []GitHubReview { + return g.Reviews +} + +func (g gitHubImpl) GetRequestedReviewers() GitHubReviewers { + return g.RequestedReviewers +} + +// gitLabImpl is the internal implementation of the GitLab interface +type gitLabImpl struct { + Metadata RepoMetaData `json:"Metadata"` + MR GitLabMR `json:"mr"` + Commits []GitLabMRCommit `json:"commits"` + Approvals GitLabApproval `json:"approvals"` +} + +func (g gitLabImpl) GetMetadata() RepoMetaData { + return g.Metadata +} + +func (g gitLabImpl) GetMR() GitLabMR { + return g.MR +} + +func (g gitLabImpl) GetCommits() []GitLabMRCommit { + return g.Commits +} + +func (g gitLabImpl) GetApprovals() GitLabApproval { + return g.Approvals +} + +// DSLData is used for JSON unmarshaling, with concrete types +type DSLData struct { + Git gitImpl `json:"git"` + GitHub gitHubImpl `json:"github,omitempty"` + GitLab gitLabImpl `json:"gitlab,omitempty"` + Settings settingsImpl `json:"settings"` +} + +// ToInterface converts DSLData to DSL with interfaces +func (d DSLData) ToInterface() DSL { + return DSL{ + Git: d.Git, + GitHub: d.GitHub, + GitLab: d.GitLab, + Settings: d.Settings, + } +} + type CLIArgs struct { Base string `json:"base"` Verbose string `json:"verbose"` diff --git a/danger-js/types_danger_test.go b/danger-js/types_danger_test.go new file mode 100644 index 0000000..0595f40 --- /dev/null +++ b/danger-js/types_danger_test.go @@ -0,0 +1,369 @@ +package dangerJs + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Note: parseDiffContent is now a shared function in types_danger.go + +func TestParseDiffContent(t *testing.T) { + tests := []struct { + name string + gitDiffOutput string + wantFileDiff FileDiff + }{ + { + name: "basic added and removed lines", + gitDiffOutput: `diff --git a/test.go b/test.go +index 123..456 100644 +--- a/test.go ++++ b/test.go +@@ -1 +1 @@ +-func oldFunction() { ++func newFunction() { +@@ -5 +5,2 @@ ++ return "added line" +- fmt.Println("removed line")`, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "func newFunction() {", Line: 1}, + {Content: "\treturn \"added line\"", Line: 5}, + }, + RemovedLines: []DiffLine{ + {Content: "func oldFunction() {", Line: 1}, + {Content: "\tfmt.Println(\"removed line\")", Line: 5}, + }, + }, + }, + { + name: "only added lines", + gitDiffOutput: `diff --git a/new.go b/new.go +index 123..456 100644 +--- a/new.go ++++ b/new.go +@@ -1,0 +1,3 @@ ++package main ++ ++func main() {}`, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "package main", Line: 1}, + {Content: "", Line: 2}, + {Content: "func main() {}", Line: 3}, + }, + RemovedLines: nil, + }, + }, + { + name: "only removed lines", + gitDiffOutput: `diff --git a/old.go b/old.go +index 123..456 100644 +--- a/old.go ++++ b/old.go +@@ -1,3 +0,0 @@ +-package main +- +-func old() {}`, + wantFileDiff: FileDiff{ + AddedLines: nil, + RemovedLines: []DiffLine{ + {Content: "package main", Line: 1}, + {Content: "", Line: 2}, + {Content: "func old() {}", Line: 3}, + }, + }, + }, + { + name: "no changes", + gitDiffOutput: ``, + wantFileDiff: FileDiff{ + AddedLines: nil, + RemovedLines: nil, + }, + }, + { + name: "complex diff with context lines", + gitDiffOutput: `diff --git a/complex.go b/complex.go +index 123..456 100644 +--- a/complex.go ++++ b/complex.go +@@ -10,5 +10,6 @@ + unchanged line 1 + unchanged line 2 +- old implementation ++ new implementation ++ additional line + unchanged line 3`, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "\tnew implementation", Line: 10}, + {Content: "\tadditional line", Line: 11}, + }, + RemovedLines: []DiffLine{ + {Content: "\told implementation", Line: 10}, + }, + }, + }, + { + name: "lines with special characters and whitespace", + gitDiffOutput: `diff --git a/special.go b/special.go +index 123..456 100644 +--- a/special.go ++++ b/special.go +@@ -1,2 +1,2 @@ +- fmt.Printf("Hello %s\n", name) ++ fmt.Printf("Hi %s!\n", name)`, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "\tfmt.Printf(\"Hi %s!\\n\", name)", Line: 1}, + }, + RemovedLines: []DiffLine{ + {Content: "\tfmt.Printf(\"Hello %s\\n\", name)", Line: 1}, + }, + }, + }, + { + name: "lines with only symbols", + gitDiffOutput: `diff --git a/symbols.go b/symbols.go +index 123..456 100644 +--- a/symbols.go ++++ b/symbols.go +@@ -1,2 +1,2 @@ +-} ++},`, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "},", Line: 1}, + }, + RemovedLines: []DiffLine{ + {Content: "}", Line: 1}, + }, + }, + }, + { + name: "empty added and removed lines", + gitDiffOutput: `diff --git a/empty.go b/empty.go +index 123..456 100644 +--- a/empty.go ++++ b/empty.go +@@ -1,2 +1,2 @@ +- ++ +- ++ `, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "", Line: 1}, + {Content: " ", Line: 2}, + }, + RemovedLines: []DiffLine{ + {Content: "", Line: 1}, + {Content: " ", Line: 2}, + }, + }, + }, + { + name: "diff with file headers only", + gitDiffOutput: `diff --git a/test.go b/test.go +index 123..456 100644 +--- a/test.go ++++ b/test.go`, + wantFileDiff: FileDiff{ + AddedLines: nil, + RemovedLines: nil, + }, + }, + { + name: "multiple hunks with mixed changes", + gitDiffOutput: `diff --git a/multi.go b/multi.go +index 123..456 100644 +--- a/multi.go ++++ b/multi.go +@@ -1,3 +1,4 @@ + package main + ++import "fmt" + func main() { +@@ -10,6 +11,7 @@ + if true { +- fmt.Println("old") ++ fmt.Println("new") ++ fmt.Println("extra") + } + }`, + wantFileDiff: FileDiff{ + AddedLines: []DiffLine{ + {Content: "import \"fmt\"", Line: 1}, + {Content: "\t\tfmt.Println(\"new\")", Line: 11}, + {Content: "\t\tfmt.Println(\"extra\")", Line: 12}, + }, + RemovedLines: []DiffLine{ + {Content: "\t\tfmt.Println(\"old\")", Line: 10}, + }, + }, + }, + { + name: "malformed diff without hunk headers", + gitDiffOutput: `diff --git a/bad.go b/bad.go +index 123..456 100644 +--- a/bad.go ++++ b/bad.go ++added line without hunk header +-removed line without hunk header`, + wantFileDiff: FileDiff{ + AddedLines: nil, // Should be empty since no valid hunk header + RemovedLines: nil, // Should be empty since no valid hunk header + }, + }, + { + name: "malformed hunk header", + gitDiffOutput: `diff --git a/bad.go b/bad.go +index 123..456 100644 +--- a/bad.go ++++ b/bad.go +@@ invalid hunk header @@ ++added line after invalid header +-removed line after invalid header`, + wantFileDiff: FileDiff{ + AddedLines: nil, // Should be empty since hunk header is invalid + RemovedLines: nil, // Should be empty since hunk header is invalid + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFileDiff := parseDiffContent(tt.gitDiffOutput) + require.Equal(t, tt.wantFileDiff, gotFileDiff) + }) + } +} + +func TestValidateFilePath(t *testing.T) { + tests := []struct { + name string + path string + wantValid bool + }{ + { + name: "valid relative path", + path: "src/main.go", + wantValid: true, + }, + { + name: "empty path", + path: "", + wantValid: false, + }, + { + name: "path traversal attempt", + path: "../../../etc/passwd", + wantValid: false, + }, + { + name: "absolute path", + path: "/etc/passwd", + wantValid: false, + }, + { + name: "path with shell metacharacters", + path: "file; rm -rf /", + wantValid: false, + }, + { + name: "path with backticks", + path: "file`whoami`.go", + wantValid: false, + }, + { + name: "path with pipes", + path: "file|cat /etc/passwd", + wantValid: false, + }, + { + name: "path with spaces in filename", + path: "my file.go", // Invalid due to spaces (can cause issues with shell command parsing) + wantValid: false, + }, + { + name: "valid deeply nested path", + path: "src/pkg/utils/helper.go", + wantValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValid := validateFilePath(tt.path) + require.Equal(t, tt.wantValid, gotValid) + }) + } +} + +func TestValidateGitRef(t *testing.T) { + tests := []struct { + name string + ref string + wantValid bool + }{ + { + name: "valid branch name", + ref: "main", + wantValid: true, + }, + { + name: "valid commit hash", + ref: "abc123def456", + wantValid: true, + }, + { + name: "empty ref", + ref: "", + wantValid: false, + }, + { + name: "ref with shell metacharacters", + ref: "branch; rm -rf /", + wantValid: false, + }, + { + name: "ref with whitespace", + ref: "my branch", + wantValid: false, + }, + { + name: "ref with path traversal", + ref: "../main", + wantValid: false, + }, + { + name: "ref starting with slash", + ref: "/main", + wantValid: false, + }, + { + name: "ref ending with slash", + ref: "main/", + wantValid: false, + }, + { + name: "valid feature branch", + ref: "feature/new-diff-parsing", + wantValid: true, + }, + { + name: "ref with control characters", + ref: "main\x00", + wantValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValid := validateGitRef(tt.ref) + require.Equal(t, tt.wantValid, gotValid) + }) + } +} diff --git a/danger-js/types_github.go b/danger-js/types_github.go index 870d1dc..25658b1 100644 --- a/danger-js/types_github.go +++ b/danger-js/types_github.go @@ -2,15 +2,6 @@ package dangerJs import "time" -type GitHub struct { - Issue GitHubIssue `json:"issue"` - PR GitHubPR `json:"pr"` - ThisPR GitHubAPIPR `json:"thisPR"` - Commits []GitHubCommit `json:"commits"` - Reviews []GitHubReview `json:"reviews"` - RequestedReviewers GitHubReviewers `json:"requested_reviewers"` -} - type GitHubIssue struct { Labels []GitHubIssueLabel `json:"labels"` } diff --git a/danger-js/types_gitlab.go b/danger-js/types_gitlab.go index 8e25a00..62690f4 100644 --- a/danger-js/types_gitlab.go +++ b/danger-js/types_gitlab.go @@ -2,13 +2,6 @@ package dangerJs import "time" -type GitLab struct { - Metadata RepoMetaData `json:"Metadata"` - MR GitLabMR `json:"mr"` - Commits []GitLabMRCommit `json:"commits"` - Approvals GitLabApproval `json:"approvals"` -} - type RepoMetaData struct { RepoSlug string `json:"repoSlug"` PullRequestID string `json:"pullRequestID"` diff --git a/go.mod b/go.mod index 334ca27..317c7bd 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,13 @@ module github.com/danger/golang go 1.24 +tool github.com/golangci/revgrep/cmd/revgrep + require github.com/stretchr/testify v1.10.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golangci/revgrep v0.8.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 713a0b4..7b229ad 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= +github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=