Artifact c5565ef5ab6774230f92436b4022f3a7f7536a4dca0df8a966c716b2becbbf1d:
- File
cli/repo_merge.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: 3228) [annotate] [blame] [check-ins using] [more...]
package cli import ( "context" "errors" "fmt" libfossil "github.com/danmestas/go-libfossil" ) // RepoMergeCmd merges a divergent version into the current checkout. In a // store with a structural horizon, a merge whose base lies below the horizon // asks --remote, else the saved last-sync-url, for the base and deepens the // horizon to reach it before merging (draft-fossil-horizon-00 ยง5.5). type RepoMergeCmd struct { RecoveryFlags Version string `arg:"" help:"Version to merge into current checkout"` Strategy string `help:"Override merge strategy for all files"` DryRun bool `help:"Show what would be merged without writing"` Dir string `short:"d" help:"Checkout directory" default:"."` } func (c *RepoMergeCmd) Run(g *Globals) (err error) { r, co, done, err := openWorkingCheckout(g, c.Dir) if err != nil { return err } defer closeWith(done, &err) rid, err := co.ResolveVersion(c.Version) if err != nil { return fmt.Errorf("resolving %s: %w", c.Version, err) } opts := libfossil.CheckoutMergeOpts{Version: rid, Strategy: c.Strategy, DryRun: c.DryRun} res, err := co.Merge(opts) if errors.Is(err, libfossil.ErrBelowHorizon) { // Only a crossing contacts a peer: an ordinary merge stays local. required, rerr := c.requiredContent(r) if rerr != nil { return errors.Join(err, rerr) } if required.Peer != nil { ctx, cancel := cloneContext(context.Background(), c.Timeout) defer cancel() res, err = co.MergeContext(ctx, opts, required) } } if err != nil { return err } printMergeResult(res, c.DryRun) return nil } // mergeLabels are the labels fossil's merge prints for each action. var mergeLabels = map[string]string{ "updated": "UPDATE", "merged": "MERGE", "conflict": "MERGE", "kept": "KEPT", "forked": "FORK", "added": "ADDED", "deleted": "DELETE", "mode": "MODE", "renamed": "RENAME", } // printMergeResult reports a merge the way fossil's merge command does: // renames after every other change but additions, which come last. func printMergeResult(res libfossil.CheckoutMergeResult, dryRun bool) { if len(res.Files) == 0 { fmt.Println("merge skipped: nothing to merge") return } conflicts := 0 for _, f := range res.Files { label, ok := mergeLabels[f.Action] if !ok { panic("cli.printMergeResult: unknown merge action " + f.Action) } switch f.Action { case "renamed", "added": // Printed below. case "conflict": fmt.Printf("%s %s\n***** merge conflict in %s\n", label, f.Name, f.Name) conflicts++ case "kept": fmt.Printf("%s %s (%s)\n", label, f.Name, f.Reason) conflicts++ case "forked": fmt.Printf("%s %s (conflict-fork: all versions preserved)\n", label, f.Name) conflicts++ default: fmt.Printf("%s %s\n", label, f.Name) } } for _, f := range res.Files { if f.RenamedTo != "" { fmt.Printf("%s %s -> %s\n", mergeLabels["renamed"], f.Name, f.RenamedTo) } } for _, f := range res.Files { if f.Action == "added" { fmt.Printf("%s %s\n", mergeLabels["added"], f.Name) } } if conflicts > 0 { fmt.Printf("WARNING: %d merge conflicts\n", conflicts) } if dryRun { fmt.Println("REMINDER: this was a dry run - no files were actually changed.") } }