Artifact 5cefe1067772a220b4402308a31a8cdf3a5320826b75e650f9467bbc2960d791:
- File
cli/repo_merge.go
— part of check-in
[d8458ad960]
at
2026-10-22 17:53:21
on branch feat/horizon-cones
— feat(merge): refuse a base below the structural horizon, or deepen to it
Section 5.5 forbids merging against a base the store does not hold. In a structural store the walk can reach a boundary check-in as the base, or find none where both sides' ancestry leaves the store, and the merge then failed on a phantom with an error that never named the horizon. content.MergeBaseCrossing decides the crossing for checkout merges and branch merges alike, and the error names the horizon, the base and the deepen, matching libfossil.ErrBelowHorizon.
Checkout.MergeContext is the remedy zit's merge applies: given a peer it asks the origin for the base, deepens to reach it and merges again over the real base. 'repo merge' takes the recovery flags and contacts the remote only when a merge crosses the horizon. (user: [email protected], size: 2830) [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", } // printMergeResult reports a merge the way fossil's merge command does. 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 "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) } } 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.") } }