Fold the unpublished projects into the overview table
They were a list underneath the table, which meant reading the same project names in two different shapes. They are rows now, with the action in an "init" column that only appears when some row needs it, and they sort to the bottom as their own group: an un-inited directory is a different kind of task and should not push the daily ones down. Every directory under the base gets a row, not just the repositories -- `init` is exactly what turns a plain directory into a project, so leaving those out would have hidden the ones the column is for. Such a row has no git state to show and costs no subprocesses either, since projectStatus now checks for .git before running any. The count line gained "N to init"; the projects count still counts repositories, so the two numbers stay meaningful side by side. An unreachable server marks nothing at all, as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -157,16 +157,18 @@ thing that sees the local base directory *and* the git server at once.
|
|||||||
```
|
```
|
||||||
< src > overview
|
< src > overview
|
||||||
notes * ↑2 laptop 3h
|
notes * ↑2 laptop 3h
|
||||||
website * ✓ desktop 2d → hub
|
website * ✓ desktop 2d → hub
|
||||||
Betaflight3.0.0 ✓ workstation 20d → gitea hub
|
Betaflight3.0.0 ✓ workstation 20d → gitea hub
|
||||||
scratch (wip) – laptop 1h
|
experiments init
|
||||||
4 projects · 2 dirty · 2 in sync
|
sandbox (wip) – laptop 1h init
|
||||||
not on the server (init) experiments, sandbox
|
4 projects · 2 dirty · 2 in sync · 2 to init
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
hunting along each line. The projects that need something done come first;
|
hunting along each line. The projects that need something done come first, the
|
||||||
within a group the order stays alphabetical, so positions do not jump around.
|
settled ones next, and the directories the git server does not have yet come
|
||||||
|
last — those are a different kind of task. Within each group the order stays
|
||||||
|
alphabetical, so positions do not jump around.
|
||||||
|
|
||||||
| column | meaning |
|
| column | meaning |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -175,19 +177,24 @@ within a group the order stays alphabetical, so positions do not jump around.
|
|||||||
| `↑n` `↓n` | commits ahead of / behind the upstream (`↑2↓1` when both) |
|
| `↑n` `↓n` | commits ahead of / behind the upstream (`↑2↓1` when both) |
|
||||||
| `✓` | in sync with the upstream |
|
| `✓` | in sync with the upstream |
|
||||||
| `–` | the branch tracks nothing — never pushed |
|
| `–` | the branch tracks nothing — never pushed |
|
||||||
|
| `init` | the git server does not have this one; run `init` |
|
||||||
| host, age | who last committed and when, from the `[user@host]` stamp |
|
| host, age | who last committed and when, from the `[user@host]` stamp |
|
||||||
| `→` | mirror targets this repository has a remote for |
|
| `→` | mirror targets this repository has a remote for |
|
||||||
|
|
||||||
|
Every directory under the base gets a row, including those that are not
|
||||||
|
repositories at all — `init` is exactly what turns one into a project, so it
|
||||||
|
belongs in the table rather than in a list underneath it. Such a row simply has
|
||||||
|
no git state to show.
|
||||||
|
|
||||||
The host and age come from the commit itself — `push` writes `[user@host]` into
|
The host and age come from the commit itself — `push` writes `[user@host]` into
|
||||||
every message, so `overview` can say where a project was last worked on without
|
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 line at the end is the join no git command can do: the local projects the
|
The `init` column is the join no git command can do. If the server cannot be
|
||||||
git server has never seen, which are the ones `init` is for. If the server
|
reached, no row is marked and mgsh says so — not knowing is not the same as
|
||||||
cannot be reached, mgsh says so instead of claiming everything is missing. The
|
knowing they are missing. The other direction, repositories on the server that
|
||||||
other direction — repositories on the server that are not here — is what `list`
|
are not here, is what `list` shows.
|
||||||
shows.
|
|
||||||
|
|
||||||
### Credential check
|
### Credential check
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -830,6 +830,7 @@ func TestFormatProjStatus(t *testing.T) {
|
|||||||
[]string{"g", "laptop", "→ hub gitea"}, nil},
|
[]string{"g", "laptop", "→ hub gitea"}, nil},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
c.s.isRepo = true // these all describe real repositories
|
||||||
got := formatProjStatus(c.s, w)
|
got := formatProjStatus(c.s, w)
|
||||||
for _, sub := range c.contains {
|
for _, sub := range c.contains {
|
||||||
if !strings.Contains(got, sub) {
|
if !strings.Contains(got, sub) {
|
||||||
@@ -878,23 +879,24 @@ func TestOverviewColumnsAlign(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAttentionRank(t *testing.T) {
|
func TestAttentionRank(t *testing.T) {
|
||||||
needs := []projStatus{
|
ranks := []struct {
|
||||||
{dirty: true},
|
s projStatus
|
||||||
{ahead: 1},
|
want int
|
||||||
{behind: 1},
|
}{
|
||||||
|
{projStatus{isRepo: true, dirty: true}, 0},
|
||||||
|
{projStatus{isRepo: true, ahead: 1}, 0},
|
||||||
|
{projStatus{isRepo: true, behind: 1}, 0},
|
||||||
|
{projStatus{isRepo: true, hasUpstream: true}, 1},
|
||||||
|
{projStatus{isRepo: true}, 1}, // clean, no upstream
|
||||||
|
// not on the server is a different kind of task and goes last, even
|
||||||
|
// when the working tree is dirty — it cannot be pushed anyway
|
||||||
|
{projStatus{isRepo: true, notOnServer: true}, 2},
|
||||||
|
{projStatus{isRepo: true, dirty: true, notOnServer: true}, 2},
|
||||||
|
{projStatus{notOnServer: true}, 2},
|
||||||
}
|
}
|
||||||
quiet := []projStatus{
|
for _, c := range ranks {
|
||||||
{hasUpstream: true},
|
if got := attentionRank(c.s); got != c.want {
|
||||||
{}, // clean, no upstream: nothing to do about it here
|
t.Errorf("attentionRank(%+v) = %d, want %d", c.s, got, c.want)
|
||||||
}
|
|
||||||
for _, s := range needs {
|
|
||||||
if attentionRank(s) != 0 {
|
|
||||||
t.Errorf("%+v should sort to the top", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range quiet {
|
|
||||||
if attentionRank(s) != 1 {
|
|
||||||
t.Errorf("%+v should sort below the ones needing action", s)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-39
@@ -24,9 +24,11 @@ import (
|
|||||||
type projStatus struct {
|
type projStatus struct {
|
||||||
name string
|
name string
|
||||||
branch string
|
branch string
|
||||||
|
isRepo bool // has a .git of its own
|
||||||
dirty bool
|
dirty bool
|
||||||
ahead, behind int
|
ahead, behind int
|
||||||
hasUpstream bool
|
hasUpstream bool
|
||||||
|
notOnServer bool // known to be missing from the git server
|
||||||
lastHost string // machine that made the last commit, from "[user@host]"
|
lastHost string // machine that made the last commit, from "[user@host]"
|
||||||
lastWhen time.Time // when that was
|
lastWhen time.Time // when that was
|
||||||
mirrors []string // configured mirror remotes present in this repo
|
mirrors []string // configured mirror remotes present in this repo
|
||||||
@@ -60,19 +62,19 @@ func overviewAll() {
|
|||||||
srvCh <- serverList{names, err}
|
srvCh <- serverList{names, err}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var local, repos []string
|
// every directory gets a row, repository or not: one that is not a
|
||||||
|
// repository yet is exactly what `init` is for, and putting it in the table
|
||||||
|
// beats a separate list underneath
|
||||||
|
var local []string
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
|
||||||
continue
|
local = append(local, e.Name())
|
||||||
}
|
|
||||||
local = append(local, e.Name())
|
|
||||||
if isDir(BASE + "/" + e.Name() + "/.git") {
|
|
||||||
repos = append(repos, e.Name())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := scanProjects(repos)
|
rows := scanProjects(local)
|
||||||
srv := <-srvCh
|
srv := <-srvCh
|
||||||
|
markUnpublished(rows, srv.names, srv.err)
|
||||||
|
|
||||||
// what needs doing first, alphabetical within each group
|
// what needs doing first, alphabetical within each group
|
||||||
sort.SliceStable(rows, func(i, j int) bool {
|
sort.SliceStable(rows, func(i, j int) bool {
|
||||||
@@ -80,12 +82,21 @@ func overviewAll() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
fmt.Println(col(cGray, "no git projects under "+BASE))
|
fmt.Println(col(cGray, "nothing under "+BASE))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w := measureOverview(rows)
|
w := measureOverview(rows)
|
||||||
dirtyN, syncN := 0, 0
|
repoN, dirtyN, syncN, initN := 0, 0, 0, 0
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
fmt.Println(formatProjStatus(r, w))
|
fmt.Println(formatProjStatus(r, w))
|
||||||
|
if r.notOnServer {
|
||||||
|
initN++
|
||||||
|
}
|
||||||
|
if !r.isRepo {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
repoN++
|
||||||
if r.dirty {
|
if r.dirty {
|
||||||
dirtyN++
|
dirtyN++
|
||||||
}
|
}
|
||||||
@@ -93,12 +104,15 @@ func overviewAll() {
|
|||||||
syncN++
|
syncN++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(rows) > 0 {
|
|
||||||
fmt.Println(col(cGray, fmt.Sprintf("%d projects · %d dirty · %d in sync",
|
|
||||||
len(rows), dirtyN, syncN)))
|
|
||||||
}
|
|
||||||
|
|
||||||
reportUnpublished(local, srv.names, srv.err)
|
summary := fmt.Sprintf("%d projects · %d dirty · %d in sync", repoN, dirtyN, syncN)
|
||||||
|
if initN > 0 {
|
||||||
|
summary += fmt.Sprintf(" · %d to init", initN)
|
||||||
|
}
|
||||||
|
fmt.Println(col(cGray, summary))
|
||||||
|
if srv.err != nil {
|
||||||
|
fmt.Println(col(cGray, " git server not reachable — local view only"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanProjects collects the state of every project concurrently. Each project
|
// scanProjects collects the state of every project concurrently. Each project
|
||||||
@@ -120,36 +134,31 @@ func scanProjects(names []string) []projStatus {
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// reportUnpublished names the local projects the git server has never seen —
|
// markUnpublished flags the rows the git server has never seen — the ones
|
||||||
// the ones `init` is for. The reverse direction, server repositories missing
|
// `init` is for. A listing that failed leaves every row unmarked: not knowing
|
||||||
// here, is what `list` is for and is not repeated.
|
// is not the same as knowing they are missing, and marking all of them would
|
||||||
func reportUnpublished(local, server []string, err error) {
|
// tell the user to re-init their whole base directory.
|
||||||
|
func markUnpublished(rows []projStatus, server []string, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(col(cGray, " git server not reachable — local view only"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onServer := map[string]bool{}
|
onServer := map[string]bool{}
|
||||||
for _, n := range server {
|
for _, n := range server {
|
||||||
onServer[n] = true
|
onServer[n] = true
|
||||||
}
|
}
|
||||||
|
for i := range rows {
|
||||||
var missing []string
|
rows[i].notOnServer = !onServer[rows[i].name]
|
||||||
for _, n := range local {
|
|
||||||
if !onServer[n] {
|
|
||||||
missing = append(missing, n)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(missing) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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. A
|
||||||
|
// directory without a repository is reported as it is, and costs no
|
||||||
|
// subprocesses at all.
|
||||||
func projectStatus(name, dir string) projStatus {
|
func projectStatus(name, dir string) projStatus {
|
||||||
s := projStatus{name: name, branch: "-"}
|
s := projStatus{name: name, branch: "-"}
|
||||||
|
if s.isRepo = isDir(dir + "/.git"); !s.isRepo {
|
||||||
|
return s
|
||||||
|
}
|
||||||
readStatus(&s, dir)
|
readStatus(&s, dir)
|
||||||
readLastCommit(&s, dir)
|
readLastCommit(&s, dir)
|
||||||
s.mirrors = configuredMirrors(dir)
|
s.mirrors = configuredMirrors(dir)
|
||||||
@@ -232,7 +241,13 @@ func configuredMirrors(dir string) []string {
|
|||||||
// overviewWidths are the column widths of the overview table, measured from the
|
// overviewWidths are the column widths of the overview table, measured from the
|
||||||
// rows so every field starts at the same place. Ragged columns were what made
|
// rows so every field starts at the same place. Ragged columns were what made
|
||||||
// the old one-line-per-project output hard to read.
|
// the old one-line-per-project output hard to read.
|
||||||
type overviewWidths struct{ label, sync, host int }
|
type overviewWidths struct {
|
||||||
|
label, sync, host int
|
||||||
|
hint bool // any row carries an action hint
|
||||||
|
}
|
||||||
|
|
||||||
|
// hintWidth is the width of the action column, sized for its only word.
|
||||||
|
const hintWidth = 4
|
||||||
|
|
||||||
// measureOverview sizes the columns for a set of rows.
|
// measureOverview sizes the columns for a set of rows.
|
||||||
func measureOverview(rows []projStatus) overviewWidths {
|
func measureOverview(rows []projStatus) overviewWidths {
|
||||||
@@ -241,6 +256,7 @@ func measureOverview(rows []projStatus) overviewWidths {
|
|||||||
w.label = max(w.label, utf8.RuneCountInString(projLabel(r)))
|
w.label = max(w.label, utf8.RuneCountInString(projLabel(r)))
|
||||||
w.sync = max(w.sync, utf8.RuneCountInString(syncState(r)))
|
w.sync = max(w.sync, utf8.RuneCountInString(syncState(r)))
|
||||||
w.host = max(w.host, utf8.RuneCountInString(r.lastHost))
|
w.host = max(w.host, utf8.RuneCountInString(r.lastHost))
|
||||||
|
w.hint = w.hint || r.notOnServer
|
||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -261,6 +277,8 @@ func projLabel(s projStatus) string {
|
|||||||
// following field out of line.
|
// following field out of line.
|
||||||
func syncState(s projStatus) string {
|
func syncState(s projStatus) string {
|
||||||
switch {
|
switch {
|
||||||
|
case !s.isRepo:
|
||||||
|
return "" // nothing to compare: there is no repository here yet
|
||||||
case s.ahead > 0 && s.behind > 0:
|
case s.ahead > 0 && s.behind > 0:
|
||||||
return fmt.Sprintf("↑%d↓%d", s.ahead, s.behind)
|
return fmt.Sprintf("↑%d↓%d", s.ahead, s.behind)
|
||||||
case s.ahead > 0:
|
case s.ahead > 0:
|
||||||
@@ -287,13 +305,20 @@ func syncColor(s projStatus) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// attentionRank sorts the rows worth acting on to the top. With many projects,
|
// attentionRank groups the rows: work in progress at the top, then everything
|
||||||
// scanning the whole list for the two dirty ones is the actual work.
|
// that is settled, and last the directories the server does not have yet. With
|
||||||
|
// many projects, scanning the whole list for the two dirty ones is the actual
|
||||||
|
// work — and an un-inited directory is a different kind of task, not something
|
||||||
|
// to push past the daily ones.
|
||||||
func attentionRank(s projStatus) int {
|
func attentionRank(s projStatus) int {
|
||||||
if s.dirty || s.ahead > 0 || s.behind > 0 {
|
switch {
|
||||||
|
case s.notOnServer:
|
||||||
|
return 2
|
||||||
|
case s.dirty || s.ahead > 0 || s.behind > 0:
|
||||||
return 0
|
return 0
|
||||||
|
default:
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatProjStatus renders one row of the overview table.
|
// formatProjStatus renders one row of the overview table.
|
||||||
@@ -317,10 +342,17 @@ func formatProjStatus(s projStatus, w overviewWidths) string {
|
|||||||
b.WriteString(" " + col(cGray, padRight(s.lastHost, w.host)))
|
b.WriteString(" " + col(cGray, padRight(s.lastHost, w.host)))
|
||||||
b.WriteString(" " + col(cGray, fmt.Sprintf("%4s", age)))
|
b.WriteString(" " + col(cGray, fmt.Sprintf("%4s", age)))
|
||||||
}
|
}
|
||||||
|
if w.hint {
|
||||||
|
hint := ""
|
||||||
|
if s.notOnServer {
|
||||||
|
hint = "init"
|
||||||
|
}
|
||||||
|
b.WriteString(" " + col(cYellow, padRight(hint, hintWidth)))
|
||||||
|
}
|
||||||
if len(s.mirrors) > 0 {
|
if len(s.mirrors) > 0 {
|
||||||
b.WriteString(col(cGray, " → "+strings.Join(s.mirrors, " ")))
|
b.WriteString(col(cGray, " → "+strings.Join(s.mirrors, " ")))
|
||||||
}
|
}
|
||||||
return b.String()
|
return strings.TrimRight(b.String(), " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortAge renders a duration compactly: 90s -> "1m", 36h -> "1d".
|
// shortAge renders a duration compactly: 90s -> "1m", 36h -> "1d".
|
||||||
|
|||||||
+51
-30
@@ -82,42 +82,63 @@ func TestCommitHostRe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReportUnpublishedUnreachableServer: when the server cannot be listed, the
|
// TestMarkUnpublishedUnreachableServer: a listing that failed must leave every
|
||||||
// overview must say so rather than claim every project is missing there.
|
// row unmarked. Not knowing is not the same as knowing they are missing —
|
||||||
func TestReportUnpublishedUnreachableServer(t *testing.T) {
|
// marking all of them would tell the user to re-init their whole base.
|
||||||
out := captureStdout(t, func() {
|
func TestMarkUnpublishedUnreachableServer(t *testing.T) {
|
||||||
reportUnpublished([]string{"a", "b"}, nil, errors.New("network is unreachable"))
|
rows := []projStatus{{name: "a"}, {name: "b"}}
|
||||||
})
|
markUnpublished(rows, nil, errors.New("network is unreachable"))
|
||||||
if strings.Contains(out, "not on the server") {
|
for _, r := range rows {
|
||||||
t.Errorf("an unreachable server was reported as missing repositories: %q", out)
|
if r.notOnServer {
|
||||||
}
|
t.Errorf("%s marked as missing although the server could not be listed", r.name)
|
||||||
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
|
// TestMarkUnpublished flags only what the server really does not have.
|
||||||
// seen. The other direction — repositories there but not here — is what `list`
|
func TestMarkUnpublished(t *testing.T) {
|
||||||
// is for, and repeating it here only added noise.
|
rows := []projStatus{{name: "both"}, {name: "onlyhere"}}
|
||||||
func TestReportUnpublished(t *testing.T) {
|
markUnpublished(rows, []string{"both", "onlythere"}, nil)
|
||||||
out := captureStdout(t, func() {
|
if rows[0].notOnServer {
|
||||||
reportUnpublished([]string{"both", "onlyhere"}, []string{"both", "onlythere"}, nil)
|
t.Error("a project present on both sides was marked")
|
||||||
})
|
|
||||||
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") {
|
if !rows[1].notOnServer {
|
||||||
t.Errorf("server-only project should no longer be listed: %q", out)
|
t.Error("a local-only project was not marked")
|
||||||
}
|
|
||||||
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
|
// TestUnpublishedRowsCarryTheHint: the entries live in the table now, with the
|
||||||
if out := captureStdout(t, func() {
|
// action in their own column, rather than in a list underneath it.
|
||||||
reportUnpublished([]string{"both"}, []string{"both"}, nil)
|
func TestUnpublishedRowsCarryTheHint(t *testing.T) {
|
||||||
}); strings.TrimSpace(out) != "" {
|
useColor = false
|
||||||
t.Errorf("nothing to report should print nothing, got %q", out)
|
rows := []projStatus{
|
||||||
|
{name: "published", isRepo: true, hasUpstream: true},
|
||||||
|
{name: "fresh", isRepo: true, notOnServer: true},
|
||||||
|
{name: "notarepo", notOnServer: true},
|
||||||
|
}
|
||||||
|
w := measureOverview(rows)
|
||||||
|
if !w.hint {
|
||||||
|
t.Fatal("hint column not reserved although rows need it")
|
||||||
|
}
|
||||||
|
got := []string{}
|
||||||
|
for _, r := range rows {
|
||||||
|
got = append(got, formatProjStatus(r, w))
|
||||||
|
}
|
||||||
|
if strings.Contains(got[0], "init") {
|
||||||
|
t.Errorf("a published project was hinted: %q", got[0])
|
||||||
|
}
|
||||||
|
for _, i := range []int{1, 2} {
|
||||||
|
if !strings.Contains(got[i], "init") {
|
||||||
|
t.Errorf("row %d missing the init hint: %q", i, got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// a directory that is not a repository has no sync state to report
|
||||||
|
if strings.ContainsAny(got[2], "✓–↑↓") {
|
||||||
|
t.Errorf("non-repository row claims a git state: %q", got[2])
|
||||||
|
}
|
||||||
|
// with nothing to hint the column disappears entirely
|
||||||
|
if measureOverview(rows[:1]).hint {
|
||||||
|
t.Error("hint column reserved although no row needs it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
4.0.38
|
4.0.40
|
||||||
|
|||||||
Reference in New Issue
Block a user