Drop the "not cloned here" line from overview

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>
This commit is contained in:
2026-07-26 17:45:02 +02:00
co-authored by Claude Opus 5
parent cb1ff98c3d
commit 8ca05d6ad2
4 changed files with 37 additions and 45 deletions
+5 -5
View File
@@ -162,7 +162,6 @@ thing that sees the local base directory *and* the git server at once.
scratch (wip) laptop 1h scratch (wip) laptop 1h
4 projects · 2 dirty · 2 in sync 4 projects · 2 dirty · 2 in sync
not on the server (init) experiments, sandbox not on the server (init) experiments, sandbox
not cloned here (clone) oldproject
``` ```
Every field sits in its own column, so the eye can go down one instead of Every field sits in its own column, so the eye can go down one instead of
@@ -184,10 +183,11 @@ every message, so `overview` can say where a project was last worked on without
storing anything. On a setup spanning a laptop and a workstation that is usually storing anything. On a setup spanning a laptop and a workstation that is usually
the piece of information you actually wanted. the piece of information you actually wanted.
The two lists at the end are the join no git command can do: local projects the The line at the end is the join no git command can do: the local projects the
server has never seen (`init` them) and server repositories missing on this git server has never seen, which are the ones `init` is for. If the server
machine (`clone` them). If the server cannot be reached, mgsh says so instead of cannot be reached, mgsh says so instead of claiming everything is missing. The
claiming everything is missing. other direction — repositories on the server that are not here — is what `list`
shows.
### Credential check ### Credential check
+14 -31
View File
@@ -5,9 +5,8 @@ package main
// mgsh is the only thing that sees all three places a project can live: the // mgsh is the only thing that sees all three places a project can live: the
// local base directory, the internal ssh server, and the public mirrors. Joining // local base directory, the internal ssh server, and the public mirrors. Joining
// those answers the questions plain git cannot — which projects were never // those answers the questions plain git cannot — which projects were never
// pushed to the server, which exist there but not on this machine, and which // pushed to the server, and which machine last touched each one (every `push`
// machine last touched each one (every `push` stamps "[user@host]" into the // stamps "[user@host]" into the commit message, so that comes for free).
// commit message, so that comes for free).
import ( import (
"fmt" "fmt"
@@ -99,7 +98,7 @@ func overviewAll() {
len(rows), dirtyN, syncN))) len(rows), dirtyN, syncN)))
} }
reportInventory(local, srv.names, srv.err) reportUnpublished(local, srv.names, srv.err)
} }
// scanProjects collects the state of every project concurrently. Each project // scanProjects collects the state of every project concurrently. Each project
@@ -121,47 +120,31 @@ func scanProjects(names []string) []projStatus {
return rows return rows
} }
// reportInventory names the projects that live on only one side: local ones the // reportUnpublished names the local projects the git server has never seen —
// server has never seen (candidates for `init`) and server repositories missing // the ones `init` is for. The reverse direction, server repositories missing
// here (candidates for `clone`). // here, is what `list` is for and is not repeated.
func reportInventory(local, server []string, err error) { func reportUnpublished(local, server []string, err error) {
if err != nil { if err != nil {
fmt.Println(col(cGray, " git server not reachable — local view only")) fmt.Println(col(cGray, " git server not reachable — local view only"))
return return
} }
have := map[string]bool{}
for _, n := range local {
have[n] = true
}
onServer := map[string]bool{} onServer := map[string]bool{}
for _, n := range server { for _, n := range server {
onServer[n] = true onServer[n] = true
} }
var missingRemote, missingLocal []string var missing []string
for _, n := range local { for _, n := range local {
if !onServer[n] { if !onServer[n] {
missingRemote = append(missingRemote, n) missing = append(missing, n)
} }
} }
for _, n := range server { if len(missing) == 0 {
if !have[n] { return
missingLocal = append(missingLocal, n)
}
}
sort.Strings(missingRemote)
sort.Strings(missingLocal)
// the two labels share a column so their contents line up as well
const labelW = 24
if len(missingRemote) > 0 {
fmt.Printf(" %s %s\n", col(cYellow, padRight("not on the server (init)", labelW)),
col(cGray, strings.Join(missingRemote, ", ")))
}
if len(missingLocal) > 0 {
fmt.Printf(" %s %s\n", col(cCyan, padRight("not cloned here (clone)", labelW)),
col(cGray, strings.Join(missingLocal, ", ")))
} }
sort.Strings(missing)
fmt.Printf(" %s %s\n", col(cYellow, "not on the server (init)"),
col(cGray, strings.Join(missing, ", ")))
} }
// projectStatus gathers the git state of a single project directory. // projectStatus gathers the git state of a single project directory.
+17 -8
View File
@@ -82,11 +82,11 @@ func TestCommitHostRe(t *testing.T) {
} }
} }
// TestReportInventoryUnreachableServer: when the server cannot be listed, the // TestReportUnpublishedUnreachableServer: when the server cannot be listed, the
// overview must say so rather than claim every project is missing there. // overview must say so rather than claim every project is missing there.
func TestReportInventoryUnreachableServer(t *testing.T) { func TestReportUnpublishedUnreachableServer(t *testing.T) {
out := captureStdout(t, func() { out := captureStdout(t, func() {
reportInventory([]string{"a", "b"}, nil, errors.New("network is unreachable")) reportUnpublished([]string{"a", "b"}, nil, errors.New("network is unreachable"))
}) })
if strings.Contains(out, "not on the server") { if strings.Contains(out, "not on the server") {
t.Errorf("an unreachable server was reported as missing repositories: %q", out) t.Errorf("an unreachable server was reported as missing repositories: %q", out)
@@ -96,20 +96,29 @@ func TestReportInventoryUnreachableServer(t *testing.T) {
} }
} }
// TestReportInventorySplitsSides is the join that plain git cannot do. // TestReportUnpublished names only the local projects the server has never
func TestReportInventorySplitsSides(t *testing.T) { // 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() { out := captureStdout(t, func() {
reportInventory([]string{"both", "onlyhere"}, []string{"both", "onlythere"}, nil) reportUnpublished([]string{"both", "onlyhere"}, []string{"both", "onlythere"}, nil)
}) })
if !strings.Contains(out, "not on the server (init)") || !strings.Contains(out, "onlyhere") { if !strings.Contains(out, "not on the server (init)") || !strings.Contains(out, "onlyhere") {
t.Errorf("local-only project not reported: %q", out) t.Errorf("local-only project not reported: %q", out)
} }
if !strings.Contains(out, "not cloned here (clone)") || !strings.Contains(out, "onlythere") { if strings.Contains(out, "onlythere") {
t.Errorf("server-only project not reported: %q", out) t.Errorf("server-only project should no longer be listed: %q", out)
} }
if strings.Contains(out, "both") { if strings.Contains(out, "both") {
t.Errorf("a project present on both sides should not be listed: %q", out) 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) { func TestShortAge(t *testing.T) {
+1 -1
View File
@@ -1 +1 @@
4.0.36 4.0.38