Show repository sizes in list

`list -a` had sizes because archives are files; repositories are
directories, and a long listing reports the inode size for those -- 4096
for every single one. Taking that number would have filled the column
with the same meaningless value, so the real disk usage is asked of `du`
instead, appended to the same remote command so it still costs one round
trip.

The column is dropped entirely when no usable sizes come back, rather
than showing a column of zeroes, so a server without a working `du`
degrades to the previous output. The summary line carries the total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 17:30:12 +02:00
co-authored by Claude Opus 5
parent 8c68b28dc2
commit 3a420093d1
4 changed files with 119 additions and 11 deletions
+64 -4
View File
@@ -24,6 +24,45 @@ var (
wsRe = regexp.MustCompile(`\s+`)
)
// listMarker separates the two sections of the combined listing command, so
// `list` gets both the long listing and the disk usage in one round trip.
const listMarker = "---mgsh---"
// duRe matches one `du -sk` line: kilobytes, then the path.
var duRe = regexp.MustCompile(`^(\d+)\s+(.*)$`)
// splitAtMarker divides the remote output into the part before and after the
// marker line. Everything is in the first section when the marker is absent —
// which is what happens when only a plain listing was asked for.
func splitAtMarker(lines []string, marker string) (before, after []string) {
for i, ln := range lines {
if strings.TrimSpace(ln) == marker {
return lines[:i], lines[i+1:]
}
}
return lines, nil
}
// parseDuSizes turns `du -sk` output into a name -> bytes map. A long listing
// reports the inode size for a directory — the same number for every bare
// repository — so this is the only way to say how large one actually is.
// A symlinked repository reports the size of the link, not of its target.
func parseDuSizes(lines []string) map[string]int64 {
out := map[string]int64{}
for _, ln := range lines {
m := duRe.FindStringSubmatch(strings.TrimRight(ln, "\r"))
if m == nil {
continue
}
kb, err := strconv.ParseInt(m[1], 10, 64)
if err != nil {
continue
}
out[strings.TrimPrefix(strings.TrimSpace(m[2]), "./")] = kb * 1024
}
return out
}
// lsEntry is one parsed entry of the server's listing.
type lsEntry struct {
name string // with the ".git" / ".git.tar.gz" suffix removed
@@ -203,19 +242,34 @@ func runCommandDepth(line string, depth int) bool {
one, many = "archive", "archives"
}
pat := strings.ToLower(word(words, 1))
lines, err := sshOut("/bin/ls -ltr " + shq(path))
remote := "/bin/ls -ltr " + shq(path)
if !opt["a"] {
// archives are files and carry a real size; repositories are
// directories, whose listed size is the inode's, so ask du in the
// same round trip
remote += "; echo " + shq(listMarker) + "; du -sk *.git 2>/dev/null"
}
lines, err := sshOut(remote)
if err != nil {
errorln("could not list " + many + " on the git server")
break
}
lsLines, duLines := splitAtMarker(lines, listMarker)
sizes := parseDuSizes(duLines)
var entries []lsEntry
for _, ln := range lines {
var total int64
for _, ln := range lsLines {
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 !opt["a"] {
e.size = sizes[e.name+suffix] // 0 when du said nothing
}
total += e.size
entries = append(entries, e)
}
if len(entries) == 0 {
@@ -226,12 +280,18 @@ func runCommandDepth(line string, depth int) bool {
fmt.Println(col(cGray, what))
break
}
fmt.Print(formatRepoList(entries, opt["a"]))
// no size column when the server gave no usable sizes, rather than a
// column of zeroes
fmt.Print(formatRepoList(entries, total > 0))
label := many
if len(entries) == 1 {
label = one
}
fmt.Println(col(cGray, fmt.Sprintf("%d %s", len(entries), label)))
summary := fmt.Sprintf("%d %s", len(entries), label)
if total > 0 {
summary += " · " + humanSize(total)
}
fmt.Println(col(cGray, summary))
case "show": // show a repository's log directly on the server
prj := PRJ