repos / git-pr

a self-hosted git collaboration server
git clone https://github.com/picosh/git-pr.git

commit
58bf51e
parent
04029e4
author
Eric Bower
date
2024-05-11 13:53:59 -0400 EDT
refactor: use urfav/cli
5 files changed,  +418, -329
A cli.go
M go.mod
M go.sum
M mdw.go
A cli.go
+363, -0
  1@@ -0,0 +1,363 @@
  2+package git
  3+
  4+import (
  5+	"fmt"
  6+	"io"
  7+	"path/filepath"
  8+	"strconv"
  9+	"text/tabwriter"
 10+	"time"
 11+
 12+	"github.com/charmbracelet/soft-serve/pkg/utils"
 13+	"github.com/charmbracelet/ssh"
 14+	"github.com/charmbracelet/wish"
 15+	"github.com/urfave/cli/v2"
 16+)
 17+
 18+func NewTabWriter(out io.Writer) *tabwriter.Writer {
 19+	return tabwriter.NewWriter(out, 0, 0, 1, ' ', tabwriter.TabIndent)
 20+}
 21+
 22+func getPrID(str string) (int64, error) {
 23+	prID, err := strconv.ParseInt(str, 10, 64)
 24+	return prID, err
 25+}
 26+
 27+func NewCli(sesh ssh.Session, be *Backend, pr GitPatchRequest) *cli.App {
 28+	pubkey := be.Pubkey(sesh.PublicKey())
 29+	app := &cli.App{
 30+		Name:        "ssh",
 31+		Description: "A companion SSH server to allow external collaboration",
 32+		Writer:      sesh,
 33+		ErrWriter:   sesh,
 34+		Commands: []*cli.Command{
 35+			{
 36+				Name:  "git-receive-pack",
 37+				Usage: "Receive what is pushed into the repository",
 38+				Action: func(cCtx *cli.Context) error {
 39+					repoName := cCtx.Args().First()
 40+					err := gitServiceCommands(sesh, be, "git-receive-patch", repoName)
 41+					return err
 42+				},
 43+			},
 44+			{
 45+				Name:  "git-upload-pack",
 46+				Usage: "Send objects packed back to git-fetch-pack",
 47+				Action: func(cCtx *cli.Context) error {
 48+					repoName := cCtx.Args().First()
 49+					err := gitServiceCommands(sesh, be, "git-upload-patch", repoName)
 50+					return err
 51+				},
 52+			},
 53+			{
 54+				Name:  "ls",
 55+				Usage: "list all git repos",
 56+				Action: func(cCtx *cli.Context) error {
 57+					repos, err := pr.GetRepos()
 58+					if err != nil {
 59+						return err
 60+					}
 61+					writer := NewTabWriter(sesh)
 62+					fmt.Fprintln(writer, "Name\tDir")
 63+					for _, repo := range repos {
 64+						fmt.Fprintf(
 65+							writer,
 66+							"%s\t%s\n",
 67+							utils.SanitizeRepo(repo),
 68+							filepath.Join(be.ReposDir(), repo),
 69+						)
 70+					}
 71+					writer.Flush()
 72+					return nil
 73+				},
 74+			},
 75+			{
 76+				Name:  "pr",
 77+				Usage: "manage patch requests",
 78+				Subcommands: []*cli.Command{
 79+					{
 80+						Name:  "ls",
 81+						Usage: "list all patch requests",
 82+						Action: func(cCtx *cli.Context) error {
 83+							prs, err := pr.GetPatchRequests()
 84+							if err != nil {
 85+								return err
 86+							}
 87+
 88+							writer := NewTabWriter(sesh)
 89+							fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
 90+							for _, req := range prs {
 91+								fmt.Fprintf(
 92+									writer,
 93+									"%d\t%s\t[%s]\t%s\n",
 94+									req.ID,
 95+									req.Name,
 96+									req.Status,
 97+									req.CreatedAt.Format(time.RFC3339Nano),
 98+								)
 99+							}
100+							writer.Flush()
101+							return nil
102+						},
103+					},
104+					{
105+						Name:  "create",
106+						Usage: "submit a new patch request",
107+						Action: func(cCtx *cli.Context) error {
108+							repoID := cCtx.Args().First()
109+							request, err := pr.SubmitPatchRequest(pubkey, repoID, sesh)
110+							if err != nil {
111+								return err
112+							}
113+							wish.Printf(
114+								sesh,
115+								"Patch Request submitted! Use the ID for interacting with this Patch Request.\nID\tName\n%d\t%s\n",
116+								request.ID,
117+								request.Name,
118+							)
119+							return nil
120+						},
121+					},
122+					{
123+						Name:  "print",
124+						Usage: "print the patches for a patch request",
125+						Action: func(cCtx *cli.Context) error {
126+							prID, err := getPrID(cCtx.Args().First())
127+							if err != nil {
128+								return err
129+							}
130+
131+							patches, err := pr.GetPatchesByPrID(prID)
132+							if err != nil {
133+								return err
134+							}
135+
136+							if len(patches) == 1 {
137+								wish.Println(sesh, patches[0].RawText)
138+								return nil
139+							}
140+
141+							for _, patch := range patches {
142+								wish.Printf(sesh, "%s\n\n\n", patch.RawText)
143+							}
144+
145+							return nil
146+						},
147+					},
148+					{
149+						Name:  "stats",
150+						Usage: "print patch request with patch stats",
151+						Action: func(cCtx *cli.Context) error {
152+							prID, err := getPrID(cCtx.Args().First())
153+							if err != nil {
154+								return err
155+							}
156+
157+							request, err := pr.GetPatchRequestByID(prID)
158+							if err != nil {
159+								return err
160+							}
161+
162+							writer := NewTabWriter(sesh)
163+							fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
164+							fmt.Fprintf(
165+								writer,
166+								"%d\t%s\t[%s]\t%s\n%s\n\n",
167+								request.ID, request.Name, request.Status, request.CreatedAt.Format(time.RFC3339Nano),
168+								request.Text,
169+							)
170+							writer.Flush()
171+
172+							patches, err := pr.GetPatchesByPrID(prID)
173+							if err != nil {
174+								return err
175+							}
176+
177+							for _, patch := range patches {
178+								reviewTxt := ""
179+								if patch.Review {
180+									reviewTxt = "[review]"
181+								}
182+								wish.Printf(
183+									sesh,
184+									"%s %s %s\n%s <%s>\n%s\n\n---\n%s\n%s\n\n\n",
185+									patch.Title,
186+									reviewTxt,
187+									patch.CommitSha,
188+									patch.AuthorName,
189+									patch.AuthorEmail,
190+									patch.AuthorDate.Format(time.RFC3339Nano),
191+									patch.BodyAppendix,
192+									patch.Body,
193+								)
194+							}
195+
196+							return nil
197+						},
198+					},
199+					{
200+						Name:  "summary",
201+						Usage: "list patches in patch request",
202+						Action: func(cCtx *cli.Context) error {
203+							prID, err := getPrID(cCtx.Args().First())
204+							if err != nil {
205+								return err
206+							}
207+							request, err := pr.GetPatchRequestByID(prID)
208+							if err != nil {
209+								return err
210+							}
211+
212+							writer := NewTabWriter(sesh)
213+							fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
214+							fmt.Fprintf(
215+								writer,
216+								"%d\t%s\t[%s]\t%s\n%s\n",
217+								request.ID, request.Name, request.Status, request.CreatedAt.Format(time.RFC3339Nano),
218+								request.Text,
219+							)
220+							writer.Flush()
221+
222+							patches, err := pr.GetPatchesByPrID(prID)
223+							if err != nil {
224+								return err
225+							}
226+
227+							w := NewTabWriter(sesh)
228+							fmt.Fprintln(w, "Title\tStatus\tCommit\tAuthor\tDate")
229+							for _, patch := range patches {
230+								reviewTxt := ""
231+								if patch.Review {
232+									reviewTxt = "[review]"
233+								}
234+								fmt.Fprintf(
235+									w,
236+									"%s\t%s\t%s\t%s <%s>\t%s\n",
237+									patch.Title,
238+									reviewTxt,
239+									patch.CommitSha,
240+									patch.AuthorName,
241+									patch.AuthorEmail,
242+									patch.AuthorDate.Format(time.RFC3339Nano),
243+								)
244+							}
245+							w.Flush()
246+
247+							return nil
248+						},
249+					},
250+					{
251+						Name:  "accept",
252+						Usage: "accept a patch request",
253+						Action: func(cCtx *cli.Context) error {
254+							prID, err := getPrID(cCtx.Args().First())
255+							if err != nil {
256+								return err
257+							}
258+							if !be.IsAdmin(sesh.PublicKey()) {
259+								return fmt.Errorf("must be admin to accpet PR")
260+							}
261+							err = pr.UpdatePatchRequest(prID, "accept")
262+							return err
263+						},
264+					},
265+					{
266+						Name:  "close",
267+						Usage: "close a patch request",
268+						Action: func(cCtx *cli.Context) error {
269+							prID, err := getPrID(cCtx.Args().First())
270+							if err != nil {
271+								return err
272+							}
273+							if !be.IsAdmin(sesh.PublicKey()) {
274+								return fmt.Errorf("must be admin to close PR")
275+							}
276+							err = pr.UpdatePatchRequest(prID, "close")
277+							return err
278+						},
279+					},
280+					{
281+						Name:  "reopen",
282+						Usage: "reopen a patch request",
283+						Action: func(cCtx *cli.Context) error {
284+							prID, err := getPrID(cCtx.Args().First())
285+							if err != nil {
286+								return err
287+							}
288+							if !be.IsAdmin(sesh.PublicKey()) {
289+								return fmt.Errorf("must be admin to close PR")
290+							}
291+							err = pr.UpdatePatchRequest(prID, "open")
292+							return err
293+						},
294+					},
295+					{
296+						Name:  "add",
297+						Usage: "append a patch to the patch request",
298+						Flags: []cli.Flag{
299+							&cli.BoolFlag{
300+								Name:  "review",
301+								Usage: "mark patch as a review",
302+							},
303+						},
304+						Action: func(cCtx *cli.Context) error {
305+							prID, err := getPrID(cCtx.Args().First())
306+							if err != nil {
307+								return err
308+							}
309+							isAdmin := be.IsAdmin(sesh.PublicKey())
310+							isReview := cCtx.Bool("review")
311+							var req PatchRequest
312+							err = be.DB.Get(&req, "SELECT * FROM patch_requests WHERE id=?", prID)
313+							if err != nil {
314+								return err
315+							}
316+							isPrOwner := req.Pubkey == be.Pubkey(sesh.PublicKey())
317+							if !isAdmin && !isPrOwner {
318+								return fmt.Errorf("unauthorized, you are not the owner of this Patch Request")
319+							}
320+
321+							patch, err := pr.SubmitPatch(pubkey, prID, isReview, sesh)
322+							if err != nil {
323+								return err
324+							}
325+							reviewTxt := ""
326+							if isReview {
327+								err = pr.UpdatePatchRequest(prID, "review")
328+								if err != nil {
329+									return err
330+								}
331+								reviewTxt = "[review]"
332+							}
333+
334+							wish.Println(sesh, "Patch submitted!")
335+							writer := NewTabWriter(sesh)
336+							fmt.Fprintln(
337+								writer,
338+								"ID\tTitle",
339+							)
340+							fmt.Fprintf(
341+								writer,
342+								"%d\t%s %s\n",
343+								patch.ID,
344+								patch.Title,
345+								reviewTxt,
346+							)
347+							writer.Flush()
348+							return nil
349+						},
350+					},
351+					{
352+						Name:  "comment",
353+						Usage: "comment on a patch request",
354+						Action: func(cCtx *cli.Context) error {
355+							return nil
356+						},
357+					},
358+				},
359+			},
360+		},
361+	}
362+
363+	return app
364+}
M go.mod
+4, -0
 1@@ -25,6 +25,7 @@ require (
 2 	github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect
 3 	github.com/charmbracelet/x/exp/term v0.0.0-20240229115032-4b79243a3516 // indirect
 4 	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
 5+	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
 6 	github.com/creack/pty v1.1.21 // indirect
 7 	github.com/dustin/go-humanize v1.0.1 // indirect
 8 	github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
 9@@ -55,7 +56,10 @@ require (
10 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
11 	github.com/rivo/uniseg v0.4.7 // indirect
12 	github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 // indirect
13+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
14 	github.com/sergi/go-diff v1.1.0 // indirect
15+	github.com/urfave/cli/v2 v2.27.2 // indirect
16+	github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
17 	golang.org/x/crypto v0.21.0 // indirect
18 	golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
19 	golang.org/x/mod v0.14.0 // indirect
M go.sum
+8, -0
 1@@ -34,6 +34,8 @@ github.com/charmbracelet/x/exp/term v0.0.0-20240229115032-4b79243a3516 h1:wL/Piy
 2 github.com/charmbracelet/x/exp/term v0.0.0-20240229115032-4b79243a3516/go.mod h1:ntNL6rRIDmBHKUmo6ZKt344wCTcrPsSrfVj72qT8A5U=
 3 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
 4 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 5+github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
 6+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 7 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
 8 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 9 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10@@ -127,6 +129,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
11 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
12 github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0=
13 github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4=
14+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
15+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
16 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
17 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
18 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
19@@ -139,6 +143,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
20 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
21 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
22 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
23+github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
24+github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
25+github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
26+github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
27 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
28 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
29 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
M mdw.go
+6, -329
  1@@ -1,343 +1,20 @@
  2 package git
  3 
  4 import (
  5-	"flag"
  6-	"fmt"
  7-	"io"
  8-	"path/filepath"
  9-	"regexp"
 10-	"strconv"
 11-	"strings"
 12-	"text/tabwriter"
 13-	"time"
 14-
 15-	"github.com/charmbracelet/soft-serve/pkg/git"
 16-	"github.com/charmbracelet/soft-serve/pkg/utils"
 17 	"github.com/charmbracelet/ssh"
 18 	"github.com/charmbracelet/wish"
 19 )
 20 
 21-func gitServiceCommands(sesh ssh.Session, be *Backend, cmd, repoName string) error {
 22-	name := utils.SanitizeRepo(repoName)
 23-	// git bare repositories should end in ".git"
 24-	// https://git-scm.com/docs/gitrepository-layout
 25-	repoID := be.RepoID(name)
 26-	reposDir := be.ReposDir()
 27-	err := git.EnsureWithin(reposDir, repoID)
 28-	if err != nil {
 29-		return err
 30-	}
 31-	repoPath := filepath.Join(reposDir, repoID)
 32-	serviceCmd := git.ServiceCommand{
 33-		Stdin:  sesh,
 34-		Stdout: sesh,
 35-		Stderr: sesh.Stderr(),
 36-		Dir:    repoPath,
 37-		Env:    sesh.Environ(),
 38-	}
 39-
 40-	if cmd == "git-receive-pack" {
 41-		err := git.ReceivePack(sesh.Context(), serviceCmd)
 42-		if err != nil {
 43-			return err
 44-		}
 45-	} else if cmd == "git-upload-pack" {
 46-		err := git.UploadPack(sesh.Context(), serviceCmd)
 47-		if err != nil {
 48-			return err
 49-		}
 50-	}
 51-
 52-	return nil
 53-}
 54-
 55-func flagSet(sesh ssh.Session, cmdName string) *flag.FlagSet {
 56-	cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
 57-	cmd.SetOutput(sesh)
 58-	return cmd
 59-}
 60-
 61-func NewTabWriter(out io.Writer) *tabwriter.Writer {
 62-	return tabwriter.NewWriter(out, 0, 0, 1, ' ', tabwriter.TabIndent)
 63-}
 64-
 65 func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware {
 66-	isNumRe := regexp.MustCompile(`^\d+$`)
 67-
 68 	return func(next ssh.Handler) ssh.Handler {
 69 		return func(sesh ssh.Session) {
 70-			pubkey := be.Pubkey(sesh.PublicKey())
 71 			args := sesh.Command()
 72-			cmd := "help"
 73-			if len(args) > 0 {
 74-				cmd = args[0]
 75-			}
 76-
 77-			if cmd == "git-receive-pack" || cmd == "git-upload-pack" {
 78-				repoName := args[1]
 79-				err := gitServiceCommands(sesh, be, cmd, repoName)
 80-				if err != nil {
 81-					wish.Fatalln(sesh, err)
 82-					return
 83-				}
 84-			} else if cmd == "help" {
 85-				wish.Println(sesh, "commands: [help, pr, ls, git-receive-pack, git-upload-pack]")
 86-			} else if cmd == "ls" {
 87-				repos, err := pr.GetRepos()
 88-				if err != nil {
 89-					wish.Fatalln(sesh, err)
 90-					return
 91-				}
 92-				writer := NewTabWriter(sesh)
 93-				fmt.Fprintln(writer, "Name\tDir")
 94-				for _, repo := range repos {
 95-					fmt.Fprintf(
 96-						writer,
 97-						"%s\t%s\n",
 98-						utils.SanitizeRepo(repo),
 99-						filepath.Join(be.ReposDir(), repo),
100-					)
101-				}
102-				writer.Flush()
103-			} else if cmd == "pr" {
104-				/*
105-					ssh git.sh ls
106-					ssh git.sh pr ls
107-					git format-patch -1 HEAD~1 --stdout | ssh git.sh pr create
108-					ssh git.sh pr print 1
109-					ssh git.sh pr print 1 --summary
110-					ssh git.sh pr print 1 --ls
111-					ssh git.sh pr accept 1
112-					ssh git.sh pr close 1
113-					git format-patch -1 HEAD~1 --stdout | ssh git.sh pr review 1
114-					echo "my feedback" | ssh git.sh pr comment 1
115-				*/
116-				prCmd := flagSet(sesh, "pr")
117-				out := prCmd.Bool("stdout", false, "print patchset to stdout")
118-				accept := prCmd.Bool("accept", false, "mark patch request as accepted")
119-				closed := prCmd.Bool("close", false, "mark patch request as closed")
120-				review := prCmd.Bool("review", false, "mark patch request as reviewed")
121-				stats := prCmd.Bool("stats", false, "return summary instead of patches")
122-				prLs := prCmd.Bool("ls", false, "return list of patches")
123-
124-				if len(args) < 2 {
125-					wish.Fatalln(sesh, "must provide repo name or patch request ID")
126-					return
127-				}
128-
129-				var err error
130-				err = prCmd.Parse(args[2:])
131-				if err != nil {
132-					wish.Fatalln(sesh, err)
133-					return
134-				}
135-				subCmd := strings.TrimSpace(args[1])
136-
137-				repoID := ""
138-				var prID int64
139-				// figure out subcommand based on what was passed in
140-				if subCmd == "ls" {
141-					// skip proccessing
142-				} else if isNumRe.MatchString(subCmd) {
143-					// we probably have a patch request id
144-					prID, err = strconv.ParseInt(subCmd, 10, 64)
145-					if err != nil {
146-						wish.Fatalln(sesh, err)
147-						return
148-					}
149-					subCmd = "patchRequest"
150-				} else {
151-					// we probably have a repo name
152-					repoID = be.RepoID(subCmd)
153-					subCmd = "submitPatchRequest"
154-				}
155-
156-				if subCmd == "ls" {
157-					prs, err := pr.GetPatchRequests()
158-					if err != nil {
159-						wish.Fatalln(sesh, err)
160-						return
161-					}
162-
163-					writer := NewTabWriter(sesh)
164-					fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
165-					for _, req := range prs {
166-						fmt.Fprintf(
167-							writer,
168-							"%d\t%s\t[%s]\t%s\n",
169-							req.ID,
170-							req.Name,
171-							req.Status,
172-							req.CreatedAt.Format(time.RFC3339Nano),
173-						)
174-					}
175-					writer.Flush()
176-				} else if subCmd == "submitPatchRequest" {
177-					request, err := pr.SubmitPatchRequest(pubkey, repoID, sesh)
178-					if err != nil {
179-						wish.Fatalln(sesh, err)
180-						return
181-					}
182-					wish.Printf(sesh, "Patch Request submitted! Use the ID for interacting with this Patch Request.\nID\tName\n%d\t%s\n", request.ID, request.Name)
183-				} else if subCmd == "patchRequest" {
184-					if *prLs {
185-						_, err := pr.GetPatchRequestByID(prID)
186-						if err != nil {
187-							wish.Fatalln(sesh, err)
188-							return
189-						}
190-
191-						patches, err := pr.GetPatchesByPrID(prID)
192-						if err != nil {
193-							wish.Fatalln(sesh, err)
194-							return
195-						}
196-
197-						writer := NewTabWriter(sesh)
198-						fmt.Fprintln(writer, "ID\tTitle\tReview\tAuthor\tDate")
199-						for _, patch := range patches {
200-							fmt.Fprintf(
201-								writer,
202-								"%d\t%s\t%t\t%s\t%s\n",
203-								patch.ID,
204-								patch.Title,
205-								patch.Review,
206-								patch.AuthorName,
207-								patch.AuthorDate.Format(time.RFC3339Nano),
208-							)
209-						}
210-						writer.Flush()
211-						return
212-					}
213-
214-					if *stats {
215-						request, err := pr.GetPatchRequestByID(prID)
216-						if err != nil {
217-							wish.Fatalln(sesh, err)
218-							return
219-						}
220-
221-						writer := NewTabWriter(sesh)
222-						fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
223-						fmt.Fprintf(
224-							writer,
225-							"%d\t%s\t[%s]\t%s\n%s\n\n",
226-							request.ID, request.Name, request.Status, request.CreatedAt.Format(time.RFC3339Nano),
227-							request.Text,
228-						)
229-						writer.Flush()
230-
231-						patches, err := pr.GetPatchesByPrID(prID)
232-						if err != nil {
233-							wish.Fatalln(sesh, err)
234-							return
235-						}
236-
237-						for _, patch := range patches {
238-							reviewTxt := ""
239-							if patch.Review {
240-								reviewTxt = "[review]"
241-							}
242-							wish.Printf(
243-								sesh,
244-								"%s %s %s\n%s <%s>\n%s\n\n---\n%s\n%s\n\n\n",
245-								patch.Title,
246-								reviewTxt,
247-								patch.CommitSha,
248-								patch.AuthorName,
249-								patch.AuthorEmail,
250-								patch.AuthorDate.Format(time.RFC3339Nano),
251-								patch.BodyAppendix,
252-								patch.Body,
253-							)
254-						}
255-						return
256-					}
257-
258-					if *out {
259-						patches, err := pr.GetPatchesByPrID(prID)
260-						if err != nil {
261-							wish.Fatalln(sesh, err)
262-							return
263-						}
264-
265-						if len(patches) == 1 {
266-							wish.Println(sesh, patches[0].RawText)
267-							return
268-						}
269-
270-						for _, patch := range patches {
271-							wish.Printf(sesh, "%s\n\n\n", patch.RawText)
272-						}
273-					} else if *accept {
274-						if !be.IsAdmin(sesh.PublicKey()) {
275-							wish.Fatalln(sesh, "must be admin to accept PR")
276-							return
277-						}
278-						err := pr.UpdatePatchRequest(prID, "accept")
279-						if err != nil {
280-							wish.Fatalln(sesh, err)
281-							return
282-						}
283-					} else if *closed {
284-						if !be.IsAdmin(sesh.PublicKey()) {
285-							wish.Fatalln(sesh, "must be admin to close PR")
286-							return
287-						}
288-						err := pr.UpdatePatchRequest(prID, "close")
289-						if err != nil {
290-							wish.Fatalln(sesh, err)
291-							return
292-						}
293-					} else {
294-						isAdmin := be.IsAdmin(sesh.PublicKey())
295-						var req PatchRequest
296-						err = be.DB.Get(&req, "SELECT * FROM patch_requests WHERE id=?", prID)
297-						if err != nil {
298-							wish.Fatalln(sesh, err)
299-							return
300-						}
301-						isPrOwner := req.Pubkey == be.Pubkey(sesh.PublicKey())
302-						if !isAdmin && !isPrOwner {
303-							wish.Fatalln(sesh, "unauthorized, you are not the owner of this Patch Request")
304-							return
305-						}
306-
307-						patch, err := pr.SubmitPatch(pubkey, prID, *review, sesh)
308-						if err != nil {
309-							wish.Fatalln(sesh, err)
310-							return
311-						}
312-						reviewTxt := ""
313-						if *review {
314-							err = pr.UpdatePatchRequest(prID, "review")
315-							if err != nil {
316-								wish.Fatalln(sesh, err)
317-								return
318-							}
319-							reviewTxt = "[review]"
320-						}
321-
322-						wish.Println(sesh, "Patch submitted!")
323-						writer := NewTabWriter(sesh)
324-						fmt.Fprintln(
325-							writer,
326-							"ID\tTitle",
327-						)
328-						fmt.Fprintf(
329-							writer,
330-							"%d\t%s %s\n",
331-							patch.ID,
332-							patch.Title,
333-							reviewTxt,
334-						)
335-						writer.Flush()
336-					}
337-				}
338-
339-				return
340-			} else {
341-				next(sesh)
342+			cli := NewCli(sesh, be, pr)
343+			margs := append([]string{"git"}, args...)
344+			err := cli.Run(margs)
345+			if err != nil {
346+				be.Logger.Error("error when running cli", "err", err)
347+				wish.Fatalln(sesh, err)
348 				return
349 			}
350 		}
M util.go
+37, -0
 1@@ -6,8 +6,11 @@ import (
 2 	"errors"
 3 	"io"
 4 	"os"
 5+	"path/filepath"
 6 	"strings"
 7 
 8+	"github.com/charmbracelet/soft-serve/pkg/git"
 9+	"github.com/charmbracelet/soft-serve/pkg/utils"
10 	"github.com/charmbracelet/ssh"
11 )
12 
13@@ -43,3 +46,37 @@ func getAuthorizedKeys(path string) ([]ssh.PublicKey, error) {
14 
15 	return keys, nil
16 }
17+
18+func gitServiceCommands(sesh ssh.Session, be *Backend, cmd, repoName string) error {
19+	name := utils.SanitizeRepo(repoName)
20+	// git bare repositories should end in ".git"
21+	// https://git-scm.com/docs/gitrepository-layout
22+	repoID := be.RepoID(name)
23+	reposDir := be.ReposDir()
24+	err := git.EnsureWithin(reposDir, repoID)
25+	if err != nil {
26+		return err
27+	}
28+	repoPath := filepath.Join(reposDir, repoID)
29+	serviceCmd := git.ServiceCommand{
30+		Stdin:  sesh,
31+		Stdout: sesh,
32+		Stderr: sesh.Stderr(),
33+		Dir:    repoPath,
34+		Env:    sesh.Environ(),
35+	}
36+
37+	if cmd == "git-receive-pack" {
38+		err := git.ReceivePack(sesh.Context(), serviceCmd)
39+		if err != nil {
40+			return err
41+		}
42+	} else if cmd == "git-upload-pack" {
43+		err := git.UploadPack(sesh.Context(), serviceCmd)
44+		if err != nil {
45+			return err
46+		}
47+	}
48+
49+	return nil
50+}