[mike@mwxm4]

This commit is contained in:
2026-08-11 16:14:55 +02:00
parent 34be410a97
commit 55f42ec50d
5 changed files with 201 additions and 32 deletions
+48 -4
View File
@@ -1,6 +1,7 @@
package main
import (
"errors"
"os"
"os/exec"
"path/filepath"
@@ -127,8 +128,30 @@ var sshExec = func(remote string, capture bool) ([]byte, error) {
return out, err
}
// errRemoteBang is what a command carrying a '!' comes back with, so a caller
// sees a failure rather than a command that quietly did something else.
var errRemoteBang = errors.New("remote command contains '!'")
// remoteRejected reports whether a command must not be sent at all, and says
// why. '!' is the one character shq cannot protect: csh expands history *before*
// it looks at quotes, and it does so non-interactively too — `echo 'fix!now'`
// answers "Event not found" on the tcsh server. No spelling survives both csh
// and sh, so the only safe move is not to send one. This is the place every
// remote command passes.
func remoteRejected(remote string) bool {
if !strings.ContainsRune(remote, '!') {
return false
}
errorln("not sending a command with '!' in it — the server's login shell " +
"would expand it instead of passing it on: " + remote)
return true
}
// ssh runs a single remote command over ssh with inherited stdio.
func ssh(remote string) error {
if remoteRejected(remote) {
return errRemoteBang
}
_, err := sshExec(remote, false)
return err
}
@@ -144,6 +167,9 @@ func sshOK(remote string) bool {
// sshOut runs a remote command and returns its stdout split into lines.
func sshOut(remote string) ([]string, error) {
if remoteRejected(remote) {
return nil, errRemoteBang
}
out, err := sshExec(remote, true)
lines := strings.Split(string(out), "\n")
for len(lines) > 0 && lines[len(lines)-1] == "" {
@@ -152,10 +178,27 @@ func sshOut(remote string) ([]string, error) {
return lines, err
}
// serverPath anchors a path on the git server at the configured gitpath.
//
// Every remote command has to name its target outright, because the login
// directory of the git user is not necessarily the directory holding the bare
// repositories. With `gituser = git` and `gitpath = /home/git` the two are the
// same place and a bare "." worked by luck; with `gituser = root` and
// `gitpath = /root/mgsh` it lists the home directory, where there is nothing to
// find.
func serverPath(rel string) string {
base := strings.TrimRight(cfg.GitPath, "/")
rel = strings.TrimPrefix(strings.TrimSpace(rel), "./")
if rel == "" || rel == "." {
return base
}
return base + "/" + rel
}
// serverRepoNames lists the bare repositories on the git server, without the
// ".git" suffix.
func serverRepoNames() ([]string, error) {
lines, err := sshOut("/bin/ls .")
lines, err := sshOut("/bin/ls " + shq(serverPath(".")))
if err != nil {
return nil, err
}
@@ -169,10 +212,11 @@ func serverRepoNames() ([]string, error) {
}
// serverEntryExists reports whether entry is present in the remote directory
// path (relative to the git user's home). The error is returned rather than
// folded into the bool so a failed lookup is never mistaken for "not there".
// path (relative to gitpath, which serverPath resolves). The error is returned
// rather than folded into the bool so a failed lookup is never mistaken for
// "not there".
func serverEntryExists(path, entry string) (bool, error) {
lines, err := sshOut("/bin/ls " + shq(path))
lines, err := sshOut("/bin/ls " + shq(serverPath(path)))
if err != nil {
return false, err
}