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:
+51
-17
@@ -14,32 +14,46 @@ import (
|
||||
var (
|
||||
optRe = regexp.MustCompile(`^-(\w)$`)
|
||||
numRe = regexp.MustCompile(`^\d+$`)
|
||||
// a `ls -ltr` long-listing line: mode, link count, owner, group, size, then
|
||||
// the date columns and the name. Owner and group are matched as opaque
|
||||
// a `ls -ltr` long-listing line: mode, link count, owner, group, size, the
|
||||
// three date columns, then the name. Owner and group are matched as opaque
|
||||
// fields — the bare repositories need not belong to a user or group
|
||||
// literally named "git".
|
||||
lsEntryRe = regexp.MustCompile(`^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+(.*)$`)
|
||||
lsEntryRe = regexp.MustCompile(`^\S+\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$`)
|
||||
gitDirRe = regexp.MustCompile(`^(.*)\.git$`)
|
||||
sanRe = regexp.MustCompile(`[,;:\\/='"|?><-]+`)
|
||||
wsRe = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// lsEntry extracts the "<date columns> <name>" tail of a `ls -ltr` line whose
|
||||
// entry name ends in suffix, with the suffix removed. It returns "" for any
|
||||
// other line (the leading "total" line, entries of a different kind).
|
||||
func lsEntry(line, suffix string) string {
|
||||
// lsEntry is one parsed entry of the server's listing.
|
||||
type lsEntry struct {
|
||||
name string // with the ".git" / ".git.tar.gz" suffix removed
|
||||
date string // the ls date columns, normalised to a fixed 12 columns
|
||||
size int64
|
||||
}
|
||||
|
||||
// parseLsEntry reads one `ls -ltr` line whose entry name ends in suffix. It
|
||||
// returns false for anything else: the leading "total" line, entries of another
|
||||
// kind, or output that does not look like a long listing at all.
|
||||
func parseLsEntry(line, suffix string) (lsEntry, bool) {
|
||||
m := lsEntryRe.FindStringSubmatch(strings.TrimSpace(line))
|
||||
if m == nil {
|
||||
return ""
|
||||
return lsEntry{}, false
|
||||
}
|
||||
name := m[1]
|
||||
name := m[5]
|
||||
if i := strings.Index(name, " -> "); i >= 0 {
|
||||
name = name[:i] // a symlinked bare repo lists as "link.git -> target.git"
|
||||
}
|
||||
if !strings.HasSuffix(name, suffix) {
|
||||
return ""
|
||||
return lsEntry{}, false
|
||||
}
|
||||
return strings.TrimSuffix(name, suffix)
|
||||
size, _ := strconv.ParseInt(m[1], 10, 64)
|
||||
return lsEntry{
|
||||
name: strings.TrimSuffix(name, suffix),
|
||||
// ls pads these itself, but only in its own column widths; re-pad so
|
||||
// "Sep 28 2016" and "Jan 3 14:32" line up at 12 either way
|
||||
date: fmt.Sprintf("%s %2s %5s", m[2], m[3], m[4]),
|
||||
size: size,
|
||||
}, true
|
||||
}
|
||||
|
||||
// validProject reports whether name is usable as a project name: a single path
|
||||
@@ -184,20 +198,40 @@ func runCommandDepth(line string, depth int) bool {
|
||||
if opt["a"] {
|
||||
path, suffix = "./archive", ".git.tar.gz"
|
||||
}
|
||||
pat := word(words, 1)
|
||||
one, many := "repository", "repositories"
|
||||
if opt["a"] {
|
||||
one, many = "archive", "archives"
|
||||
}
|
||||
pat := strings.ToLower(word(words, 1))
|
||||
lines, err := sshOut("/bin/ls -ltr " + shq(path))
|
||||
if err != nil {
|
||||
errorln("could not list repositories on the git server")
|
||||
errorln("could not list " + many + " on the git server")
|
||||
break
|
||||
}
|
||||
var entries []lsEntry
|
||||
for _, ln := range lines {
|
||||
if pat != "" && !strings.Contains(strings.ToLower(ln), strings.ToLower(pat)) {
|
||||
e, ok := parseLsEntry(ln, suffix)
|
||||
// the pattern filters the name, not the whole listing line — an
|
||||
// accidental match on the date or the owner helps nobody
|
||||
if !ok || (pat != "" && !strings.Contains(strings.ToLower(e.name), pat)) {
|
||||
continue
|
||||
}
|
||||
if name := lsEntry(ln, suffix); name != "" {
|
||||
fmt.Println(colorRepoLine(name))
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
what := "no " + many + " on the git server"
|
||||
if pat != "" {
|
||||
what = "no " + many + " matching '" + word(words, 1) + "'"
|
||||
}
|
||||
fmt.Println(col(cGray, what))
|
||||
break
|
||||
}
|
||||
fmt.Print(formatRepoList(entries, opt["a"]))
|
||||
label := many
|
||||
if len(entries) == 1 {
|
||||
label = one
|
||||
}
|
||||
fmt.Println(col(cGray, fmt.Sprintf("%d %s", len(entries), label)))
|
||||
|
||||
case "show": // show a repository's log directly on the server
|
||||
prj := PRJ
|
||||
|
||||
Reference in New Issue
Block a user