Artifact 5b639e6db0cdab837eccf0b34f4d2db400985908e770138894f574fe655a2cfb:
- File
internal/merge/renames_test.go
— part of check-in
[2c3a78cf5e]
at
2026-10-22 21:16:18
on branch feat/horizon-cones
— fix(merge): follow renames as fossil's merge does
A file renamed on one side and edited on the other was merged as a deletion plus an addition: the edit was kept under the old name and the renamed copy arrived beside it, so fossil and go-libfossil left different trees and recorded different changes for the same merge.
Port fossil's rename matching (merge.c merge_cmd): the name pivot nid (pivot_find(1) of M and V, rerun against P when they differ), the renames from nid to V, P and M (path.c find_filename_changes, one way, now path.FilenameChanges), and the fv table those fill (add_renames, the nid!=pid name fill-in, then the files of V, P and M) as merge.MatchFiles/FollowRenames. Checkout.Merge classifies each matched file as before and renames on disk and in vfile a file the merged-in version renamed and the checkout did not, after the other steps, moving swapped names together and setting aside a removed or deleted row that held the new name. Where fossil would rename over a kept tracked file or an untracked one, Merge refuses, as it already does for adds.
Repo.Merge matches files the same way and commits the merged tree whole, recording the rename (FileToCommit.OldName); the whole-tree commit also stops it carrying forward a file the merge deleted. The CLI prints RENAME lines where fossil's merge prints them. (user: [email protected], size: 2651) [annotate] [blame] [check-ins using] [more...]
package merge import ( "reflect" "testing" "github.com/danmestas/go-libfossil/internal/path" ) func TestMatchFiles(t *testing.T) { ch := func(pairs ...string) []path.NameChange { var out []path.NameChange for i := 0; i < len(pairs); i += 2 { out = append(out, path.NameChange{From: pairs[i], To: pairs[i+1]}) } return out } local := func(names ...string) []LocalFile { var out []LocalFile for _, n := range names { out = append(out, LocalFile{Name: n}) } return out } cases := []struct { name string in MatchInput want []FileMatch }{{ name: "renamed in M", in: MatchInput{Local: local("f"), Pivot: []string{"f"}, Merged: []string{"r"}, ToMerged: ch("f", "r")}, want: []FileMatch{{Local: 0, Pivot: "f", Merged: "r"}}, }, { name: "renamed in V", in: MatchInput{Local: local("r"), Pivot: []string{"f"}, Merged: []string{"f"}, ToLocal: ch("f", "r")}, want: []FileMatch{{Local: 0, Pivot: "f", Merged: "f"}}, }, { name: "renamed apart", in: MatchInput{Local: local("r"), Pivot: []string{"f"}, Merged: []string{"s"}, ToLocal: ch("f", "r"), ToMerged: ch("f", "s")}, want: []FileMatch{{Local: 0, Pivot: "f", Merged: "s"}}, }, { name: "swapped in M", in: MatchInput{Local: local("a", "b", "f"), Pivot: []string{"a", "b", "f"}, Merged: []string{"a", "b", "f"}, ToMerged: ch("a", "f", "f", "a")}, want: []FileMatch{ {Local: 0, Pivot: "a", Merged: "f"}, {Local: 1, Pivot: "b", Merged: "b"}, {Local: 2, Pivot: "f", Merged: "a"}, }, }, { name: "added and deleted", in: MatchInput{Local: []LocalFile{{Name: "f"}, {Name: "n", Added: true}}, Pivot: []string{"f", "g"}, Merged: []string{"g", "m"}}, want: []FileMatch{ {Local: 0, Pivot: "f", Merged: "f"}, {Local: 1, Pivot: "n", Merged: "n"}, {Local: -1, Pivot: "g", Merged: "g"}, {Local: -1, Merged: "m"}, }, }, { name: "renamed locally, not committed", in: MatchInput{Local: []LocalFile{{Name: "r", Orig: "f"}}, Pivot: []string{"f"}, Merged: []string{"f"}}, want: []FileMatch{{Local: 0, Pivot: "f", Merged: "f"}}, }, { // The name pivot is not the content pivot: P's name for the file // is filled in from N's. name: "name pivot fill-in", in: MatchInput{Local: local("c"), Pivot: []string{"a"}, Merged: []string{"a"}, ToLocal: ch("a", "c"), NamePivotDiffers: true}, want: []FileMatch{{Local: 0, Pivot: "a", Merged: "a"}}, }} for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := MatchFiles(tc.in) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, tc.want) { t.Errorf("MatchFiles = %+v, want %+v", got, tc.want) } }) } }