The remote commands are the only code in mgsh that can destroy data, and they were the least verifiable: each one is a single string handed to a login shell, so a missing quote silently changes which paths it touches. Every one of them now goes through the sshExec variable, and yesno is a variable too, so a test can record what would have been sent and answer the confirmations without a terminal. The tests pin down what the previous commit fixed by reasoning alone: that a declined or unreachable `init` sends no rm -rf, that a project named "my 'weird' project" reaches the server fully quoted, and that archive's cp/tar/rm sequence is named, ordered and quoted correctly. push is driven end to end against a real local bare repository. archive also gained the server-side existence check that init, clone and show already had, so a project that was never pushed reports that instead of failing inside cp -r. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
1.8 KiB
Go
76 lines
1.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/chzyer/readline"
|
|
)
|
|
|
|
// yesno asks a y/n question with a default and reads a single keypress. It is a
|
|
// variable because it guards the destructive operations: tests replace it to
|
|
// drive those paths without a terminal, and to assert that the question was
|
|
// asked at all.
|
|
var yesno = func(prompt string, def bool) bool {
|
|
suffix := " y/N ? "
|
|
if def {
|
|
suffix = " Y/n ? "
|
|
}
|
|
ans := strings.ToLower(strings.TrimSpace(getkey(prompt + suffix)))
|
|
if ans == "" {
|
|
return def
|
|
}
|
|
return ans == "y"
|
|
}
|
|
|
|
// getkey reads a single keypress from the terminal without echo. It reads a
|
|
// single byte directly from stdin; readline is not reading at this point (we
|
|
// are inside command execution), so there is no reader to desync with.
|
|
//
|
|
// Anything else already typed on the same line is discarded: answering "yes"
|
|
// to a y/n prompt must not leave "es\n" queued for the next readline call,
|
|
// where it would come back as a bogus command.
|
|
func getkey(prompt string) string {
|
|
fmt.Print(prompt)
|
|
tty := readline.IsTerminal(int(os.Stdin.Fd()))
|
|
if tty {
|
|
stty("-icanon", "-echo")
|
|
}
|
|
var buf [1]byte
|
|
n, err := os.Stdin.Read(buf[:])
|
|
if tty {
|
|
drainTTY()
|
|
stty("icanon", "echo")
|
|
}
|
|
key := ""
|
|
if err == nil && n > 0 {
|
|
key = strings.Trim(string(buf[:n]), "\r\n\t")
|
|
}
|
|
fmt.Println(key)
|
|
return key
|
|
}
|
|
|
|
// drainTTY discards input already queued on the terminal. `min 0 time 0` makes
|
|
// a read return whatever is buffered without waiting, so this cannot block when
|
|
// nothing is pending.
|
|
func drainTTY() {
|
|
stty("-icanon", "-echo", "min", "0", "time", "0")
|
|
buf := make([]byte, 256)
|
|
for {
|
|
n, err := os.Stdin.Read(buf)
|
|
if err != nil || n == 0 {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func stty(args ...string) {
|
|
c := exec.Command("stty", args...)
|
|
c.Stdin = os.Stdin
|
|
c.Stdout = os.Stdout
|
|
c.Stderr = os.Stderr
|
|
c.Run()
|
|
}
|