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:
@@ -134,14 +134,20 @@ worked-on project sits closest to the prompt:
|
|||||||
|
|
||||||
```
|
```
|
||||||
< src > list
|
< src > list
|
||||||
Betaflight3.0.0 Sep 28 2016
|
Betaflight3.0.0 Sep 28 2016 181M
|
||||||
website Mar 3 2024
|
website Mar 3 2024 2.1M
|
||||||
notes Jan 3 14:32
|
notes Jan 3 14:32 876K
|
||||||
3 repositories
|
3 repositories · 184M
|
||||||
```
|
```
|
||||||
|
|
||||||
`list -a` lists the archives instead, with their sizes; a pattern filters by
|
The size is the repository's real disk usage on the server, asked of `du` in
|
||||||
name (`list note`).
|
the same round trip as the listing — a long listing reports the inode size for
|
||||||
|
a directory, which is the same number for every repository and says nothing. If
|
||||||
|
the server produces no usable sizes the column is left out rather than filled
|
||||||
|
with zeroes.
|
||||||
|
|
||||||
|
`list -a` lists the archives instead, whose sizes come from the listing itself;
|
||||||
|
a pattern filters by name (`list note`).
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
|
|||||||
+64
-4
@@ -24,6 +24,45 @@ var (
|
|||||||
wsRe = regexp.MustCompile(`\s+`)
|
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.
|
// lsEntry is one parsed entry of the server's listing.
|
||||||
type lsEntry struct {
|
type lsEntry struct {
|
||||||
name string // with the ".git" / ".git.tar.gz" suffix removed
|
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"
|
one, many = "archive", "archives"
|
||||||
}
|
}
|
||||||
pat := strings.ToLower(word(words, 1))
|
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 {
|
if err != nil {
|
||||||
errorln("could not list " + many + " on the git server")
|
errorln("could not list " + many + " on the git server")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
lsLines, duLines := splitAtMarker(lines, listMarker)
|
||||||
|
sizes := parseDuSizes(duLines)
|
||||||
|
|
||||||
var entries []lsEntry
|
var entries []lsEntry
|
||||||
for _, ln := range lines {
|
var total int64
|
||||||
|
for _, ln := range lsLines {
|
||||||
e, ok := parseLsEntry(ln, suffix)
|
e, ok := parseLsEntry(ln, suffix)
|
||||||
// the pattern filters the name, not the whole listing line — an
|
// the pattern filters the name, not the whole listing line — an
|
||||||
// accidental match on the date or the owner helps nobody
|
// accidental match on the date or the owner helps nobody
|
||||||
if !ok || (pat != "" && !strings.Contains(strings.ToLower(e.name), pat)) {
|
if !ok || (pat != "" && !strings.Contains(strings.ToLower(e.name), pat)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !opt["a"] {
|
||||||
|
e.size = sizes[e.name+suffix] // 0 when du said nothing
|
||||||
|
}
|
||||||
|
total += e.size
|
||||||
entries = append(entries, e)
|
entries = append(entries, e)
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
@@ -226,12 +280,18 @@ func runCommandDepth(line string, depth int) bool {
|
|||||||
fmt.Println(col(cGray, what))
|
fmt.Println(col(cGray, what))
|
||||||
break
|
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
|
label := many
|
||||||
if len(entries) == 1 {
|
if len(entries) == 1 {
|
||||||
label = one
|
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
|
case "show": // show a repository's log directly on the server
|
||||||
prj := PRJ
|
prj := PRJ
|
||||||
|
|||||||
@@ -943,3 +943,45 @@ func TestExpandAlias(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSplitAtMarker(t *testing.T) {
|
||||||
|
lines := []string{"a", "b", "---mgsh---", "c", "d"}
|
||||||
|
before, after := splitAtMarker(lines, "---mgsh---")
|
||||||
|
if strings.Join(before, ",") != "a,b" || strings.Join(after, ",") != "c,d" {
|
||||||
|
t.Errorf("split = %v / %v", before, after)
|
||||||
|
}
|
||||||
|
// no marker: everything is the first section, so a server that produced no
|
||||||
|
// du output simply yields no sizes
|
||||||
|
before, after = splitAtMarker([]string{"a", "b"}, "---mgsh---")
|
||||||
|
if strings.Join(before, ",") != "a,b" || after != nil {
|
||||||
|
t.Errorf("split without marker = %v / %v", before, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDuSizes(t *testing.T) {
|
||||||
|
lines := []string{
|
||||||
|
"185432\tBetaflight3.0.0.git",
|
||||||
|
"2144\twebsite.git",
|
||||||
|
"876 spaced-with-blanks.git", // some du implementations use spaces
|
||||||
|
"1024\t./with-dot-slash.git",
|
||||||
|
"1500\tmy project.git", // a name with a space survives
|
||||||
|
"garbage",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
got := parseDuSizes(lines)
|
||||||
|
want := map[string]int64{
|
||||||
|
"Betaflight3.0.0.git": 185432 * 1024,
|
||||||
|
"website.git": 2144 * 1024,
|
||||||
|
"spaced-with-blanks.git": 876 * 1024,
|
||||||
|
"with-dot-slash.git": 1024 * 1024,
|
||||||
|
"my project.git": 1500 * 1024,
|
||||||
|
}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("parseDuSizes = %v, want %d entries", got, len(want))
|
||||||
|
}
|
||||||
|
for k, v := range want {
|
||||||
|
if got[k] != v {
|
||||||
|
t.Errorf("parseDuSizes[%q] = %d, want %d", k, got[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
4.0.29
|
4.0.31
|
||||||
|
|||||||
Reference in New Issue
Block a user