Skip to content

Commit 87c7fe4

Browse files
Your Nameclaude
andcommitted
Add file metadata to diff output. Emit ^ {"file":"..."} header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 800d290 commit 87c7fe4

6 files changed

Lines changed: 120 additions & 9 deletions

File tree

v2/diff_read_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,25 @@ func TestReadDiffLegacyMetadata(t *testing.T) {
413413
require.True(t, d[0].Metadata.Merge)
414414
}
415415

416+
func TestReadDiffFileMetadata(t *testing.T) {
417+
input := "^ {\"file\":\"example.json\"}\n@ [\"a\"]\n- 1\n+ 2\n"
418+
d, err := ReadDiffString(input)
419+
require.NoError(t, err)
420+
require.Len(t, d, 1)
421+
require.Len(t, d[0].Options, 1)
422+
fo, ok := d[0].Options[0].(fileOption)
423+
require.True(t, ok)
424+
require.Equal(t, "example.json", fo.file)
425+
}
426+
427+
func TestReadDiffUnknownMetadataSkipped(t *testing.T) {
428+
input := "^ {\"unknown_key\":\"value\"}\n@ [\"a\"]\n- 1\n+ 2\n"
429+
d, err := ReadDiffString(input)
430+
require.NoError(t, err)
431+
require.Len(t, d, 1)
432+
// The unknown ^ line was silently skipped
433+
}
434+
416435
func TestReadDiffDuplicateMergeOption(t *testing.T) {
417436
// Duplicate MERGE option — exercises the isMerge check in duplicate detection
418437
input := "^ \"MERGE\"\n^ \"MERGE\"\n@ [\"a\"]\n- 1\n+ 2\n"

v2/diff_write_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,23 @@ func TestRenderMergeVoidAdd(t *testing.T) {
940940
}
941941
}
942942

