Artifact 80be6c373ee7dd2dad84360143631058f75c45998227cfc78977203835a23287:
- File
cli/repo_merge.go
— part of check-in
[f5744359d3]
at
2026-04-19 16:30:54
on branch trunk
— Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.
This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.
Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ (user: [email protected], size: 4892) [annotate] [blame] [check-ins using]
package cli import ( "fmt" "os" "path/filepath" libfossil "github.com/danmestas/libfossil" "github.com/danmestas/libfossil/db" "github.com/danmestas/libfossil/internal/blob" "github.com/danmestas/libfossil/internal/content" "github.com/danmestas/libfossil/internal/fsltype" "github.com/danmestas/libfossil/internal/merge" ) // RepoMergeCmd merges a divergent version into the current checkout. type RepoMergeCmd struct { 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) error { r, err := g.OpenRepo() if err != nil { return err } defer r.Close() localRid, err := resolveRID(r, "tip") if err != nil { return fmt.Errorf("resolving local tip: %w", err) } remoteRid, err := resolveRID(r, c.Version) if err != nil { return fmt.Errorf("resolving remote version: %w", err) } inner := r.Inner() idb := inner.DB() ancestorRid, err := r.FindCommonAncestor(localRid, remoteRid) if err != nil { return fmt.Errorf("finding common ancestor: %w", err) } var ancestorUUID string idb.QueryRow("SELECT uuid FROM blob WHERE rid=?", ancestorRid).Scan(&ancestorUUID) if len(ancestorUUID) > 10 { ancestorUUID = ancestorUUID[:10] } fmt.Printf("common ancestor: %s (rid=%d)\n", ancestorUUID, ancestorRid) baseFiles, err := r.ListFiles(ancestorRid) if err != nil { return fmt.Errorf("listing ancestor files: %w", err) } localFiles, err := r.ListFiles(localRid) if err != nil { return fmt.Errorf("listing local files: %w", err) } remoteFiles, err := r.ListFiles(remoteRid) if err != nil { return fmt.Errorf("listing remote files: %w", err) } baseMap := toFileMap(baseFiles) localMap := toFileMap(localFiles) remoteMap := toFileMap(remoteFiles) resolver := merge.LoadResolver(inner, fsltype.FslID(localRid)) ckout, err := openCheckout(c.Dir) if err != nil && !c.DryRun { return err } if ckout != nil { defer ckout.Close() } vid, _ := checkoutVid(ckout) merged, conflicts := 0, 0 allFiles := make(map[string]bool) for name := range localMap { allFiles[name] = true } for name := range remoteMap { allFiles[name] = true } for name := range allFiles { localUUID := localMap[name] remoteUUID := remoteMap[name] baseUUID := baseMap[name] if localUUID == remoteUUID { continue } stratName := c.Strategy if stratName == "" { stratName = resolver.Resolve(name) } strat, ok := merge.StrategyByName(stratName) if !ok { return fmt.Errorf("unknown strategy %q for %s", stratName, name) } baseContent := loadBlobByUUID(idb, baseUUID) localContent := loadBlobByUUID(idb, localUUID) remoteContent := loadBlobByUUID(idb, remoteUUID) if c.DryRun { fmt.Printf(" [%s] %s\n", stratName, name) continue } result, err := strat.Merge(baseContent, localContent, remoteContent) if err != nil { return fmt.Errorf("merging %s: %w", name, err) } outPath := filepath.Join(c.Dir, name) os.MkdirAll(filepath.Dir(outPath), 0o755) if result.Clean { os.WriteFile(outPath, result.Content, 0o644) if ckout != nil { ckout.Exec("UPDATE vfile SET chnged=1 WHERE pathname=? AND vid=?", name, vid) } fmt.Printf(" [merged] %s (%s)\n", name, stratName) merged++ } else if strat.Name() == "conflict-fork" { merge.EnsureConflictTable(inner) var baseRID, localRID, remoteRID int64 idb.QueryRow("SELECT rid FROM blob WHERE uuid=?", baseUUID).Scan(&baseRID) idb.QueryRow("SELECT rid FROM blob WHERE uuid=?", localUUID).Scan(&localRID) idb.QueryRow("SELECT rid FROM blob WHERE uuid=?", remoteUUID).Scan(&remoteRID) merge.RecordConflictFork(inner, name, baseRID, localRID, remoteRID) fmt.Printf(" [fork] %s (conflict-fork: all versions preserved)\n", name) conflicts++ } else { os.WriteFile(outPath, result.Content, 0o644) os.WriteFile(outPath+".LOCAL", localContent, 0o644) os.WriteFile(outPath+".BASELINE", baseContent, 0o644) os.WriteFile(outPath+".MERGE", remoteContent, 0o644) if ckout != nil { ckout.Exec("UPDATE vfile SET chnged=5 WHERE pathname=? AND vid=?", name, vid) } fmt.Printf(" [CONFLICT] %s (%s, %d regions)\n", name, stratName, len(result.Conflicts)) conflicts++ } } fmt.Printf("\n%d files merged, %d conflicts\n", merged, conflicts) return nil } func toFileMap(files []libfossil.FileEntry) map[string]string { m := make(map[string]string) for _, f := range files { m[f.Name] = f.UUID } return m } func loadBlobByUUID(d *db.DB, uuid string) []byte { if uuid == "" { return nil } rid, ok := blob.Exists(d, uuid) if !ok { return nil } data, err := content.Expand(d, rid) if err != nil { return nil } return data }