It answered a question `list` already answers, and it did so on every run: the point of the overview is the state of the projects you have, not a second listing of the server. reportInventory became reportUnpublished and now reports one thing -- the local projects the server has never seen, which are the ones `init` is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
package main
|
|
|
|
// overview_test.go — the inventory view: what mgsh knows that plain git cannot.
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestReadStatusParsesPorcelainV2 covers the single `git status` call that
|
|
// replaced three separate ones: branch, upstream, ahead/behind and dirty all
|
|
// come out of its header.
|
|
func TestReadStatusParsesPorcelainV2(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bare := filepath.Join(t.TempDir(), "o.git")
|
|
mustGit(t, "", "init", "--bare", "-q", bare)
|
|
mustGit(t, dir, "init", "-q")
|
|
mustGit(t, dir, "config", "user.name", "t")
|
|
mustGit(t, dir, "config", "user.email", "t@e")
|
|
mustGit(t, dir, "commit", "-q", "--allow-empty", "-m", "[mike@laptop] work")
|
|
mustGit(t, dir, "remote", "add", "origin", bare)
|
|
mustGit(t, dir, "push", "-q", "-u", "origin", "HEAD")
|
|
|
|
var s projStatus
|
|
readStatus(&s, dir)
|
|
if !s.hasUpstream || s.ahead != 0 || s.behind != 0 || s.dirty {
|
|
t.Errorf("clean synced repo = %+v", s)
|
|
}
|
|
if s.branch == "" || s.branch == "-" {
|
|
t.Errorf("branch not read: %q", s.branch)
|
|
}
|
|
|
|
// one unstaged file and one unpushed commit
|
|
if err := os.WriteFile(filepath.Join(dir, "x"), []byte("x"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustGit(t, dir, "commit", "-q", "--allow-empty", "-m", "[mike@desktop] more")
|
|
|
|
s = projStatus{}
|
|
readStatus(&s, dir)
|
|
if !s.dirty {
|
|
t.Error("untracked file did not register as dirty")
|
|
}
|
|
if s.ahead != 1 {
|
|
t.Errorf("ahead = %d, want 1", s.ahead)
|
|
}
|
|
|
|
// and the host stamp `push` writes is picked up
|
|
var l projStatus
|
|
readLastCommit(&l, dir)
|
|
if l.lastHost != "desktop" {
|
|
t.Errorf("lastHost = %q, want desktop", l.lastHost)
|
|
}
|
|
if l.lastWhen.IsZero() {
|
|
t.Error("lastWhen not read")
|
|
}
|
|
}
|
|
|
|
// TestCommitHostRe: only mgsh's own "[user@host]" stamp counts.
|
|
func TestCommitHostRe(t *testing.T) {
|
|
cases := map[string]string{
|
|
"[mike@laptop] fixed a thing": "laptop",
|
|
"[mike@build-01] ": "build-01",
|
|
"[@host] no user": "host",
|
|
"fixed a thing": "",
|
|
"[not a stamp] text": "",
|
|
"see [a@b] mid-line": "",
|
|
}
|
|
for subj, want := range cases {
|
|
got := ""
|
|
if m := commitHostRe.FindStringSubmatch(subj); m != nil {
|
|
got = m[1]
|
|
}
|
|
if got != want {
|
|
t.Errorf("host of %q = %q, want %q", subj, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReportUnpublishedUnreachableServer: when the server cannot be listed, the
|
|
// overview must say so rather than claim every project is missing there.
|
|
func TestReportUnpublishedUnreachableServer(t *testing.T) {
|
|
out := captureStdout(t, func() {
|
|
reportUnpublished([]string{"a", "b"}, nil, errors.New("network is unreachable"))
|
|
})
|
|
if strings.Contains(out, "not on the server") {
|
|
t.Errorf("an unreachable server was reported as missing repositories: %q", out)
|
|
}
|
|
if !strings.Contains(out, "not reachable") {
|
|
t.Errorf("no hint that the server was unreachable: %q", out)
|
|
}
|
|
}
|
|
|
|
// TestReportUnpublished names only the local projects the server has never
|
|
// seen. The other direction — repositories there but not here — is what `list`
|
|
// is for, and repeating it here only added noise.
|
|
func TestReportUnpublished(t *testing.T) {
|
|
out := captureStdout(t, func() {
|
|
reportUnpublished([]string{"both", "onlyhere"}, []string{"both", "onlythere"}, nil)
|
|
})
|
|
if !strings.Contains(out, "not on the server (init)") || !strings.Contains(out, "onlyhere") {
|
|
t.Errorf("local-only project not reported: %q", out)
|
|
}
|
|
if strings.Contains(out, "onlythere") {
|
|
t.Errorf("server-only project should no longer be listed: %q", out)
|
|
}
|
|
if strings.Contains(out, "both") {
|
|
t.Errorf("a project present on both sides should not be listed: %q", out)
|
|
}
|
|
|
|
// nothing unpublished: no line at all, not an empty label
|
|
if out := captureStdout(t, func() {
|
|
reportUnpublished([]string{"both"}, []string{"both"}, nil)
|
|
}); strings.TrimSpace(out) != "" {
|
|
t.Errorf("nothing to report should print nothing, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestShortAge(t *testing.T) {
|
|
cases := []struct {
|
|
d time.Duration
|
|
want string
|
|
}{
|
|
{30 * time.Second, "now"},
|
|
{90 * time.Second, "1m"},
|
|
{2 * time.Hour, "2h"},
|
|
{36 * time.Hour, "1d"},
|
|
{20 * 24 * time.Hour, "20d"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := shortAge(c.d); got != c.want {
|
|
t.Errorf("shortAge(%v) = %q, want %q", c.d, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// captureStdout collects everything a function prints.
|
|
func captureStdout(t *testing.T, fn func()) string {
|
|
t.Helper()
|
|
old := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.Stdout = w
|
|
done := make(chan string)
|
|
go func() {
|
|
var b strings.Builder
|
|
io.Copy(&b, r)
|
|
done <- b.String()
|
|
}()
|
|
fn()
|
|
w.Close()
|
|
os.Stdout = old
|
|
return <-done
|
|
}
|