package main import ( "os" "os/exec" "path/filepath" "strings" ) // runInDir runs a command with inherited stdio, optionally in dir. func runInDir(dir, name string, args ...string) error { c := exec.Command(name, args...) if dir != "" { c.Dir = dir } c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr return c.Run() } // runQuiet runs a command in dir with its output discarded, for probes whose // exit status is the only interesting part. func runQuiet(dir, name string, args ...string) error { c := exec.Command(name, args...) c.Dir = dir return c.Run() } // sshKeyPath returns the identity file to authenticate with at the git server, // or "" when no 'gitkey' is configured. A bare name is looked up in ~/.ssh, a // path (absolute or ~/-relative) is used as given. func sshKeyPath() string { k := strings.TrimSpace(cfg.GitKey) if k == "" { return "" } home, _ := os.UserHomeDir() switch { case strings.HasPrefix(k, "~/"): return filepath.Join(home, k[2:]) case strings.ContainsRune(k, '/'): return k default: return filepath.Join(home, ".ssh", k) } } // sshArgs prefixes the ssh options shared by every connection to the git // server: the configured port and, when set, the identity file. func sshArgs(rest ...string) []string { args := []string{"-p", cfg.GitPort} if k := sshKeyPath(); k != "" { args = append(args, "-i", k) } return append(args, rest...) } // gitEnv is the environment for a git subprocess: nil (inherit) unless a // 'gitkey' is configured, in which case git's ssh transport is pointed at it. // Set per command instead of in mgsh's own environment, so a `!git ...` shell // escape keeps whatever the user's shell would normally do. func gitEnv() []string { k := sshKeyPath() if k == "" { return nil } return append(os.Environ(), "GIT_SSH_COMMAND=ssh -i "+shq(k)) } // git runs a git command with inherited stdio, optionally in dir. func git(dir string, args ...string) error { c := exec.Command("git", args...) if dir != "" { c.Dir = dir } c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr c.Env = gitEnv() return c.Run() } // gitOK runs git and, on failure, prints a red summary. Returns success. func gitOK(dir string, args ...string) bool { if err := git(dir, args...); err != nil { errorln("git " + strings.Join(args, " ") + " failed") return false } return true } func gitCapture(dir string, args ...string) (string, error) { c := exec.Command("git", args...) if dir != "" { c.Dir = dir } c.Env = gitEnv() out, err := c.Output() return string(out), err } // shq quotes s for interpolation into a remote /bin/sh command line. Every // remote command is a single string handed to the login shell, so any project // name or configured path reaching it must go through here — otherwise a space // splits one argument into two and a backtick runs on the server. func shq(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } // ssh runs a single remote command over ssh with inherited stdio. func ssh(remote string) error { return runInDir("", "ssh", sshArgs(cfg.GitUser+"@"+cfg.GitHost, remote)...) } // sshOK runs a remote command and, on failure, prints a red summary. func sshOK(remote string) bool { if err := ssh(remote); err != nil { errorln("ssh failed: " + remote) return false } return true } // sshOut runs a remote command and returns its stdout split into lines. func sshOut(remote string) ([]string, error) { c := exec.Command("ssh", sshArgs(cfg.GitUser+"@"+cfg.GitHost, remote)...) c.Stderr = os.Stderr out, err := c.Output() lines := strings.Split(string(out), "\n") for len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } return lines, err } // 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". func serverEntryExists(path, entry string) (bool, error) { lines, err := sshOut("/bin/ls " + shq(path)) if err != nil { return false, err } for _, ln := range lines { if strings.TrimSpace(ln) == entry { return true, nil } } return false, nil } // forwardShell runs a line via /bin/sh -c in the active project directory. func forwardShell(line string) { runInDir(DIR, "/bin/sh", "-c", line) }