Keep git's own words in a fetch's reason: a bare "error:" line no longer stands for the forge's refusal
mesh/delivery merged: source.main moved to novox/mesh-sdk@1888d1d230b3
mesh/delivery-group group fix/fetch-reads-all merged: every member is merged
mesh/check pass ff81bdf8cc8f on 960d6c34d25e | unit 5815 run, 345 kept, 18 skipped | integration 5603 run, 640 kept, 29 skipped | over bound: unit, in…
mesh/delivery merged: source.main moved to novox/mesh-sdk@1888d1d230b3
mesh/delivery-group group fix/fetch-reads-all merged: every member is merged
mesh/check pass ff81bdf8cc8f on 960d6c34d25e | unit 5815 run, 345 kept, 18 skipped | integration 5603 run, 640 kept, 29 skipped | over bound: unit, in…
The mirror of jschoubben/car-hunter on ace said only "git -C: error:": git writes a bare "error:" before the forge's message, and the reason kept the first non-empty line. forgekey.Said keeps every line that says something, leaves bare prefixes out, joins them and bounds them at 600 bytes; every git and ssh-keygen reason of buildstep and forgekey goes through it, and the mirror's commands are named by their verb, not by -C.
This commit is contained in:
@@ -101,7 +101,7 @@ func (k *KeyKeeper) ensure(ctx context.Context) error {
|
||||
}
|
||||
r := k.Run(ctx, nil, "ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", k.Account+"@"+k.Node+" (mesh)", "-f", k.path())
|
||||
if r.Status != 0 || r.Err != "" {
|
||||
return fmt.Errorf("ssh-keygen: %s", firstLine(r.Err+" "+r.Stderr))
|
||||
return fmt.Errorf("ssh-keygen: %s", Said(r.Err+"\n"+r.Stderr))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+31
-4
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Ran is what a command did: its output, its exit status, and why it never ran to an answer.
|
||||
@@ -108,11 +109,37 @@ func stableDir(clean bool, env []string) string {
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
// SaidBound bounds what Said keeps of a command's words, so a reason stays a line of a step's record and of a log.
|
||||
const SaidBound = 600
|
||||
|
||||
// Said is what a command said of its failure in its own words, for a reason: every line that says something, trimmed
|
||||
// and joined with "; ", bounded at SaidBound bytes. A line that is a bare prefix and nothing else (git writes "error:"
|
||||
// alone before and after the forge's own message, "remote:" or "fatal:" likewise) is left out, so the cause is never
|
||||
// cut off behind it: the first line alone of `git fetch` from a repository the account cannot read was "error:".
|
||||
func Said(s string) string {
|
||||
var keep []string
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" {
|
||||
return l
|
||||
l = strings.TrimSpace(l)
|
||||
if l == "" || barePrefix(l) {
|
||||
continue
|
||||
}
|
||||
keep = append(keep, l)
|
||||
}
|
||||
return ""
|
||||
out := strings.Join(keep, "; ")
|
||||
if len(out) > SaidBound {
|
||||
cut := SaidBound
|
||||
for cut > 0 && !utf8.RuneStart(out[cut]) {
|
||||
cut--
|
||||
}
|
||||
out = out[:cut] + " …"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// barePrefix says whether a line is one word and a colon, such as "error:" or "remote:", with nothing after it.
|
||||
func barePrefix(l string) bool {
|
||||
if !strings.HasSuffix(l, ":") {
|
||||
return false
|
||||
}
|
||||
return !strings.ContainsAny(strings.TrimSuffix(l, ":"), " \t:")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package forgekey
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// git's own words survive as the reason: a bare "error:" or "fatal:" line is never the whole reason (the mirror of
|
||||
// jschoubben/car-hunter "is not used: git -C: error:", 2026-10-26), every line that says something is kept, and the
|
||||
// words are bounded.
|
||||
func TestSaidKeepsTheCauseBehindABarePrefix(t *testing.T) {
|
||||
forge := "error: \nerror: User: 22:nox-builder with Key: 30:mesh:ace.mesh-build-go is not authorized to read jschoubben/car-hunter.\nerror: \nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n"
|
||||
for _, c := range []struct{ name, in, want string }{
|
||||
{"forge refusal", forge, "error: User: 22:nox-builder with Key: 30:mesh:ace.mesh-build-go is not authorized to read jschoubben/car-hunter.; fatal: Could not read from remote repository.; Please make sure you have the correct access rights; and the repository exists."},
|
||||
{"one line", "fatal: couldn't find remote ref x\n", "fatal: couldn't find remote ref x"},
|
||||
{"bare prefixes only", "error:\nremote:\n", ""},
|
||||
{"a colon inside stays", " ENOENT \nfatal: bad: thing\n", "ENOENT; fatal: bad: thing"},
|
||||
{"empty", "", ""},
|
||||
} {
|
||||
if got := Said(c.in); got != c.want {
|
||||
t.Errorf("%s: Said = %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
long := Said(strings.Repeat("é", SaidBound))
|
||||
if !strings.HasSuffix(long, " …") || len(long) > SaidBound+len(" …") || !strings.HasPrefix(long, "é") {
|
||||
t.Errorf("long: %d bytes, ends %q", len(long), long[len(long)-6:])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user