repos / git-pr

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

commit
fbfd037
parent
7fda3e7
author
Eric Bower
date
2024-05-10 12:05:26 -0400 EDT
progress
2 files changed,  +276, -202
M mdw.go
A pr.go
M mdw.go
+78, -202
  1@@ -1,18 +1,16 @@
  2 package git
  3 
  4 import (
  5-	"bytes"
  6 	"flag"
  7 	"fmt"
  8 	"io"
  9-	"os"
 10 	"path/filepath"
 11 	"regexp"
 12 	"strconv"
 13 	"strings"
 14+	"text/tabwriter"
 15 	"time"
 16 
 17-	"github.com/bluekeyes/go-gitdiff/gitdiff"
 18 	"github.com/charmbracelet/soft-serve/pkg/git"
 19 	"github.com/charmbracelet/soft-serve/pkg/utils"
 20 	"github.com/charmbracelet/ssh"
 21@@ -59,197 +57,8 @@ func flagSet(sesh ssh.Session, cmdName string) *flag.FlagSet {
 22 	return cmd
 23 }
 24 
 25-// ssh git.sh ls
 26-// git format-patch --stdout | ssh git.sh pr test
 27-// git format-patch --stdout | ssh git.sh pr 123 --review
 28-// ssh git.sh pr ls
 29-// ssh git.sh pr 123 --stdout | git am -3
 30-// ssh git.sh pr 123 --approve # or --close
 31-// echo "here is a comment" | ssh git.sh pr 123 --comment
 32-
 33-type GitPatchRequest interface {
 34-	GetRepos() ([]string, error)
 35-	SubmitPatchRequest(pubkey string, repoID string, patches io.Reader) (*PatchRequest, error)
 36-	SubmitPatch(pubkey string, prID int64, review bool, patch io.Reader) (*Patch, error)
 37-	GetPatchRequestByID(prID int64) (*PatchRequest, error)
 38-	GetPatchRequests() ([]*PatchRequest, error)
 39-	GetPatchesByPrID(prID int64) ([]*Patch, error)
 40-	UpdatePatchRequest(prID int64, status string) error
 41-}
 42-
 43-type PrCmd struct {
 44-	Backend *Backend
 45-}
 46-
 47-var _ GitPatchRequest = PrCmd{}
 48-var _ GitPatchRequest = (*PrCmd)(nil)
 49-
 50-func (pr PrCmd) GetRepos() ([]string, error) {
 51-	repos := []string{}
 52-	entries, err := os.ReadDir(pr.Backend.ReposDir())
 53-	if err != nil {
 54-		return repos, err
 55-	}
 56-	for _, entry := range entries {
 57-		if entry.IsDir() {
 58-			repos = append(repos, entry.Name())
 59-		}
 60-	}
 61-	return repos, nil
 62-}
 63-
 64-func (pr PrCmd) GetPatchesByPrID(prID int64) ([]*Patch, error) {
 65-	patches := []*Patch{}
 66-	err := pr.Backend.DB.Select(
 67-		&patches,
 68-		"SELECT * FROM patches WHERE patch_request_id=?",
 69-		prID,
 70-	)
 71-	if err != nil {
 72-		return patches, err
 73-	}
 74-	if len(patches) == 0 {
 75-		return patches, fmt.Errorf("no patches found for Patch Request ID: %d", prID)
 76-	}
 77-	return patches, nil
 78-}
 79-
 80-func (cmd PrCmd) GetPatchRequests() ([]*PatchRequest, error) {
 81-	prs := []*PatchRequest{}
 82-	err := cmd.Backend.DB.Select(
 83-		&prs,
 84-		"SELECT * FROM patch_requests",
 85-	)
 86-	return prs, err
 87-}
 88-
 89-func (cmd PrCmd) GetPatchRequestByID(prID int64) (*PatchRequest, error) {
 90-	pr := PatchRequest{}
 91-	err := cmd.Backend.DB.Get(
 92-		&pr,
 93-		"SELECT * FROM patch_requests WHERE id=?",
 94-		prID,
 95-	)
 96-	return &pr, err
 97-}
 98-
 99-// status types: open, close, accept, review
100-func (cmd PrCmd) UpdatePatchRequest(prID int64, status string) error {
101-	_, err := cmd.Backend.DB.Exec(
102-		"UPDATE patch_requests SET status=? WHERE id=?", status, prID,
103-	)
104-	return err
105-}
106-
107-func (cmd PrCmd) SubmitPatch(pubkey string, prID int64, review bool, patch io.Reader) (*Patch, error) {
108-	pr := PatchRequest{}
109-	err := cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
110-	if err != nil {
111-		return nil, err
112-	}
113-
114-	// need to read io.Reader from session twice
115-	var buf bytes.Buffer
116-	tee := io.TeeReader(patch, &buf)
117-
118-	_, preamble, err := gitdiff.Parse(tee)
119-	if err != nil {
120-		return nil, err
121-	}
122-	header, err := gitdiff.ParsePatchHeader(preamble)
123-	if err != nil {
124-		return nil, err
125-	}
126-
127-	patchID := 0
128-	row := cmd.Backend.DB.QueryRow(
129-		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, review, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
130-		pubkey,
131-		prID,
132-		header.Author.Name,
133-		header.Author.Email,
134-		header.AuthorDate,
135-		header.Title,
136-		header.Body,
137-		header.BodyAppendix,
138-		header.SHA,
139-		review,
140-		buf.String(),
141-	)
142-	err = row.Scan(&patchID)
143-	if err != nil {
144-		return nil, err
145-	}
146-
147-	var patchRec Patch
148-	err = cmd.Backend.DB.Get(&patchRec, "SELECT * FROM patches WHERE id=?", patchID)
149-	return &patchRec, err
150-}
151-
152-func (cmd PrCmd) SubmitPatchRequest(pubkey string, repoID string, patches io.Reader) (*PatchRequest, error) {
153-	err := git.EnsureWithin(cmd.Backend.ReposDir(), repoID)
154-	if err != nil {
155-		return nil, err
156-	}
157-	loc := filepath.Join(cmd.Backend.ReposDir(), repoID)
158-	_, err = os.Stat(loc)
159-	if os.IsNotExist(err) {
160-		return nil, fmt.Errorf("repo does not exist: %s", loc)
161-	}
162-
163-	// need to read io.Reader from session twice
164-	var buf bytes.Buffer
165-	tee := io.TeeReader(patches, &buf)
166-
167-	_, preamble, err := gitdiff.Parse(tee)
168-	if err != nil {
169-		return nil, err
170-	}
171-	header, err := gitdiff.ParsePatchHeader(preamble)
172-	if err != nil {
173-		return nil, err
174-	}
175-	prName := header.Title
176-	prDesc := header.Body
177-
178-	var prID int64
179-	row := cmd.Backend.DB.QueryRow(
180-		"INSERT INTO patch_requests (pubkey, repo_id, name, text, status, updated_at) VALUES(?, ?, ?, ?, ?, ?) RETURNING id",
181-		pubkey,
182-		repoID,
183-		prName,
184-		prDesc,
185-		"open",
186-		time.Now(),
187-	)
188-	err = row.Scan(&prID)
189-	if err != nil {
190-		return nil, err
191-	}
192-	if prID == 0 {
193-		return nil, fmt.Errorf("could not create patch request")
194-	}
195-
196-	_, err = cmd.Backend.DB.Exec(
197-		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
198-		pubkey,
199-		prID,
200-		header.Author.Name,
201-		header.Author.Email,
202-		header.AuthorDate,
203-		header.Title,
204-		header.Body,
205-		header.BodyAppendix,
206-		header.SHA,
207-		buf.String(),
208-	)
209-	if err != nil {
210-		return nil, err
211-	}
212-
213-	var pr PatchRequest
214-	err = cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
215-	return &pr, err
216+func NewTabWriter(out io.Writer) *tabwriter.Writer {
217+	return tabwriter.NewWriter(out, 0, 0, 1, ' ', tabwriter.TabIndent)
218 }
219 
220 func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware {
221@@ -279,15 +88,17 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
222 					wish.Fatalln(sesh, err)
223 					return
224 				}
225-				wish.Printf(sesh, "Name\tDir\n")
226+				writer := NewTabWriter(sesh)
227+				fmt.Fprintln(writer, "Name\tDir")
228 				for _, repo := range repos {
229-					wish.Printf(
230-						sesh,
231+					fmt.Fprintf(
232+						writer,
233 						"%s\t%s\n",
234 						utils.SanitizeRepo(repo),
235 						filepath.Join(be.ReposDir(), repo),
236 					)
237 				}
238+				writer.Flush()
239 			} else if cmd == "pr" {
240 				prCmd := flagSet(sesh, "pr")
241 				out := prCmd.Bool("stdout", false, "print patchset to stdout")
242@@ -295,6 +106,7 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
243 				closed := prCmd.Bool("close", false, "mark patch request as closed")
244 				review := prCmd.Bool("review", false, "mark patch request as reviewed")
245 				stats := prCmd.Bool("stats", false, "return summary instead of patches")
246+				prLs := prCmd.Bool("ls", false, "return list of patches")
247 
248 				if len(args) < 2 {
249 					wish.Fatalln(sesh, "must provide repo name or patch request ID")
250@@ -334,10 +146,20 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
251 						wish.Fatalln(sesh, err)
252 						return
253 					}
254-					wish.Printf(sesh, "Name\tID\n")
255+
256+					writer := NewTabWriter(sesh)
257+					fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
258 					for _, req := range prs {
259-						wish.Printf(sesh, "%s\t%d\n", req.Name, req.ID)
260+						fmt.Fprintf(
261+							writer,
262+							"%d\t%s\t[%s]\t%s\n",
263+							req.ID,
264+							req.Name,
265+							req.Status,
266+							req.CreatedAt.Format(time.RFC3339Nano),
267+						)
268 					}
269+					writer.Flush()
270 				} else if subCmd == "submitPatchRequest" {
271 					request, err := pr.SubmitPatchRequest(pubkey, repoID, sesh)
272 					if err != nil {
273@@ -346,6 +168,36 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
274 					}
275 					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)
276 				} else if subCmd == "patchRequest" {
277+					if *prLs {
278+						_, err := pr.GetPatchRequestByID(prID)
279+						if err != nil {
280+							wish.Fatalln(sesh, err)
281+							return
282+						}
283+
284+						patches, err := pr.GetPatchesByPrID(prID)
285+						if err != nil {
286+							wish.Fatalln(sesh, err)
287+							return
288+						}
289+
290+						writer := NewTabWriter(sesh)
291+						fmt.Fprintln(writer, "ID\tTitle\tReview\tAuthor\tDate")
292+						for _, patch := range patches {
293+							fmt.Fprintf(
294+								writer,
295+								"%d\t%s\t%t\t%s\t%s\n",
296+								patch.ID,
297+								patch.Title,
298+								patch.Review,
299+								patch.AuthorName,
300+								patch.AuthorDate.Format(time.RFC3339Nano),
301+							)
302+						}
303+						writer.Flush()
304+						return
305+					}
306+
307 					if *stats {
308 						request, err := pr.GetPatchRequestByID(prID)
309 						if err != nil {
310@@ -353,7 +205,15 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
311 							return
312 						}
313 
314-						wish.Printf(sesh, "%s\t[%s]\n%s\n%s\n%s\n", request.Name, request.Status, request.CreatedAt.Format(time.RFC3339Nano), request.Pubkey, request.Text)
315+						writer := NewTabWriter(sesh)
316+						fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
317+						fmt.Fprintf(
318+							writer,
319+							"%d\t%s\t[%s]\t%s\n%s\n\n",
320+							request.ID, request.Name, request.Status, request.CreatedAt.Format(time.RFC3339Nano),
321+							request.Text,
322+						)
323+						writer.Flush()
324 
325 						patches, err := pr.GetPatchesByPrID(prID)
326 						if err != nil {
327@@ -368,7 +228,7 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
328 							}
329 							wish.Printf(
330 								sesh,
331-								"%s %s %s\n%s <%s>\n%s\n\n---\n%s\n%s\n",
332+								"%s %s %s\n%s <%s>\n%s\n\n---\n%s\n%s\n\n\n",
333 								patch.Title,
334 								reviewTxt,
335 								patch.CommitSha,
336@@ -436,14 +296,30 @@ func GitPatchRequestMiddleware(be *Backend, pr GitPatchRequest) wish.Middleware
337 							wish.Fatalln(sesh, err)
338 							return
339 						}
340+						reviewTxt := ""
341 						if *review {
342 							err = pr.UpdatePatchRequest(prID, "review")
343 							if err != nil {
344 								wish.Fatalln(sesh, err)
345 								return
346 							}
347+							reviewTxt = "[review]"
348 						}
349-						wish.Printf(sesh, "Patch submitted! (ID:%d)\n", patch.ID)
350+
351+						wish.Println(sesh, "Patch submitted!")
352+						writer := NewTabWriter(sesh)
353+						fmt.Fprintln(
354+							writer,
355+							"ID\tTitle",
356+						)
357+						fmt.Fprintf(
358+							writer,
359+							"%d\t%s %s\n",
360+							patch.ID,
361+							patch.Title,
362+							reviewTxt,
363+						)
364+						writer.Flush()
365 					}
366 				}
367 
A pr.go
+198, -0
  1@@ -0,0 +1,198 @@
  2+package git
  3+
  4+import (
  5+	"bytes"
  6+	"fmt"
  7+	"io"
  8+	"os"
  9+	"path/filepath"
 10+	"time"
 11+
 12+	"github.com/bluekeyes/go-gitdiff/gitdiff"
 13+	"github.com/charmbracelet/soft-serve/pkg/git"
 14+)
 15+
 16+type GitPatchRequest interface {
 17+	GetRepos() ([]string, error)
 18+	SubmitPatchRequest(pubkey string, repoID string, patches io.Reader) (*PatchRequest, error)
 19+	SubmitPatch(pubkey string, prID int64, review bool, patch io.Reader) (*Patch, error)
 20+	GetPatchRequestByID(prID int64) (*PatchRequest, error)
 21+	GetPatchRequests() ([]*PatchRequest, error)
 22+	GetPatchesByPrID(prID int64) ([]*Patch, error)
 23+	UpdatePatchRequest(prID int64, status string) error
 24+}
 25+
 26+type PrCmd struct {
 27+	Backend *Backend
 28+}
 29+
 30+var _ GitPatchRequest = PrCmd{}
 31+var _ GitPatchRequest = (*PrCmd)(nil)
 32+
 33+func (pr PrCmd) GetRepos() ([]string, error) {
 34+	repos := []string{}
 35+	entries, err := os.ReadDir(pr.Backend.ReposDir())
 36+	if err != nil {
 37+		return repos, err
 38+	}
 39+	for _, entry := range entries {
 40+		if entry.IsDir() {
 41+			repos = append(repos, entry.Name())
 42+		}
 43+	}
 44+	return repos, nil
 45+}
 46+
 47+func (pr PrCmd) GetPatchesByPrID(prID int64) ([]*Patch, error) {
 48+	patches := []*Patch{}
 49+	err := pr.Backend.DB.Select(
 50+		&patches,
 51+		"SELECT * FROM patches WHERE patch_request_id=?",
 52+		prID,
 53+	)
 54+	if err != nil {
 55+		return patches, err
 56+	}
 57+	if len(patches) == 0 {
 58+		return patches, fmt.Errorf("no patches found for Patch Request ID: %d", prID)
 59+	}
 60+	return patches, nil
 61+}
 62+
 63+func (cmd PrCmd) GetPatchRequests() ([]*PatchRequest, error) {
 64+	prs := []*PatchRequest{}
 65+	err := cmd.Backend.DB.Select(
 66+		&prs,
 67+		"SELECT * FROM patch_requests",
 68+	)
 69+	return prs, err
 70+}
 71+
 72+func (cmd PrCmd) GetPatchRequestByID(prID int64) (*PatchRequest, error) {
 73+	pr := PatchRequest{}
 74+	err := cmd.Backend.DB.Get(
 75+		&pr,
 76+		"SELECT * FROM patch_requests WHERE id=?",
 77+		prID,
 78+	)
 79+	return &pr, err
 80+}
 81+
 82+// Status types: open, close, accept, review.
 83+func (cmd PrCmd) UpdatePatchRequest(prID int64, status string) error {
 84+	_, err := cmd.Backend.DB.Exec(
 85+		"UPDATE patch_requests SET status=? WHERE id=?", status, prID,
 86+	)
 87+	return err
 88+}
 89+
 90+func (cmd PrCmd) SubmitPatch(pubkey string, prID int64, review bool, patch io.Reader) (*Patch, error) {
 91+	pr := PatchRequest{}
 92+	err := cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
 93+	if err != nil {
 94+		return nil, err
 95+	}
 96+
 97+	// need to read io.Reader from session twice
 98+	var buf bytes.Buffer
 99+	tee := io.TeeReader(patch, &buf)
100+
101+	_, preamble, err := gitdiff.Parse(tee)
102+	if err != nil {
103+		return nil, err
104+	}
105+	header, err := gitdiff.ParsePatchHeader(preamble)
106+	if err != nil {
107+		return nil, err
108+	}
109+
110+	patchID := 0
111+	row := cmd.Backend.DB.QueryRow(
112+		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, review, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
113+		pubkey,
114+		prID,
115+		header.Author.Name,
116+		header.Author.Email,
117+		header.AuthorDate,
118+		header.Title,
119+		header.Body,
120+		header.BodyAppendix,
121+		header.SHA,
122+		review,
123+		buf.String(),
124+	)
125+	err = row.Scan(&patchID)
126+	if err != nil {
127+		return nil, err
128+	}
129+
130+	var patchRec Patch
131+	err = cmd.Backend.DB.Get(&patchRec, "SELECT * FROM patches WHERE id=?", patchID)
132+	return &patchRec, err
133+}
134+
135+func (cmd PrCmd) SubmitPatchRequest(pubkey string, repoID string, patches io.Reader) (*PatchRequest, error) {
136+	err := git.EnsureWithin(cmd.Backend.ReposDir(), repoID)
137+	if err != nil {
138+		return nil, err
139+	}
140+	loc := filepath.Join(cmd.Backend.ReposDir(), repoID)
141+	_, err = os.Stat(loc)
142+	if os.IsNotExist(err) {
143+		return nil, fmt.Errorf("repo does not exist: %s", loc)
144+	}
145+
146+	// need to read io.Reader from session twice
147+	var buf bytes.Buffer
148+	tee := io.TeeReader(patches, &buf)
149+
150+	_, preamble, err := gitdiff.Parse(tee)
151+	if err != nil {
152+		return nil, err
153+	}
154+	header, err := gitdiff.ParsePatchHeader(preamble)
155+	if err != nil {
156+		return nil, err
157+	}
158+	prName := header.Title
159+	prDesc := header.Body
160+
161+	var prID int64
162+	row := cmd.Backend.DB.QueryRow(
163+		"INSERT INTO patch_requests (pubkey, repo_id, name, text, status, updated_at) VALUES(?, ?, ?, ?, ?, ?) RETURNING id",
164+		pubkey,
165+		repoID,
166+		prName,
167+		prDesc,
168+		"open",
169+		time.Now(),
170+	)
171+	err = row.Scan(&prID)
172+	if err != nil {
173+		return nil, err
174+	}
175+	if prID == 0 {
176+		return nil, fmt.Errorf("could not create patch request")
177+	}
178+
179+	_, err = cmd.Backend.DB.Exec(
180+		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
181+		pubkey,
182+		prID,
183+		header.Author.Name,
184+		header.Author.Email,
185+		header.AuthorDate,
186+		header.Title,
187+		header.Body,
188+		header.BodyAppendix,
189+		header.SHA,
190+		buf.String(),
191+	)
192+	if err != nil {
193+		return nil, err
194+	}
195+
196+	var pr PatchRequest
197+	err = cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
198+	return &pr, err
199+}