mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(log): bdrive log reads as a timeline — newest first by edit time (#BEA-40) (#90)
`bdrive log` sorted by the lamport clock, so the wall-clock stamps it prints came out non-monotonic — two 06:09:24 rows above a 06:10:00 row. And every op of one scan is stamped with that scan's commit time, so a 22-file agent run collapsed onto a single stamp. Neither is readable as a timeline, which is the whole job of the command. Two display-only changes: - `journal.Op` gains `Mtime` (`omitzero`, so old journals and old binaries are unaffected), populated on put ops from the `os.FileInfo` the scan already holds. Deletes and conflict copies keep their commit time. - `syncer.DisplayTime` / `SortForDisplay` order by the timestamp that is actually printed, ties broken by reversed `journal.Less`. `bdrive log` sorts and *then* truncates, so `-n 25` is the 25 newest by that stamp. `journal.Less`, `Sort`, and `Replay` are untouched — replay order is the convergence contract, so the sort lives in `syncer`, not in `journal`. `LogEntries` also keeps returning causal order because `bdrive restore` walks it to find a file's previous version. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b1c0bba415
commit
872ba702cb
@@ -39,6 +39,10 @@ type Op struct {
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Mode uint32 `json:"mode,omitempty"` // permission bits
|
||||
Note string `json:"note,omitempty"` // e.g. "conflict copy of <path>"
|
||||
// Mtime is when the file was last written, as opposed to Time, which is
|
||||
// when the op was committed. Display only — never an input to Less or
|
||||
// Replay, since it comes from the filesystem and can be anything.
|
||||
Mtime time.Time `json:"mtime,omitzero"` // put only
|
||||
}
|
||||
|
||||
// Less defines the total order used to replay ops from many devices.
|
||||
|
||||
@@ -3,6 +3,7 @@ package journal
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -93,3 +94,37 @@ func TestParseSkipsBlankLines(t *testing.T) {
|
||||
t.Fatalf("got %v %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMtimeIsAdditive pins the wire shape: an op carrying Mtime round-trips,
|
||||
// and an op without one emits no "mtime" key at all — so a journal written by
|
||||
// this code still parses in the old shape. (omitempty would not do this: it
|
||||
// does not omit a zero struct.)
|
||||
func TestMtimeIsAdditive(t *testing.T) {
|
||||
mt := time.Unix(1700000000, 0).UTC()
|
||||
with := op(1, "a", 1, KindPut, "x.txt", "blob1")
|
||||
with.Mtime = mt
|
||||
without := op(2, "a", 2, KindDelete, "x.txt", "")
|
||||
|
||||
data, err := Marshal([]Op{with, without})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
if !strings.Contains(lines[0], `"mtime"`) {
|
||||
t.Fatalf("put op lost its mtime: %s", lines[0])
|
||||
}
|
||||
if strings.Contains(lines[1], "mtime") {
|
||||
t.Fatalf("op without mtime should emit no mtime key: %s", lines[1])
|
||||
}
|
||||
|
||||
got, err := Parse(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got[0].Mtime.Equal(mt) {
|
||||
t.Fatalf("Mtime = %v, want %v", got[0].Mtime, mt)
|
||||
}
|
||||
if !got[1].Mtime.IsZero() {
|
||||
t.Fatalf("Mtime should be zero, got %v", got[1].Mtime)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user