Artifact 6f28195cdd10b1ce4adcb22b9c4ca4b14c49497380551896d3e82c88e1789c3a:
- File
cmd/libfossil/snapshot_test.go
— part of check-in
[16d2c51f61]
at
2026-10-19 23:06:03
on branch feat/cli-snapshot
— test(cli): compare log across a rename with fossil finfo
The rename test built its history through manifest.Checkin and checked our own idea of the result. It now builds the history with the fossil binary and compares repo log with fossil finfo on the same repository: both stop a name's history at the rename. The doc tells an embedder how to follow a file across renames, and where log differs from finfo. (user: [email protected], size: 23068) [annotate] [blame] [check-ins using] [more...]
package main import ( "bytes" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "sync" "testing" libfossil "github.com/danmestas/go-libfossil" "github.com/danmestas/go-libfossil/testutil" ) // exitNotFound is the documented exit status for a version, path or history // that does not exist. const exitNotFound = 2 var uuidRE = regexp.MustCompile(`^[0-9a-f]{40}([0-9a-f]{24})?$`) // runCLIStdin is runCLI with stdin supplied, for the commands that read the // content to commit from it. func runCLIStdin(t *testing.T, stdin string, args ...string) (stdout, stderr string, exitCode int) { t.Helper() cmd := exec.Command(os.Args[0], args...) cmd.Env = append(os.Environ(), "LIBFOSSIL_TEST_MAIN=1") cmd.Stdin = strings.NewReader(stdin) var outBuf, errBuf bytes.Buffer cmd.Stdout = &outBuf cmd.Stderr = &errBuf err := cmd.Run() switch e := err.(type) { case nil: exitCode = 0 case *exec.ExitError: exitCode = e.ExitCode() default: t.Fatalf("running subprocess: %v", err) } return outBuf.String(), errBuf.String(), exitCode } func newSnapshotRepo(t *testing.T) string { t.Helper() path := filepath.Join(t.TempDir(), "notes.fossil") stdout, stderr, code := runCLI(t, "repo", "new", path, "--user", "tester") if code != 0 { t.Fatalf("repo new exited %d\nstdout:\n%s\nstderr:\n%s", code, stdout, stderr) } return path } // snapshot commits content at name through stdin and returns the printed uuid. func snapshot(t *testing.T, repoPath, name, content string) string { t.Helper() stdout, stderr, code := runCLIStdin(t, content, "-R", repoPath, "repo", "snapshot", "-m", "snap "+name, "--user", "tester", "--path", name, "--stdin") if code != 0 { t.Fatalf("snapshot %s exited %d\nstdout:\n%s\nstderr:\n%s", name, code, stdout, stderr) } uuid := strings.TrimSpace(stdout) if !uuidRE.MatchString(uuid) { t.Fatalf("snapshot %s printed %q, want a single uuid", name, stdout) } return uuid } type logRow struct { UUID string `json:"uuid"` Time string `json:"time"` User string `json:"user"` Comment string `json:"comment"` Action string `json:"action"` FileUUID string `json:"file_uuid"` PrevName string `json:"prev_name"` } func pathLog(t *testing.T, repoPath string, args ...string) []logRow { t.Helper() full := append([]string{"-R", repoPath, "repo", "log", "--json"}, args...) stdout, stderr, code := runCLI(t, full...) if code != 0 { t.Fatalf("repo log exited %d\nstdout:\n%s\nstderr:\n%s", code, stdout, stderr) } var rows []logRow if err := json.Unmarshal([]byte(stdout), &rows); err != nil { t.Fatalf("repo log --json is not a JSON array: %v\n%s", err, stdout) } return rows } func TestSnapshotCommitsBytesWithoutCheckout(t *testing.T) { repoPath := newSnapshotRepo(t) uuid := snapshot(t, repoPath, "notes/x.md", "hello\n") stdout, stderr, code := runCLI(t, "-R", repoPath, "repo", "cat", "--at", uuid, "notes/x.md") if code != 0 || stdout != "hello\n" { t.Fatalf("cat exited %d, stdout %q, stderr %q; want hello", code, stdout, stderr) } } func TestSnapshotIdenticalBytesIsNoOp(t *testing.T) { repoPath := newSnapshotRepo(t) first := snapshot(t, repoPath, "x.md", "same\n") again := snapshot(t, repoPath, "x.md", "same\n") if again != first { t.Fatalf("no-op snapshot printed %s, want the existing tip %s", again, first) } if rows := pathLog(t, repoPath, "--path", "x.md"); len(rows) != 1 { t.Fatalf("log has %d rows after a no-op snapshot, want 1", len(rows)) } stdout, stderr, code := runCLIStdin(t, "same\n", "-R", repoPath, "repo", "snapshot", "-m", "m", "--path", "x.md", "--stdin", "--json") if code != 0 { t.Fatalf("snapshot --json exited %d: %s", code, stderr) } var res struct { UUID string `json:"uuid"` Changed bool `json:"changed"` } if err := json.Unmarshal([]byte(stdout), &res); err != nil { t.Fatalf("snapshot --json output %q: %v", stdout, err) } if res.UUID != first || res.Changed { t.Fatalf("snapshot --json = %+v, want uuid %s changed=false", res, first) } second := snapshot(t, repoPath, "x.md", "different\n") if second == first { t.Fatalf("changed bytes produced no new check-in") } } func TestSnapshotReadsPathFromDiskWithoutStdin(t *testing.T) { repoPath := newSnapshotRepo(t) dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "disk.md"), []byte("from disk\n"), 0o644); err != nil { t.Fatal(err) } cmd := exec.Command(os.Args[0], "-R", repoPath, "repo", "snapshot", "-m", "m", "--path", "disk.md") cmd.Dir = dir cmd.Env = append(os.Environ(), "LIBFOSSIL_TEST_MAIN=1") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("snapshot from disk: %v\n%s", err, out) } stdout, _, code := runCLI(t, "-R", repoPath, "repo", "cat", "--at", "tip", "disk.md") if code != 0 || stdout != "from disk\n" { t.Fatalf("cat exited %d, stdout %q; want the disk bytes", code, stdout) } } // TestSnapshotRejectsBadPaths checks that a --path fossil would not accept // as a repository file name fails at exit 1 with a message saying why, and // commits nothing: not a simple pathname, a control byte, or one of the names // fossil reserves (file.c file_is_reserved_name). func TestSnapshotRejectsBadPaths(t *testing.T) { repoPath := newSnapshotRepo(t) for _, tc := range []struct{ path, why string }{ {"../x.md", "clean relative path"}, {"/abs.md", "clean relative path"}, {"a/../../x", "clean relative path"}, {"", "clean relative path"}, {"a//b", "clean relative path"}, {"./x", "clean relative path"}, {"a/", "clean relative path"}, {`a\b.md`, "clean relative path"}, {"a\tb.md", "control character"}, {"a\nb.md", "control character"}, {"x\x1f.md", "control character"}, {"\x7f", ""}, // DEL is not below 0x20: accepted {"_FOSSIL_", "reserved"}, {"sub/_fossil_", "reserved"}, {".fslckout", "reserved"}, {"sub/.FSLCKOUT", "reserved"}, {".fslckout-journal", "reserved"}, {"_FOSSIL_-wal", "reserved"}, {"sub/.fslckout-shm", "reserved"}, {"my_FOSSIL_", ""}, // a tail match without a separator is a plain name } { stdout, stderr, code := runCLIStdin(t, "x", "-R", repoPath, "repo", "snapshot", "-m", "m", "--path", tc.path, "--stdin") if tc.why == "" { if code != 0 { t.Errorf("snapshot --path %q exited %d, want 0: %s", tc.path, code, stderr) } continue } if code != 1 || stdout != "" || !strings.Contains(stderr, tc.why) { t.Errorf("snapshot --path %q: exit %d, stdout %q, stderr %q; want exit 1 naming %q", tc.path, code, stdout, stderr, tc.why) } } } // TestSnapshotRefusesRepoWithoutTrunk checks that a snapshot into a // repository that has check-ins but none its main branch names fails at exit // 1 and commits nothing, rather than starting a second, unrelated root. func TestSnapshotRefusesRepoWithoutTrunk(t *testing.T) { repoPath := newSnapshotRepo(t) snapshot(t, repoPath, "a.md", "one\n") if _, stderr, code := runCLI(t, "-R", repoPath, "repo", "config", "set", "main-branch", "develop"); code != 0 { t.Fatalf("config set main-branch exited %d: %s", code, stderr) } stdout, stderr, code := runCLIStdin(t, "two\n", "-R", repoPath, "repo", "snapshot", "-m", "m", "--user", "tester", "--path", "a.md", "--stdin") if code != 1 || stdout != "" || !strings.Contains(stderr, `"develop"`) { t.Errorf("snapshot with no develop check-in: exit %d, stdout %q, stderr %q; want exit 1 naming the branch", code, stdout, stderr) } r, err := libfossil.Open(repoPath) if err != nil { t.Fatal(err) } defer r.Close() var checkins int if err := r.DB().QueryRow("SELECT count(*) FROM event WHERE type='ci'").Scan(&checkins); err != nil { t.Fatal(err) } if checkins != 1 { t.Errorf("repository has %d check-ins after the refused snapshot, want 1", checkins) } } func TestSnapshotKeepsOtherFilesOnTrunk(t *testing.T) { repoPath := newSnapshotRepo(t) snapshot(t, repoPath, "a.md", "a\n") snapshot(t, repoPath, "b.md", "b\n") stdout, _, code := runCLI(t, "-R", repoPath, "repo", "ls", "tip") if code != 0 || strings.Join(strings.Fields(stdout), " ") != "a.md b.md" { t.Fatalf("ls tip = %q (exit %d), want a.md and b.md", stdout, code) } trunk, _, _ := runCLI(t, "-R", repoPath, "repo", "resolve", "trunk") tip, _, _ := runCLI(t, "-R", repoPath, "repo", "resolve", "tip") if trunk != tip || !strings.Contains(trunk, "uuid: ") { t.Fatalf("trunk %q is not the tip %q", trunk, tip) } } func TestLogPathListsOnlyCheckinsThatChangedIt(t *testing.T) { repoPath := newSnapshotRepo(t) a1 := snapshot(t, repoPath, "a.md", "one\n") snapshot(t, repoPath, "b.md", "other\n") a2 := snapshot(t, repoPath, "a.md", "two\n") a3 := snapshot(t, repoPath, "a.md", "three\n") rows := pathLog(t, repoPath, "--path", "a.md") if len(rows) != 3 { t.Fatalf("log --path a.md has %d rows, want 3: %+v", len(rows), rows) } wantUUID := []string{a3, a2, a1} wantAction := []string{"modified", "modified", "added"} timeRE := regexp.MustCompile(`^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$`) for i, r := range rows { if r.UUID != wantUUID[i] || r.Action != wantAction[i] { t.Errorf("row %d = %s %s, want %s %s", i, r.UUID, r.Action, wantUUID[i], wantAction[i]) } if r.User != "tester" || r.Comment != "snap a.md" { t.Errorf("row %d user/comment = %q/%q", i, r.User, r.Comment) } if !uuidRE.MatchString(r.FileUUID) { t.Errorf("row %d file_uuid = %q", i, r.FileUUID) } if !timeRE.MatchString(r.Time) { t.Errorf("row %d time = %q, want RFC 3339 UTC with milliseconds", i, r.Time) } } if got := pathLog(t, repoPath, "--path", "a.md", "-n", "2"); len(got) != 2 || got[0].UUID != a3 { t.Errorf("log -n 2 = %+v, want the two newest", got) } if got := pathLog(t, repoPath, "--path", "b.md"); len(got) != 1 { t.Errorf("log --path b.md has %d rows, want 1", len(got)) } } // TestLogTimeIsTheDCard checks that each log row's time is its check-in's D // card to the millisecond. Converting the stored Julian day by truncation put // most rows 1ms early; a dozen check-ins make that near certain to show. func TestLogTimeIsTheDCard(t *testing.T) { repoPath := newSnapshotRepo(t) for i := 0; i < 12; i++ { snapshot(t, repoPath, "a.md", fmt.Sprintf("v%d\n", i)) } rows := pathLog(t, repoPath, "--path", "a.md") if len(rows) != 12 { t.Fatalf("log has %d rows, want 12", len(rows)) } for i, r := range rows { if want := dCard(t, repoPath, r.UUID) + "Z"; r.Time != want { t.Errorf("row %d time = %s, its D card says %s", i, r.Time, want) } } } // dCard returns the D card of check-in uuid, as the manifest records it. func dCard(t *testing.T, repoPath, uuid string) string { t.Helper() stdout, stderr, code := runCLI(t, "-R", repoPath, "repo", "cat", uuid) if code != 0 { t.Fatalf("repo cat %s exited %d: %s", uuid, code, stderr) } for _, line := range strings.Split(stdout, "\n") { if d, ok := strings.CutPrefix(line, "D "); ok { return d } } t.Fatalf("check-in %s has no D card:\n%s", uuid, stdout) return "" } func TestLogPathTextOutput(t *testing.T) { repoPath := newSnapshotRepo(t) uuid := snapshot(t, repoPath, "a.md", "one\n") stdout, stderr, code := runCLI(t, "-R", repoPath, "repo", "log", "--path", "a.md") if code != 0 { t.Fatalf("log exited %d: %s", code, stderr) } if !strings.HasPrefix(stdout, uuid[:10]) || !strings.Contains(stdout, "snap a.md") { t.Fatalf("log text = %q, want the short uuid and comment", stdout) } } func TestLogUnknownPathExitsNotFound(t *testing.T) { repoPath := newSnapshotRepo(t) snapshot(t, repoPath, "a.md", "one\n") stdout, _, code := runCLI(t, "-R", repoPath, "repo", "log", "--json", "--path", "missing.md") if code != exitNotFound || stdout != "" { t.Fatalf("log of unknown path: exit %d, stdout %q; want exit %d and no stdout", code, stdout, exitNotFound) } } func TestCatAtVersions(t *testing.T) { repoPath := newSnapshotRepo(t) v1 := snapshot(t, repoPath, "a.md", "one\n") v2 := snapshot(t, repoPath, "a.md", "two\n") for _, tc := range []struct{ at, want string }{ {v1, "one\n"}, {v2, "two\n"}, {"tip", "two\n"}, {v1[:12], "one\n"}, {"trunk", "two\n"}, } { stdout, stderr, code := runCLI(t, "-R", repoPath, "repo", "cat", "--at", tc.at, "a.md") if code != 0 || stdout != tc.want { t.Errorf("cat --at %s: exit %d stdout %q stderr %q, want %q", tc.at, code, stdout, stderr, tc.want) } } } func TestCatAtMissingExitsNotFound(t *testing.T) { repoPath := newSnapshotRepo(t) v1 := snapshot(t, repoPath, "a.md", "one\n") if _, _, code := runCLI(t, "-R", repoPath, "repo", "cat", "--at", v1, "nope.md"); code != exitNotFound { t.Errorf("cat of a missing path exited %d, want %d", code, exitNotFound) } if _, _, code := runCLI(t, "-R", repoPath, "repo", "cat", "--at", "deadbeefdead", "a.md"); code != exitNotFound { t.Errorf("cat at a missing version exited %d, want %d", code, exitNotFound) } } func TestCatArtifactStillWorks(t *testing.T) { repoPath := newSnapshotRepo(t) uuid := snapshot(t, repoPath, "a.md", "one\n") stdout, _, code := runCLI(t, "-R", repoPath, "repo", "cat", uuid) if code != 0 || !strings.Contains(stdout, "F a.md") { t.Fatalf("cat of a check-in artifact: exit %d stdout %q, want its manifest", code, stdout) } } func TestDiffJSONBetweenVersions(t *testing.T) { repoPath := newSnapshotRepo(t) v1 := snapshot(t, repoPath, "a.md", "one\n") v2 := snapshot(t, repoPath, "a.md", "two\n") stdout, stderr, code := runCLI(t, "-R", repoPath, "repo", "diff", "--json", v1, v2, "a.md") if code != 0 { t.Fatalf("diff exited %d: %s", code, stderr) } var entries []struct { Name string `json:"name"` Unified string `json:"unified"` } if err := json.Unmarshal([]byte(stdout), &entries); err != nil { t.Fatalf("diff --json is not a JSON array: %v\n%s", err, stdout) } if len(entries) != 1 || entries[0].Name != "a.md" || !strings.Contains(entries[0].Unified, "-one") || !strings.Contains(entries[0].Unified, "+two") { t.Fatalf("diff --json = %+v, want a.md one -> two", entries) } stdout, _, code = runCLI(t, "-R", repoPath, "repo", "diff", "--json", v2, v2, "a.md") if code != 0 || strings.TrimSpace(stdout) != "[]" { t.Fatalf("diff of identical versions: exit %d stdout %q, want []", code, stdout) } stdout, _, code = runCLI(t, "-R", repoPath, "repo", "diff", v1, v2, "a.md") if code != 0 || !strings.Contains(stdout, "+two") { t.Fatalf("diff text: exit %d stdout %q", code, stdout) } } // TestDiffPathMatchesFossil checks diff A B PATH against fossil diff --from A // --to B PATH on the same repository. PATH names a file, or a directory // holding files, in either version; one in neither is an error ("not found", // exit 2 here), not an empty diff. An unchanged file is an empty diff. func TestDiffPathMatchesFossil(t *testing.T) { fossilBin := testutil.RequireFossilBin(t) repoPath := newSnapshotRepo(t) v1 := snapshot(t, repoPath, "notes/a.md", "one\n") snapshot(t, repoPath, "keep.md", "same\n") v2 := snapshot(t, repoPath, "notes/a.md", "two\n") for _, tc := range []struct { path string found bool changed bool }{ {"notes/a.md", true, true}, {"notes", true, true}, {"keep.md", true, false}, {"nosuch.md", false, false}, {"note", false, false}, // a name prefix that is not a directory } { fossilOut, ferr := exec.Command(fossilBin, "diff", "-R", repoPath, "--from", v1, "--to", v2, tc.path).CombinedOutput() if (ferr == nil) != tc.found { t.Fatalf("fossil diff %s: err %v, the case expects found=%v\n%s", tc.path, ferr, tc.found, fossilOut) } stdout, stderr, code := runCLI(t, "-R", repoPath, "repo", "diff", "--json", v1, v2, tc.path) if !tc.found { if code != exitNotFound || stdout != "" || !strings.Contains(stderr, "not found") { t.Errorf("diff %s: exit %d, stdout %q, stderr %q; fossil fails (%s), want exit %d naming it not found", tc.path, code, stdout, stderr, strings.TrimSpace(string(fossilOut)), exitNotFound) } continue } var entries []struct { Name string `json:"name"` } if code != 0 || json.Unmarshal([]byte(stdout), &entries) != nil { t.Errorf("diff %s: exit %d, stdout %q, stderr %q; fossil succeeds", tc.path, code, stdout, stderr) continue } if got := len(entries) == 1 && entries[0].Name == "notes/a.md"; got != tc.changed { t.Errorf("diff %s = %+v, want changed=%v", tc.path, entries, tc.changed) } if fossilChanged := strings.Contains(string(fossilOut), "+two"); fossilChanged != tc.changed { t.Errorf("fossil diff %s changed=%v, the case expects %v", tc.path, fossilChanged, tc.changed) } } } func TestDiffMissingVersionExitsNotFound(t *testing.T) { repoPath := newSnapshotRepo(t) v1 := snapshot(t, repoPath, "a.md", "one\n") if _, _, code := runCLI(t, "-R", repoPath, "repo", "diff", "--json", v1, "deadbeefdead", "a.md"); code != exitNotFound { t.Errorf("diff at a missing version exited %d, want %d", code, exitNotFound) } } func TestDiffJSONNeedsTwoVersions(t *testing.T) { repoPath := newSnapshotRepo(t) if _, _, code := runCLI(t, "-R", repoPath, "repo", "diff", "--json"); code != 1 { t.Errorf("diff --json with no versions exited %d, want 1 (the parser accepts it; the command refuses it)", code) } } // exitUsage is the status the command-line parser exits with on a usage // error, as cli-embedding.md documents. const exitUsage = 80 // TestUsageErrorsExit80 pins the documented usage-error status, measured on // the binary itself: `go run` reports any non-zero status as 1. func TestUsageErrorsExit80(t *testing.T) { repoPath := newSnapshotRepo(t) for _, args := range [][]string{ {"repo", "log", "--path", "a.md", "--bogus"}, // unknown flag {"repo", "log"}, // missing required flag {"repo", "snapshot", "--path", "a.md", "--stdin"}, // missing required -m {"repo", "nosuch"}, // unknown subcommand {"repo", "cat"}, // missing argument } { stdout, stderr, code := runCLI(t, append([]string{"-R", repoPath}, args...)...) if code != exitUsage || stdout != "" { t.Errorf("%v: exit %d, stdout %q; want exit %d and no stdout\nstderr: %s", args, code, stdout, exitUsage, stderr) } } } // TestSnapshotConcurrentProcesses runs two processes committing to one // repository at once. Every commit must land, on one linear trunk whose tip // holds every file (no commit built on a stale tip loses another's file), and // the repository must pass canonical fossil's integrity check. func TestSnapshotConcurrentProcesses(t *testing.T) { fossilBin := testutil.RequireFossilBin(t) repoPath := newSnapshotRepo(t) const procs, perProc = 2, 8 var wg sync.WaitGroup failures := make(chan string, procs*perProc) for p := 0; p < procs; p++ { wg.Add(1) go func(p int) { defer wg.Done() for i := 0; i < perProc; i++ { name := fmt.Sprintf("p%d/note%d.md", p, i) _, stderr, code := runCLIStdin(t, fmt.Sprintf("body %d %d\n", p, i), "-R", repoPath, "repo", "snapshot", "-m", "snap "+name, "--user", "tester", "--path", name, "--stdin") if code != 0 { failures <- fmt.Sprintf("%s exited %d: %s", name, code, stderr) } } }(p) } wg.Wait() close(failures) for f := range failures { t.Error(f) } r, err := libfossil.Open(repoPath) if err != nil { t.Fatal(err) } defer r.Close() rid, err := r.ResolveVersion("trunk") if err != nil { t.Fatal(err) } files, err := r.ListFiles(rid) if err != nil { t.Fatal(err) } if len(files) != procs*perProc { t.Errorf("trunk tip holds %d files, want %d (a commit built on a stale tip dropped files)", len(files), procs*perProc) } var leaves int if err := r.DB().QueryRow( "SELECT count(*) FROM leaf WHERE rid IN (SELECT objid FROM event WHERE type='ci')", ).Scan(&leaves); err != nil { t.Fatal(err) } if leaves != 1 { t.Errorf("repository has %d leaves, want 1 (the trunk forked)", leaves) } if err := r.Verify(); err != nil { t.Errorf("Verify: %v", err) } out, err := exec.Command(fossilBin, "test-integrity", repoPath).CombinedOutput() if err != nil { t.Errorf("fossil test-integrity: %v\n%s", err, out) } } // TestLogPathAcrossRenameMatchesFinfo builds a history with the fossil // binary (add a.md, edit, rename to b.md, edit) and compares repo log with // fossil finfo on the same repository. Like finfo, log of the new name stops // at the rename, which it reports with the old name; it does not continue // into the old name's history. Unlike finfo, which lists only check-ins that // hold a version of the file, log of the old name also lists the rename as // the check-in that deleted it. func TestLogPathAcrossRenameMatchesFinfo(t *testing.T) { fossilBin := testutil.RequireFossilBin(t) dir := t.TempDir() repoPath := filepath.Join(dir, "r.fossil") ckout := filepath.Join(dir, "ckout") if err := os.Mkdir(ckout, 0o755); err != nil { t.Fatal(err) } fossil := func(args ...string) string { t.Helper() cmd := exec.Command(fossilBin, args...) cmd.Dir = ckout cmd.Env = append(os.Environ(), "USER=tester") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("fossil %v: %v\n%s", args, err, out) } return string(out) } write := func(name, content string) { t.Helper() if err := os.WriteFile(filepath.Join(ckout, name), []byte(content), 0o644); err != nil { t.Fatal(err) } } fossil("init", "--admin-user", "tester", repoPath) fossil("open", repoPath) write("a.md", "one\n") fossil("add", "a.md") fossil("commit", "-m", "add") write("a.md", "two two\n") fossil("commit", "-m", "edit a") beforeRename := strings.Fields(fossil("timeline", "current", "-n", "1", "-F", "%H"))[0] fossil("mv", "--hard", "a.md", "b.md") fossil("commit", "-m", "rename") write("b.md", "three three three\n") fossil("commit", "-m", "edit b") // finfo -b prints one check-in per line, its hash prefix first. finfo := func(name string) []string { var ids []string for _, line := range strings.Split(strings.TrimSpace(fossil("finfo", "-b", name)), "\n") { ids = append(ids, strings.Fields(line)[0]) } return ids } short := func(rows []logRow) []string { var ids []string for _, r := range rows { ids = append(ids, r.UUID[:10]) } return ids } rows := pathLog(t, repoPath, "--path", "b.md") if got, want := short(rows), finfo("b.md"); strings.Join(got, " ") != strings.Join(want, " ") { t.Fatalf("log b.md = %v, fossil finfo b.md = %v", got, want) } if len(rows) != 2 || rows[1].Action != "renamed" || rows[1].PrevName != "a.md" || rows[1].Comment != "rename" { t.Fatalf("log b.md = %+v, want the edit, then the rename from a.md, and nothing before it", rows) } // finfo needs the file in the checkout, so look at a.md from before the // rename. fossil("update", beforeRename) old := pathLog(t, repoPath, "--path", "a.md") if len(old) == 0 || old[0].Action != "deleted" || old[0].UUID != rows[1].UUID { t.Fatalf("log a.md = %+v, want the rename first, as the deletion of a.md", old) } if got, want := short(old[1:]), finfo("a.md"); strings.Join(got, " ") != strings.Join(want, " ") { t.Fatalf("log a.md after the deletion = %v, fossil finfo a.md = %v", got, want) } }