package main import ( "bufio" "errors" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "strings" ) const codeDir = "/nezuko/code/" func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } func checkRepoName(name string) (string, error) { if strings.Contains(name, "/") { return "", fmt.Errorf("invalid name %q: contains slashes", name) } if !strings.HasSuffix(name, ".git") { name += ".git" } return name, nil } func create() error { flag.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: create REPO Create a new git repo named REPO. `) } d := flag.String("d", "", "description") flag.Parse() args := flag.Args() if len(args) == 0 { return fmt.Errorf("missing operand\nUse create -help for usage info") } if len(args) > 1 { return fmt.Errorf("too many positional arguments\nUse create -help for usage info") } name, err := checkRepoName(args[0]) if err != nil { return err } dir := filepath.Join(codeDir, name) fmt.Fprintf(os.Stderr, "mkdir %s\n", shellQuote(dir)) if err := os.Mkdir(dir, 0755); err != nil { return err } fmt.Fprintf(os.Stderr, "cd %s && git init --bare\n", shellQuote(dir)) git := exec.Command("git", "init", "--bare") git.Dir = dir git.Stdout = os.Stdout git.Stderr = os.Stderr if err := git.Run(); err != nil { return err } infoWeb := filepath.Join(dir, "info", "web") fmt.Fprintf(os.Stderr, "mkdir %s\n", shellQuote(infoWeb)) if err := os.Mkdir(infoWeb, 0755); err != nil { return err } postReceiveHook := filepath.Join(dir, "hooks", "post-receive") const postReceiveScript = "TZ=UTC0 git log -1 --reflog --date='format-local:%Y-%m-%d %H:%M:%S' --pretty=%cd > $GIT_DIR/info/web/last-modified" fmt.Fprintf(os.Stderr, "echo %s > %s && chmod +x %[2]s\n", shellQuote(postReceiveScript), shellQuote(postReceiveHook)) postReceive, err := os.OpenFile(postReceiveHook, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err } defer postReceive.Close() if _, err := io.WriteString(postReceive, postReceiveScript+"\n"); err != nil { return err } if err := postReceive.Close(); err != nil { return err } if *d != "" { if err := setRepoDescription(name, *d); err != nil { return err } } return nil } func fileExists(path string) bool { _, err := os.Stat(path) return !errors.Is(err, fs.ErrNotExist) } func delete() error { flag.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: delete REPO Delete the git repo named REPO. `) } flag.Parse() args := flag.Args() if len(args) == 0 { return fmt.Errorf("missing operand\nUse delete -help for usage info") } if len(args) > 1 { return fmt.Errorf("too many positional arguments\nUse delete -help for usage info") } name, err := checkRepoName(args[0]) if err != nil { return err } dir := filepath.Join(codeDir, name) shortName := strings.TrimSuffix(name, ".git") fmt.Fprintf(os.Stderr, "This will irreversibly delete repo %q.\nType %q to confirm: ", name, shortName) scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() || scanner.Text() != shortName { return fmt.Errorf("name does not match") } fmt.Fprintf(os.Stderr, "rm -rf %s\n", shellQuote(dir)) if !fileExists(dir) { return fmt.Errorf("repo %q does not exist", name) } if err := os.RemoveAll(dir); err != nil { return err } return nil } func setRepoDescription(repo, description string) error { descriptionPath := filepath.Join(codeDir, repo, "description") fmt.Fprintf(os.Stderr, "echo %s > %s\n", shellQuote(description), shellQuote(descriptionPath)) desc, err := os.OpenFile(descriptionPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer desc.Close() if _, err := fmt.Fprintln(desc, description); err != nil { return err } if err := desc.Close(); err != nil { return err } return nil } func describe() error { flag.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: describe -d DESCRIPTION REPO Set the description for REPO. `) } d := flag.String("d", "", "description") flag.Parse() args := flag.Args() if len(args) == 0 { return fmt.Errorf("missing operand\nUse describe -help for usage info") } if len(args) > 1 { return fmt.Errorf("too many positional arguments\nUse describe -help for usage info") } if *d == "" { return fmt.Errorf("missing required flag -d\nUse describe -help for usage info") } name, err := checkRepoName(args[0]) if err != nil { return err } if err := setRepoDescription(name, *d); err != nil { return err } return nil } func installFile(srcPath, dstPath string) error { fmt.Fprintf(os.Stderr, "cp %s %s && chmod +x %[2]s\n", shellQuote(srcPath), shellQuote(dstPath)) src, err := os.Open(srcPath) if err != nil { return err } defer src.Close() dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err } defer dst.Close() if _, err := io.Copy(dst, src); err != nil { return err } if err := dst.Close(); err != nil { return err } return nil } func install() error { const dir = "/nezuko/code/git-shell-commands" fmt.Fprintf(os.Stderr, "Install to %q? ", dir) scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() { return fmt.Errorf("exiting") } response := strings.TrimSpace(scanner.Text()) if !(response == "" || strings.EqualFold(response, "y") || strings.EqualFold(response, "yes")) { return fmt.Errorf("exiting") } selfPath := os.Args[0] if !strings.Contains(selfPath, "/") { var err error if selfPath, err = exec.LookPath(selfPath); err != nil { return err } } helpPath := filepath.Join(dir, "help") if err := installFile(selfPath, helpPath); err != nil { return err } for _, cmd := range []string{"create", "delete", "describe"} { cmdPath := filepath.Join(dir, cmd) fmt.Fprintf(os.Stderr, "ln -f %s %s\n", shellQuote(helpPath), shellQuote(cmdPath)) if err := os.Remove(cmdPath); err != nil && !errors.Is(err, os.ErrNotExist) { return err } if err := os.Link(helpPath, cmdPath); err != nil { return err } } return nil } func help() { fmt.Printf(`Available commands: create create a new git repo delete delete a git repo describe set repo description help print this message `) } func main() { var err error switch filepath.Base(os.Args[0]) { case "create": err = create() case "delete": err = delete() case "describe": err = describe() case "git-shell-commands": err = install() default: help() } if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }