main git-pr / util / util.go
Eric Bower  ·  2026-02-25
  1package util
  2
  3import (
  4	"crypto/ed25519"
  5	"crypto/rand"
  6	"fmt"
  7	"io"
  8	"os"
  9	"path/filepath"
 10	"regexp"
 11	"strings"
 12
 13	"golang.org/x/crypto/ssh"
 14)
 15
 16func CreateTmpDir() string {
 17	tmp, err := os.MkdirTemp(os.TempDir(), "patchbin*")
 18	if err != nil {
 19		panic(err)
 20	}
 21	return tmp
 22}
 23
 24func CreateCfgFile(dataDir, cfgTmpl string, adminKey UserSSH) string {
 25	cfgPath := filepath.Join(dataDir, "patchbin.toml")
 26	cfgFi, err := os.Create(cfgPath)
 27	if err != nil {
 28		panic(err)
 29	}
 30	_, _ = fmt.Fprintf(cfgFi, cfgTmpl, dataDir, adminKey.Public())
 31	_ = cfgFi.Close()
 32	return cfgPath
 33}
 34
 35type UserSSH struct {
 36	username string
 37	signer   ssh.Signer
 38}
 39
 40func NewUserSSH(username string, signer ssh.Signer) *UserSSH {
 41	return &UserSSH{
 42		username: username,
 43		signer:   signer,
 44	}
 45}
 46
 47func (s UserSSH) Public() string {
 48	pubkey := s.signer.PublicKey()
 49	return string(ssh.MarshalAuthorizedKey(pubkey))
 50}
 51
 52func (s UserSSH) MustCmd(patch []byte, cmd string) string {
 53	res, err := s.Cmd(patch, cmd)
 54	if err != nil {
 55		panic(err)
 56	}
 57	return res
 58}
 59
 60func (s UserSSH) Cmd(patch []byte, cmd string) (string, error) {
 61	host := "localhost:2222"
 62
 63	config := &ssh.ClientConfig{
 64		User: s.username,
 65		Auth: []ssh.AuthMethod{
 66			ssh.PublicKeys(s.signer),
 67		},
 68		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 69	}
 70
 71	client, err := ssh.Dial("tcp", host, config)
 72	if err != nil {
 73		return "", err
 74	}
 75	defer func() {
 76		_ = client.Close()
 77	}()
 78
 79	session, err := client.NewSession()
 80	if err != nil {
 81		return "", err
 82	}
 83	defer func() {
 84		_ = session.Close()
 85	}()
 86
 87	stdinPipe, err := session.StdinPipe()
 88	if err != nil {
 89		return "", err
 90	}
 91
 92	stdoutPipe, err := session.StdoutPipe()
 93	if err != nil {
 94		return "", err
 95	}
 96
 97	stderrPipe, err := session.StderrPipe()
 98	if err != nil {
 99		return "", err
100	}
101
102	if err := session.Start(cmd); err != nil {
103		return "", err
104	}
105
106	if patch != nil {
107		_, err = stdinPipe.Write(patch)
108		if err != nil {
109			return "", err
110		}
111	}
112
113	_ = stdinPipe.Close()
114
115	var stdoutBuf, stderrBuf strings.Builder
116	go func() { _, _ = io.Copy(&stderrBuf, stderrPipe) }()
117	_, _ = io.Copy(&stdoutBuf, stdoutPipe)
118
119	err = session.Wait()
120	stderr := stderrBuf.String()
121	if err != nil {
122		return "", fmt.Errorf("ssh command failed: %w (stderr: %s)", err, stderr)
123	}
124
125	return stdoutBuf.String(), nil
126}
127
128// ParsePRID extracts the PR ID from the output of `pr create`.
129// Looks for the URL line: "URL: https://host/prs/123"
130func ParsePRID(output string) string {
131	re := regexp.MustCompile(`/prs/(\d+)`)
132	matches := re.FindStringSubmatch(output)
133	if len(matches) < 2 {
134		return "1" // fallback
135	}
136	return matches[1]
137}
138
139func GenerateKeys() (UserSSH, UserSSH) {
140	_, adminKey, err := ed25519.GenerateKey(rand.Reader)
141	if err != nil {
142		panic(err)
143	}
144
145	adminSigner, err := ssh.NewSignerFromKey(adminKey)
146	if err != nil {
147		panic(err)
148	}
149
150	_, userKey, err := ed25519.GenerateKey(rand.Reader)
151	if err != nil {
152		panic(err)
153	}
154
155	userSigner, err := ssh.NewSignerFromKey(userKey)
156	if err != nil {
157		panic(err)
158	}
159
160	return UserSSH{
161			username: "admin",
162			signer:   adminSigner,
163		}, UserSSH{
164			username: "contributor",
165			signer:   userSigner,
166		}
167}