@@ -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 <shelley@exe.dev>")
+
timeout := req.timeout(b.Timeouts)
// If Background is set to true, use executeBackgroundBash
@@ -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 {
@@ -482,3 +482,60 @@ func TestEdgeCases(t *testing.T) {
})
}
}
+
+func TestAddCoauthorTrailer(t *testing.T) {
+ trailer := "Co-authored-by: Shelley <shelley@exe.dev>"
+ 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 <shelley@exe.dev>" -m "Add feature"`,
+ },
+ {
+ name: "git commit with -am",
+ script: `git commit -am "Fix bug"`,
+ want: `git commit --trailer "Co-authored-by: Shelley <shelley@exe.dev>" -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 <shelley@exe.dev>" -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 <shelley@exe.dev>" -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)
+ }
+ })
+ }
+}