feat(hooks): add --hook-output flag reporting removed comments
Emits Claude Code PostToolUse JSON so automated comment removal is announced in-context instead of appearing as a silent mutation. --install-hook now writes 'shush --changes-only --hook-output'. Bump version to 0.6.0.
This commit is contained in:
@@ -226,6 +226,16 @@ func changed(changes []LineChange) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func countRemoved(changes []LineChange) int {
|
||||
n := 0
|
||||
for _, c := range changes {
|
||||
if c.Action == "modified" || c.Action == "removed" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func changesToInfos(changes []LineChange) ([]changeInfo, int, int, int) {
|
||||
var infos []changeInfo
|
||||
changedCount, keptCount, preservedCount := 0, 0, 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -53,7 +54,9 @@ func (p *Processor) processGitChanges() error {
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
fmt.Println("No changes found to process")
|
||||
if !p.cli.HookOutput {
|
||||
fmt.Println("No changes found to process")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,7 +74,9 @@ func (p *Processor) processGitChanges() error {
|
||||
}
|
||||
|
||||
if len(supportedChanges) == 0 {
|
||||
fmt.Println("No supported files found to process")
|
||||
if !p.cli.HookOutput {
|
||||
fmt.Println("No supported files found to process")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,7 +106,7 @@ func (p *Processor) processGitChanges() error {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if err := p.processFileWithLineRanges(change.Path, change.LineRanges, cfg); err != nil {
|
||||
if err := p.processFileWithLineRanges(change.Path, change.LineRanges, cfg, totals); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", change.Path, err)
|
||||
continue
|
||||
}
|
||||
@@ -113,10 +118,44 @@ func (p *Processor) processGitChanges() error {
|
||||
p.showGitTotals(totals)
|
||||
}
|
||||
|
||||
if p.cli.HookOutput {
|
||||
if out, ok := buildHookOutput(totals.TotalChanged); ok {
|
||||
fmt.Println(out)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) processFileWithLineRanges(filename string, lineRanges []git.LineRange, cfg *config.Config) error {
|
||||
type hookSpecificOutput struct {
|
||||
HookEventName string `json:"hookEventName"`
|
||||
AdditionalContext string `json:"additionalContext"`
|
||||
}
|
||||
|
||||
type hookOutput struct {
|
||||
HookSpecificOutput hookSpecificOutput `json:"hookSpecificOutput"`
|
||||
}
|
||||
|
||||
func buildHookOutput(removed int) (string, bool) {
|
||||
if removed <= 0 {
|
||||
return "", false
|
||||
}
|
||||
plural := ""
|
||||
if removed != 1 {
|
||||
plural = "s"
|
||||
}
|
||||
msg := fmt.Sprintf("shush removed %d comment%s from files just edited, per this project's comment policy. This is expected automated cleanup, not an error or lost work. Do not re-add the removed comments; to keep specific comments, add preserve patterns to .shush.toml.", removed, plural)
|
||||
b, err := json.Marshal(hookOutput{hookSpecificOutput{
|
||||
HookEventName: "PostToolUse",
|
||||
AdditionalContext: msg,
|
||||
}})
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(b), true
|
||||
}
|
||||
|
||||
func (p *Processor) processFileWithLineRanges(filename string, lineRanges []git.LineRange, cfg *config.Config, totals *GitTotals) error {
|
||||
src, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -150,6 +189,7 @@ func (p *Processor) processFileWithLineRanges(filename string, lineRanges []git.
|
||||
if err := os.WriteFile(filename, out, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
totals.TotalChanged += countRemoved(changes)
|
||||
if p.cli.Verbose {
|
||||
fmt.Printf("✓ Comments removed from %s\n", filename)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,65 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/carlosarraes/shush/internal/config"
|
||||
"github.com/carlosarraes/shush/internal/types"
|
||||
)
|
||||
|
||||
func TestCountRemoved(t *testing.T) {
|
||||
changes := []LineChange{
|
||||
{Action: "removed"},
|
||||
{Action: "modified"},
|
||||
{Action: "preserved"},
|
||||
{Action: "kept"},
|
||||
{Action: ""},
|
||||
}
|
||||
if got := countRemoved(changes); got != 2 {
|
||||
t.Errorf("countRemoved() = %d, want 2", got)
|
||||
}
|
||||
if got := countRemoved(nil); got != 0 {
|
||||
t.Errorf("countRemoved(nil) = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHookOutput(t *testing.T) {
|
||||
if _, ok := buildHookOutput(0); ok {
|
||||
t.Error("buildHookOutput(0) should be silent (ok=false)")
|
||||
}
|
||||
if _, ok := buildHookOutput(-1); ok {
|
||||
t.Error("buildHookOutput(-1) should be silent (ok=false)")
|
||||
}
|
||||
|
||||
out, ok := buildHookOutput(3)
|
||||
if !ok {
|
||||
t.Fatal("buildHookOutput(3) should emit (ok=true)")
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
HookSpecificOutput struct {
|
||||
HookEventName string `json:"hookEventName"`
|
||||
AdditionalContext string `json:"additionalContext"`
|
||||
} `json:"hookSpecificOutput"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v", err)
|
||||
}
|
||||
if parsed.HookSpecificOutput.HookEventName != "PostToolUse" {
|
||||
t.Errorf("hookEventName = %q, want PostToolUse", parsed.HookSpecificOutput.HookEventName)
|
||||
}
|
||||
if !strings.Contains(parsed.HookSpecificOutput.AdditionalContext, "3 comments") {
|
||||
t.Errorf("context missing count: %q", parsed.HookSpecificOutput.AdditionalContext)
|
||||
}
|
||||
|
||||
single, _ := buildHookOutput(1)
|
||||
if !strings.Contains(single, "1 comment ") {
|
||||
t.Errorf("singular form wrong: %q", single)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCommentsFromLine(t *testing.T) {
|
||||
cli := types.CLI{}
|
||||
p := &Processor{cli: cli}
|
||||
|
||||
@@ -38,7 +38,7 @@ func runMultilineCase(t *testing.T, tc multilineCase) {
|
||||
}
|
||||
p := &Processor{cli: tc.cli}
|
||||
if tc.gitMode {
|
||||
err = p.processFileWithLineRanges(work, tc.ranges, cfg)
|
||||
err = p.processFileWithLineRanges(work, tc.ranges, cfg, &GitTotals{})
|
||||
} else {
|
||||
lang, derr := DetectLanguage(work)
|
||||
if derr != nil {
|
||||
|
||||
@@ -46,7 +46,7 @@ func runWholeFileCase(t *testing.T, tc wholeFileCase) {
|
||||
|
||||
p := &Processor{cli: tc.cli}
|
||||
if tc.gitMode {
|
||||
err = p.processFileWithLineRanges(work, tc.ranges, cfg)
|
||||
err = p.processFileWithLineRanges(work, tc.ranges, cfg, &GitTotals{})
|
||||
} else {
|
||||
lang, derr := DetectLanguage(work)
|
||||
if derr != nil {
|
||||
|
||||
Reference in New Issue
Block a user