diff --git a/claudetool/bash.go b/claudetool/bash.go index 82f5c708a578ff0c3d2fcfed248943ec10abe8e8..8c9e673be01e467581beaea2d310671ba946762e 100644 --- a/claudetool/bash.go +++ b/claudetool/bash.go @@ -198,6 +198,9 @@ func (b *BashTool) Run(ctx context.Context, m json.RawMessage) llm.ToolOut { } } + // Add co-author trailer to git commits + req.Command = bashkit.AddCoauthorTrailer(req.Command, "Co-authored-by: Shelley ") + timeout := req.timeout(b.Timeouts) // If Background is set to true, use executeBackgroundBash diff --git a/claudetool/bashkit/bashkit.go b/claudetool/bashkit/bashkit.go index 62e1c7478fe7e0bccfd5c7adcbd78dcf60528986..676cc0b875474f3d69cfd2f31f7fc8a0eaa60436 100644 --- a/claudetool/bashkit/bashkit.go +++ b/claudetool/bashkit/bashkit.go @@ -1,6 +1,7 @@ package bashkit import ( + "bytes" "fmt" "strings" "sync" @@ -144,6 +145,87 @@ func hasBlindGitAdd(cmd *syntax.CallExpr) bool { return false } +// AddCoauthorTrailer modifies a bash script to add a Co-authored-by trailer +// to any git commit commands. Returns the modified script. +func AddCoauthorTrailer(bashScript, trailer string) string { + r := strings.NewReader(bashScript) + parser := syntax.NewParser(syntax.KeepComments(true)) + file, err := parser.Parse(r, "") + if err != nil { + // Can't parse, return original + return bashScript + } + + modified := false + syntax.Walk(file, func(node syntax.Node) bool { + callExpr, ok := node.(*syntax.CallExpr) + if !ok { + return true + } + if addTrailerToGitCommit(callExpr, trailer) { + modified = true + } + return true + }) + + if !modified { + return bashScript + } + + var buf bytes.Buffer + printer := syntax.NewPrinter() + if err := printer.Print(&buf, file); err != nil { + return bashScript + } + return buf.String() +} + +// addTrailerToGitCommit adds --trailer to a git commit command. +// Returns true if the command was modified. +func addTrailerToGitCommit(cmd *syntax.CallExpr, trailer string) bool { + if !isGitCommitCommand(cmd) { + return false + } + + // Find where to insert --trailer (right after "commit") + insertIdx := -1 + for i := 1; i < len(cmd.Args); i++ { + if cmd.Args[i].Lit() == "commit" { + insertIdx = i + 1 + break + } + } + if insertIdx < 0 { + return false + } + + // Create --trailer argument + trailerArg := &syntax.Word{ + Parts: []syntax.WordPart{ + &syntax.Lit{Value: "--trailer"}, + }, + } + // Create the trailer value argument + trailerVal := &syntax.Word{ + Parts: []syntax.WordPart{ + &syntax.DblQuoted{ + Parts: []syntax.WordPart{ + &syntax.Lit{Value: trailer}, + }, + }, + }, + } + + // Insert the two new arguments + newArgs := make([]*syntax.Word, 0, len(cmd.Args)+2) + newArgs = append(newArgs, cmd.Args[:insertIdx]...) + newArgs = append(newArgs, trailerArg, trailerVal) + newArgs = append(newArgs, cmd.Args[insertIdx:]...) + cmd.Args = newArgs + + return true +} + // isGitCommitCommand checks if a command is 'git commit'. func isGitCommitCommand(cmd *syntax.CallExpr) bool { if len(cmd.Args) < 2 { diff --git a/claudetool/bashkit/bashkit_test.go b/claudetool/bashkit/bashkit_test.go index 1658901056dbb5eeb41a7e80c929813d7ecb24de..cfbf05a6a6d4117ae38ddedc4fa546e966134025 100644 --- a/claudetool/bashkit/bashkit_test.go +++ b/claudetool/bashkit/bashkit_test.go @@ -482,3 +482,60 @@ func TestEdgeCases(t *testing.T) { }) } } + +func TestAddCoauthorTrailer(t *testing.T) { + trailer := "Co-authored-by: Shelley " + tests := []struct { + name string + script string + want string + }{ + { + name: "simple git commit", + script: `git commit -m "Add feature"`, + want: `git commit --trailer "Co-authored-by: Shelley " -m "Add feature"`, + }, + { + name: "git commit with -am", + script: `git commit -am "Fix bug"`, + want: `git commit --trailer "Co-authored-by: Shelley " -am "Fix bug"`, + }, + { + name: "no git commit", + script: `git status`, + want: `git status`, + }, + { + name: "git with flags before commit", + script: `git -C /path/to/repo commit -m "Update"`, + want: `git -C /path/to/repo commit --trailer "Co-authored-by: Shelley " -m "Update"`, + }, + { + name: "pipeline with git commit", + script: `git add file.go && git commit -m "Add file"`, + want: `git add file.go && git commit --trailer "Co-authored-by: Shelley " -m "Add file"`, + }, + { + name: "non-git command", + script: `echo hello`, + want: `echo hello`, + }, + { + name: "invalid syntax unchanged", + script: `git commit -m 'unterminated`, + want: `git commit -m 'unterminated`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := AddCoauthorTrailer(tc.script, trailer) + // Normalize whitespace for comparison + gotNorm := strings.Join(strings.Fields(got), " ") + wantNorm := strings.Join(strings.Fields(tc.want), " ") + if gotNorm != wantNorm { + t.Errorf("AddCoauthorTrailer() =\n%q\nwant:\n%q", got, tc.want) + } + }) + } +}