943+
func TestDiffRenderWithFileOption(t *testing.T) {
944+
a, err := ReadJsonString(`{"a":1}`)
945+
if err != nil {
946+
t.Fatal(err)
947+
}
948+
b, err := ReadJsonString(`{"a":2}`)
949+
if err != nil {
950+
t.Fatal(err)
951+
}
952+
d := a.Diff(b)
953+
rendered := d.Render(File("a.json"))
954+
want := "^ {\"file\":\"a.json\"}\n@ [\"a\"]\n- 1\n+ 2\n"
955+
if rendered != want {
956+
t.Errorf("got %q, want %q", rendered, want)
957+
}
958+
}
959+
943960
func TestDiffRenderEmpty(t *testing.T) {
944961
// Empty diff with options should produce no output
945962
d := Diff{}

v2/jd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ func printUsageAndExit() {
234234
}
235235

236236
func printDiff(a, b string, options []jd.Option) {
237+
options = append([]jd.Option{jd.File(flag.Arg(0))}, options...)
237238
str, haveDiff, err := diff(a, b, options)
238239
if err != nil {
239240
errorAndExit(err)
@@ -253,6 +254,7 @@ func printGitDiffDriver(options []jd.Option) error {
253254
if len(flag.Args()) != 7 {
254255
return fmt.Errorf("Git diff driver expects exactly 7 arguments.")
255256
}
257+
options = append([]jd.Option{jd.File(flag.Arg(0))}, options...)
256258
a := readFile(flag.Arg(1))
257259
b := readFile(flag.Arg(4))
258260
str, _, err := diff(a, b, options)

v2/jd/main_test.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ func TestMain(t *testing.T) {
2525
}
2626

2727
testCases := []struct {
28-
name string
29-
files map[string]string
30-
args []string
31-
exitCode int
32-
out *string
33-
outFile string
28+
name string
29+
files map[string]string
30+
args []string
31+
exitCode int
32+
out *string
33+
outFile string
34+
wantFileHeader string // if set, verify output starts with ^ {"file":"...<this>"}
3435
}{{
3536
name: "no diff",
3637
files: map[string]string{
@@ -52,7 +53,8 @@ func TestMain(t *testing.T) {
5253
`- "bar"`,
5354
`+ "baz"`,
5455
)),
55-
exitCode: 1,
56+
exitCode: 1,
57+
wantFileHeader: "a.json",
5658
}, {
5759
name: "no diff in patch mode",
5860
files: map[string]string{
@@ -145,8 +147,24 @@ func TestMain(t *testing.T) {
145147
cmd := exec.Command(os.Args[0], "-test.run", testName)
146148
cmd.Env = append(os.Environ(), jdFlags+"="+strings.Join(args, " "))
147149
out, _ := cmd.CombinedOutput()
148-
if tc.out != nil && string(out) != *tc.out {
149-
t.Errorf("wanted out %q. got %q", *tc.out, string(out))
150+
outStr := string(out)
151+
if tc.wantFileHeader != "" {
152+
prefix := `^ {"file":"`
153+
if !strings.HasPrefix(outStr, prefix) {
154+
t.Errorf("wanted file header prefix %q. got %q", prefix, outStr)
155+
} else {
156+
headerEnd := strings.Index(outStr, "\n")
157+
header := outStr[:headerEnd]
158+
if !strings.HasSuffix(header, tc.wantFileHeader+`"}`) {
159+
t.Errorf("wanted file header ending with %q. got %q", tc.wantFileHeader, header)
160+
}
161+
outStr = outStr[headerEnd+1:]
162+
}
163+
}
164+
if tc.out != nil {
165+
if outStr != *tc.out {
166+
t.Errorf("wanted out %q. got %q", *tc.out, outStr)
167+
}
150168
}
151169
if exitCode := cmd.ProcessState.ExitCode(); exitCode != tc.exitCode {
152170
t.Errorf("wanted exit code %v. got %v", tc.exitCode, exitCode)

v2/options.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ func NewOption(a any) (Option, error) {
7878
keys = append(keys, key)
7979
}
8080
return SetKeys(keys...), nil
81+
case "file":
82+
s, ok := v.(string)
83+
if !ok {
84+
return nil, fmt.Errorf("wanted string. got %T", v)
85+
}
86+
return File(s), nil
8187
case "Merge":
8288
b, ok := v.(bool)
8389
if !ok {
@@ -252,6 +258,21 @@ func PathOption(at Path, then ...Option) Option {
252258
}
253259
func (o pathOption) isOption() {}
254260

261+
type fileOption struct {
262+
file string
263+
}
264+
265+
func File(path string) Option {
266+
return fileOption{file: path}
267+
}
268+
269+
func (o fileOption) isOption() {}
270+
func (o fileOption) MarshalJSON() ([]byte, error) {
271+
return json.Marshal(map[string]string{
272+
"file": o.file,
273+
})
274+
}
275+
255276
type setKeysOption []string
256277

257278
func SetKeys(keys ...string) Option {

v2/options_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ func TestOptionJSON(t *testing.T) {
173173
}, {
174174
json: `["DIFF_OFF"]`,
175175
option: DIFF_OFF,
176+
}, {
177+
json: `[{"file":"example.json"}]`,
178+
option: File("example.json"),
176179
}}
177180
for _, c := range cases {
178181
t.Run(c.json, func(t *testing.T) {
@@ -552,6 +555,37 @@ func TestRefinePathMultiset(t *testing.T) {
552555
}
553556
}
554557

558+
func TestFileOption(t *testing.T) {
559+
// NewOption recognizes {"file":"example.json"}
560+
opt, err := NewOption(map[string]any{"file": "example.json"})
561+
require.NoError(t, err)
562+
fo, ok := opt.(fileOption)
563+
require.True(t, ok)
564+
require.Equal(t, "example.json", fo.file)
565+
566+
// MarshalJSON produces {"file":"example.json"}
567+
b, err := json.Marshal(opt)
568+
require.NoError(t, err)
569+
require.Equal(t, `{"file":"example.json"}`, string(b))
570+
571+
// Round-trip: File() -> marshal -> unmarshal via NewOption -> equal
572+
opt2 := File("a.json")
573+
b2, err := json.Marshal(opt2)
574+
require.NoError(t, err)
575+
var raw any
576+
err = json.Unmarshal(b2, &raw)
577+
require.NoError(t, err)
578+
opt3, err := NewOption(raw)
579+
require.NoError(t, err)
580+
fo3, ok := opt3.(fileOption)
581+
require.True(t, ok)
582+
require.Equal(t, "a.json", fo3.file)
583+
584+
// Wrong type for file value
585+
_, err = NewOption(map[string]any{"file": 42})
586+
require.Error(t, err)
587+
}
588+
555589
func TestRefineEmptyAt(t *testing.T) {
556590
// PathOption with empty At and non-nil path element should be skipped
557591
opts := newOptions([]Option{PathOption(Path{}, SET)})

0 commit comments

Comments
 (0)