initial commit [141.14.129.234,mike]
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
// mgsh (git shell) — Go port of the original Perl mgsh (mwx'2026).
|
||||
//
|
||||
// A small interactive shell / command-line wrapper around a self-hosted bare
|
||||
// git server, reachable over ssh. It manages a flat set of projects living
|
||||
// under a base directory (default $HOME/src, or /db/src on Linux).
|
||||
//
|
||||
// Configuration lives in ~/.mgshrc and/or MGSH_* environment variables; see
|
||||
// config.go and the README.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
)
|
||||
|
||||
// VERSION is a var (not const) so the build script can inject the current build
|
||||
// number via -ldflags "-X main.VERSION=...". The default is used for a plain
|
||||
// `go build`.
|
||||
var VERSION = "4.0.0"
|
||||
|
||||
const INFO = "mwx'2026"
|
||||
|
||||
// Global runtime state.
|
||||
var (
|
||||
cfg Config // resolved configuration
|
||||
BASE string // project base directory (cfg.Base)
|
||||
URL string // ssh:// clone/push URL
|
||||
PRJ string // current project (may be empty)
|
||||
DIR string // current working directory (BASE or BASE/PRJ)
|
||||
IP string // local IP, embedded into the initial commit message
|
||||
HOST string // short hostname
|
||||
USER string // current user name
|
||||
REPO string // repository name parsed from remote.origin.url
|
||||
BPLSTATE string // "/OFF" if build.pl symlink points at a *.off file
|
||||
BRANCH string // current git branch of the active project (if any)
|
||||
DIRTY bool // whether the active project has uncommitted changes
|
||||
useColor bool // whether to render ANSI colors
|
||||
)
|
||||
|
||||
func main() {
|
||||
setup()
|
||||
useColor = readline.IsTerminal(int(os.Stdout.Fd()))
|
||||
|
||||
fmt.Println(banner())
|
||||
|
||||
// preset project from the first argument (interactive launch: `mgsh myproj`)
|
||||
if len(os.Args) > 1 && isDir(BASE+"/"+os.Args[1]) {
|
||||
PRJ = os.Args[1]
|
||||
}
|
||||
|
||||
// command-line mode: `mgsh <command> [args...]`
|
||||
clCmd, cmdline, isHelp := parseArgs()
|
||||
if isHelp {
|
||||
help()
|
||||
return
|
||||
}
|
||||
if clCmd != 0 {
|
||||
pwd, _ := os.Getwd()
|
||||
PRJ = ""
|
||||
if strings.HasPrefix(pwd, BASE+"/") {
|
||||
PRJ = pwd[len(BASE)+1:]
|
||||
}
|
||||
if clCmd == 2 {
|
||||
cmdline = strings.TrimSpace(cmdline + " " + PRJ)
|
||||
}
|
||||
updateDirState()
|
||||
runCommand(cmdline)
|
||||
return
|
||||
}
|
||||
|
||||
runInteractive()
|
||||
}
|
||||
|
||||
// runInteractive drives the colored, history- and completion-enabled REPL.
|
||||
func runInteractive() {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
rl, err := readline.NewEx(&readline.Config{
|
||||
Prompt: promptString(),
|
||||
HistoryFile: filepath.Join(home, ".mgsh_history"),
|
||||
AutoComplete: completer(),
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "quit",
|
||||
HistorySearchFold: true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer rl.Close()
|
||||
|
||||
for {
|
||||
updateDirState()
|
||||
rl.SetPrompt(promptString())
|
||||
line, err := rl.Readline()
|
||||
if err == readline.ErrInterrupt { // Ctrl-C: clear a typed line, else quit
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
continue
|
||||
} else if err == io.EOF { // Ctrl-D
|
||||
break
|
||||
}
|
||||
if !runCommand(strings.TrimSpace(line)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseArgs mirrors the original command-line dispatch table. It returns the
|
||||
// "CLCMD" class (0 = interactive, 1 = plain, 2 = append project name), the raw
|
||||
// command line, and whether `help` was requested.
|
||||
func parseArgs() (int, string, bool) {
|
||||
if len(os.Args) < 2 {
|
||||
return 0, "", false
|
||||
}
|
||||
a0 := os.Args[1]
|
||||
if a0 == "help" {
|
||||
return 0, "", true
|
||||
}
|
||||
cls := map[string]int{
|
||||
"clone": 2, "init": 2, "log": 2,
|
||||
"push": 1, "pushremote": 1, "list": 1, "tag": 1, "archive": 1, "show": 1, "open": 1,
|
||||
"pull": 1, "fetch": 1, "status": 1, "diff": 1, "overview": 1,
|
||||
}
|
||||
if c, ok := cls[a0]; ok {
|
||||
return c, strings.Join(os.Args[1:], " "), false
|
||||
}
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
func setup() {
|
||||
cfg = loadConfig()
|
||||
if miss := cfg.missingRequired(); len(miss) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "mgsh: not configured — missing required settings: %s\n", strings.Join(miss, ", "))
|
||||
fmt.Fprintf(os.Stderr, "edit %s or set the corresponding MGSH_* environment variables, then run mgsh again.\n", configFile())
|
||||
os.Exit(1)
|
||||
}
|
||||
BASE = cfg.Base
|
||||
if !isDir(BASE) {
|
||||
fmt.Fprintf(os.Stderr, "%s not found\n", BASE)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
URL = fmt.Sprintf("ssh://%s@%s:%s%s", cfg.GitUser, cfg.GitHost, cfg.GitPort, cfg.GitPath)
|
||||
IP = resolveIP()
|
||||
HOST = shortHostname()
|
||||
if u, err := user.Current(); err == nil {
|
||||
USER = u.Username
|
||||
}
|
||||
|
||||
// git identity / settings, from config (defaults preserve prior behavior)
|
||||
if cfg.GitName != "" {
|
||||
git("", "config", "--global", "user.name", cfg.GitName)
|
||||
}
|
||||
if cfg.GitEmail != "" {
|
||||
git("", "config", "--global", "user.email", cfg.GitEmail)
|
||||
}
|
||||
if cfg.PushDefault != "" {
|
||||
git("", "config", "--global", "push.default", cfg.PushDefault)
|
||||
}
|
||||
|
||||
loadAliases()
|
||||
}
|
||||
|
||||
// updateDirState recomputes DIR, the build.pl OFF marker, the current branch /
|
||||
// dirty flag and the parsed repository name for the active project. Run once
|
||||
// per loop iteration.
|
||||
func updateDirState() {
|
||||
BRANCH = ""
|
||||
DIRTY = false
|
||||
|
||||
if PRJ != "" && isDir(BASE+"/"+PRJ) {
|
||||
DIR = BASE + "/" + PRJ
|
||||
BPLSTATE = ""
|
||||
bp := DIR + "/build.pl"
|
||||
if fi, err := os.Lstat(bp); err == nil && fi.Mode()&os.ModeSymlink != 0 {
|
||||
if target, err := os.Readlink(bp); err == nil && strings.HasSuffix(target, ".off") {
|
||||
BPLSTATE = "/OFF"
|
||||
}
|
||||
}
|
||||
if isDir(DIR + "/.git") {
|
||||
if out, err := gitCapture(DIR, "rev-parse", "--abbrev-ref", "HEAD"); err == nil {
|
||||
BRANCH = strings.TrimSpace(out)
|
||||
}
|
||||
if out, err := gitCapture(DIR, "status", "--porcelain"); err == nil && strings.TrimSpace(out) != "" {
|
||||
DIRTY = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DIR = BASE
|
||||
BPLSTATE = ""
|
||||
}
|
||||
|
||||
// derive REPO from the origin URL: basename without the ".git" suffix.
|
||||
REPO = ""
|
||||
if out, err := gitCapture(DIR, "config", "remote.origin.url"); err == nil {
|
||||
remurl := strings.TrimRight(strings.TrimSpace(out), "/")
|
||||
if remurl != "" {
|
||||
REPO = strings.TrimSuffix(filepath.Base(remurl), ".git")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user