Format list as an aligned table
The name is what the eye looks for, but it came last, behind a ragged date column, so nothing lined up. Worse, colorRepoLine rebuilt the line with strings.Fields and single spaces, which destroyed the alignment ls had produced -- the output was aligned only when colour was off. The listing line is now parsed properly instead of being split at the size field: name, date and size come out as fields, the date is re-padded to a fixed twelve columns so "Sep 28 2016" and "Jan 3 14:32" agree, and the name leads in a column sized to the longest entry. Colour decorates that layout without changing it, which a test now checks by stripping the escapes and comparing. `list -a` shows archive sizes, which were parsed and thrown away before. An empty result says so instead of printing nothing, which was indistinguishable from a failure, and the count line matches the rest of mgsh. The pattern now filters on the repository name rather than the whole listing line: matching the owner or the date was never intended and `list 2016` quietly did it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+93
-36
@@ -74,19 +74,65 @@ func TestFormatLogRecentCompact(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorRepoLine(t *testing.T) {
|
||||
func TestFormatRepoList(t *testing.T) {
|
||||
useColor = false
|
||||
in := "Sep 28 2016 Betaflight3.0.0"
|
||||
if got := colorRepoLine(in); got != in {
|
||||
t.Errorf("colorRepoLine with color off changed input: %q", got)
|
||||
entries := []lsEntry{
|
||||
{name: "short", date: "Sep 28 2016", size: 4096},
|
||||
{name: "a-much-longer-name", date: "Jan 3 14:32", size: 1536},
|
||||
}
|
||||
|
||||
useColor = true
|
||||
got := colorRepoLine(in)
|
||||
if !strings.Contains(got, "Betaflight3.0.0") || !strings.Contains(got, cGreen) || !strings.Contains(got, cYellow) {
|
||||
t.Errorf("colorRepoLine did not color parts: %q", got)
|
||||
out := formatRepoList(entries, false)
|
||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("expected 2 lines, got %d: %q", len(lines), out)
|
||||
}
|
||||
// order is preserved: `ls -ltr` already sorted by modification time
|
||||
if !strings.Contains(lines[0], "short") || !strings.Contains(lines[1], "a-much-longer-name") {
|
||||
t.Errorf("order not preserved: %q", out)
|
||||
}
|
||||
// the date starts at the same column on every line
|
||||
if strings.Index(lines[0], "Sep") != strings.Index(lines[1], "Jan") {
|
||||
t.Errorf("date column not aligned:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "4.0K") {
|
||||
t.Errorf("size shown for repositories: %q", out)
|
||||
}
|
||||
|
||||
if withSize := formatRepoList(entries, true); !strings.Contains(withSize, "4.0K") ||
|
||||
!strings.Contains(withSize, "1.5K") {
|
||||
t.Errorf("archive sizes missing: %q", withSize)
|
||||
}
|
||||
|
||||
// colour must decorate the layout, never change it
|
||||
useColor = true
|
||||
colored := formatRepoList(entries, false)
|
||||
useColor = false
|
||||
strip := func(s string) string {
|
||||
for _, c := range []string{cReset, cGreen, cYellow, cGray} {
|
||||
s = strings.ReplaceAll(s, c, "")
|
||||
}
|
||||
return s
|
||||
}
|
||||
if strip(colored) != out {
|
||||
t.Errorf("colour changed the layout:\n%q\n%q", strip(colored), out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanSize(t *testing.T) {
|
||||
cases := []struct {
|
||||
n int64
|
||||
want string
|
||||
}{
|
||||
{0, "0B"}, {512, "512B"}, {1024, "1.0K"}, {1536, "1.5K"},
|
||||
{1024 * 1024, "1.0M"}, {3 * 1024 * 1024 * 1024, "3.0G"},
|
||||
// past 10 the decimal carries nothing, as with `ls -h`
|
||||
{512 * 1024, "512K"}, {99 * 1024 * 1024, "99M"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := humanSize(c.n); got != c.want {
|
||||
t.Errorf("humanSize(%d) = %q, want %q", c.n, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
@@ -147,38 +193,49 @@ gitemail = # value is only a comment
|
||||
}
|
||||
}
|
||||
|
||||
func TestLsEntry(t *testing.T) {
|
||||
cases := []struct{ line, suffix, want string }{
|
||||
// ownership is not assumed: any user/group must list
|
||||
{"drwxr-xr-x 7 git git 4096 Sep 28 2016 myproj.git", ".git", "Sep 28 2016 myproj"},
|
||||
{"drwxr-xr-x 7 deploy deploy 4096 Sep 28 2016 myproj.git", ".git", "Sep 28 2016 myproj"},
|
||||
{"drwxr-xr-x 7 mike staff 4096 Sep 28 2016 myproj.git", ".git", "Sep 28 2016 myproj"},
|
||||
{"drwxr-xr-x. 7 git users 4096 Sep 28 2016 myproj.git", ".git", "Sep 28 2016 myproj"},
|
||||
// archives only match the archive suffix, and vice versa
|
||||
{"-rw-r--r-- 1 git git 512 Sep 28 2016 myproj.git.tar.gz", ".git.tar.gz", "Sep 28 2016 myproj"},
|
||||
{"-rw-r--r-- 1 git git 512 Sep 28 2016 myproj.git.tar.gz", ".git", ""},
|
||||
{"drwxr-xr-x 7 git git 4096 Sep 28 2016 myproj.git", ".git.tar.gz", ""},
|
||||
func TestParseLsEntry(t *testing.T) {
|
||||
cases := []struct {
|
||||
line, suffix string
|
||||
name, date string
|
||||
size int64
|
||||
ok bool
|
||||
}{
|
||||
// ownership is not assumed: any user/group must parse
|
||||
{"drwxr-xr-x 7 git git 4096 Sep 28 2016 myproj.git", ".git", "myproj", "Sep 28 2016", 4096, true},
|
||||
{"drwxr-xr-x 7 deploy deploy 4096 Sep 28 2016 myproj.git", ".git", "myproj", "Sep 28 2016", 4096, true},
|
||||
{"drwxr-xr-x. 7 git users 4096 Sep 28 2016 myproj.git", ".git", "myproj", "Sep 28 2016", 4096, true},
|
||||
// a recent entry carries a time instead of a year, and still lines up
|
||||
{"drwxr-xr-x 7 mike staff 224 Jan 3 14:32 myproj.git", ".git", "myproj", "Jan 3 14:32", 224, true},
|
||||
// a symlinked bare repo lists its target too — only the link name counts
|
||||
{"lrwxrwxrwx 1 git git 14 Sep 28 2016 myproj.git -> /srv/other.git", ".git", "myproj", "Sep 28 2016", 14, true},
|
||||
// archives carry a size worth showing
|
||||
{"-rw-r--r-- 1 git git 524288 Sep 28 2016 myproj.git.tar.gz", ".git.tar.gz", "myproj", "Sep 28 2016", 524288, true},
|
||||
// suffixes must not cross over
|
||||
{"-rw-r--r-- 1 git git 512 Sep 28 2016 myproj.git.tar.gz", ".git", "", "", 0, false},
|
||||
{"drwxr-xr-x 7 git git 4096 Sep 28 2016 myproj.git", ".git.tar.gz", "", "", 0, false},
|
||||
{"lrwxrwxrwx 1 git git 5 Sep 28 2016 notes -> x.git", ".git", "", "", 0, false},
|
||||
// non-entries
|
||||
{"total 48", ".git", ""},
|
||||
{"", ".git", ""},
|
||||
{"drwxr-xr-x 7 git git 4096 Sep 28 2016 notes", ".git", ""},
|
||||
{"total 48", ".git", "", "", 0, false},
|
||||
{"", ".git", "", "", 0, false},
|
||||
{"drwxr-xr-x 7 git git 4096 Sep 28 2016 notes", ".git", "", "", 0, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := lsEntry(c.line, c.suffix); got != c.want {
|
||||
t.Errorf("lsEntry(%q, %q) = %q, want %q", c.line, c.suffix, got, c.want)
|
||||
e, ok := parseLsEntry(c.line, c.suffix)
|
||||
if ok != c.ok {
|
||||
t.Errorf("parseLsEntry(%q, %q) ok = %v, want %v", c.line, c.suffix, ok, c.ok)
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.name != c.name || e.size != c.size {
|
||||
t.Errorf("parseLsEntry(%q) = %+v, want name %q size %d", c.line, e, c.name, c.size)
|
||||
}
|
||||
// every date renders to the same width, whichever form ls used
|
||||
if e.date != c.date || len(e.date) != 12 {
|
||||
t.Errorf("parseLsEntry(%q) date = %q (len %d), want %q at 12",
|
||||
c.line, e.date, len(e.date), c.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLsEntrySymlink(t *testing.T) {
|
||||
// a symlinked bare repo lists its target too — only the link name counts
|
||||
in := "lrwxrwxrwx 1 git git 14 Sep 28 2016 myproj.git -> /srv/other.git"
|
||||
if got := lsEntry(in, ".git"); got != "Sep 28 2016 myproj" {
|
||||
t.Errorf("lsEntry(symlink) = %q, want %q", got, "Sep 28 2016 myproj")
|
||||
}
|
||||
// and a symlink to something that is not a repo must not match
|
||||
if got := lsEntry("lrwxrwxrwx 1 git git 5 Sep 28 2016 notes -> x.git", ".git"); got != "" {
|
||||
t.Errorf("lsEntry(non-repo symlink) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user