Artifact 341b23265ce79c4fb2bba63b00c56f6b249ba2ed0c9e3aca6379449c0355486a:
- File
repo_history.go
— part of check-in
[a3fbae135e]
at
2026-04-21 02:36:41
on branch trunk
— feat: implement Repo.Diff (#1)
Fills in the TODO stub in repo_history.go so Repo.Diff returns a real unified diff for a file between two checkins instead of a "not yet implemented" error.
The implementation piggybacks on internal/diff.Unified, which already produces Myers-algorithm output. A small blobAt helper walks the manifest at each checkin, resolves the file's UUID to a blob RID, and expands the content. An absent file on a side is treated as empty bytes so pure additions and deletions render as clean insert/delete hunks. Identical content on both sides returns an empty slice rather than a DiffEntry with an empty Unified string, which keeps callers from having to distinguish "no diff" from "missing output".
Adds repo_history_test.go covering modification, identical content, addition, deletion, absent-both-sides, and the empty-filePath error. (user: [email protected], size: 3471) [annotate] [blame] [check-ins using]
package libfossil import ( "fmt" "time" "github.com/danmestas/libfossil/internal/annotate" "github.com/danmestas/libfossil/internal/blob" "github.com/danmestas/libfossil/internal/content" "github.com/danmestas/libfossil/internal/diff" "github.com/danmestas/libfossil/internal/fsltype" "github.com/danmestas/libfossil/internal/manifest" ) // LogOpts configures a log/timeline query. type LogOpts struct { Start int64 Limit int } // LogEntry represents a single checkin in the timeline. type LogEntry struct { RID int64 UUID string Comment string User string Time time.Time Parents []string } // DiffEntry describes a unified diff for a single file. type DiffEntry struct { Name string Unified string } // AnnotatedLine is a single line of blame/annotate output. type AnnotatedLine struct { Text string UUID string User string Date time.Time } // AnnotateOpts configures an annotate operation. type AnnotateOpts struct { FilePath string StartRID int64 } // StatusOpts configures a working-tree status query. type StatusOpts struct { Dir string } // BisectSession holds state for a binary-search bisect operation. type BisectSession struct { inner interface{} } // Annotate attributes each line of a file to the commit that last changed it. func (r *Repo) Annotate(opts AnnotateOpts) ([]AnnotatedLine, error) { lines, err := annotate.Annotate(r.inner, annotate.Options{ FilePath: opts.FilePath, StartRID: fsltype.FslID(opts.StartRID), }) if err != nil { return nil, fmt.Errorf("libfossil: annotate: %w", err) } result := make([]AnnotatedLine, len(lines)) for i, l := range lines { result[i] = AnnotatedLine{ Text: l.Text, UUID: l.Version.UUID, User: l.Version.User, Date: l.Version.Date, } } return result, nil } // Diff returns a unified diff for filePath between two checkins. // When the file is absent from a side, that side is treated as empty // bytes, so additions and deletions render as pure insert/delete hunks. // Returns an empty slice when both sides are byte-identical. func (r *Repo) Diff(ridA, ridB int64, filePath string) ([]DiffEntry, error) { if filePath == "" { return nil, fmt.Errorf("libfossil: diff: filePath is required") } a, err := blobAt(r, ridA, filePath) if err != nil { return nil, fmt.Errorf("libfossil: diff: checkin %d: %w", ridA, err) } b, err := blobAt(r, ridB, filePath) if err != nil { return nil, fmt.Errorf("libfossil: diff: checkin %d: %w", ridB, err) } unified := diff.Unified(a, b, diff.Options{ ContextLines: 3, SrcName: "a/" + filePath, DstName: "b/" + filePath, }) if unified == "" { return []DiffEntry{}, nil } return []DiffEntry{{Name: filePath, Unified: unified}}, nil } // blobAt returns the bytes of filePath as they exist in the given checkin. // A checkin that does not contain filePath returns (nil, nil); callers treat // that as "empty side" for diff purposes. Errors surface only for real I/O // or consistency problems (unknown RID, missing blob for a listed UUID). func blobAt(r *Repo, checkinRID int64, filePath string) ([]byte, error) { files, err := manifest.ListFiles(r.inner, fsltype.FslID(checkinRID)) if err != nil { return nil, err } for _, f := range files { if f.Name != filePath { continue } rid, ok := blob.Exists(r.DB(), f.UUID) if !ok { return nil, fmt.Errorf("blob not found for uuid %s", f.UUID) } return content.Expand(r.DB(), rid) } return nil, nil }