repos / git-pr

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

commit
8b876e0
parent
584e21b
author
Eric Bower
date
2024-05-05 11:09:35 -0400 EDT
progress
7 files changed,  +215, -264
M db.go
M mdw.go
M ssh.go
M Makefile
+4, -0
 1@@ -3,6 +3,10 @@ fmt:
 2 	deno fmt README.md
 3 .PHONY: fmt
 4 
 5+lint:
 6+	golangci-lint run -E goimports -E godot --timeout 10m
 7+.PHONY: lint
 8+
 9 build:
10 	go build -o ./build/git ./cmd/git
11 .PHONY: build
M backend.go
+0, -2
1@@ -6,8 +6,6 @@ import (
2 
3 	"github.com/charmbracelet/ssh"
4 	gossh "golang.org/x/crypto/ssh"
5-	// ssgit "github.com/charmbracelet/soft-serve/git"
6-	// "github.com/charmbracelet/soft-serve/pkg/utils"
7 )
8 
9 type Backend struct {
M db.go
+43, -1
 1@@ -2,11 +2,53 @@ package git
 2 
 3 import (
 4 	"log/slog"
 5+	"time"
 6 
 7 	"github.com/jmoiron/sqlx"
 8-	_ "modernc.org/sqlite" // sqlite driver
 9+	_ "modernc.org/sqlite"
10 )
11 
12+// PatchRequest is a database model for patches submitted to a Repo
13+type PatchRequest struct {
14+	ID        int64     `db:"id"`
15+	Pubkey    string    `db:"pubkey"`
16+	RepoID    int64     `db:"repo_id"`
17+	Name      string    `db:"name"`
18+	Text      string    `db:"text"`
19+	CreatedAt time.Time `db:"created_at"`
20+	UpdatedAt time.Time `db:"updated_at"`
21+}
22+
23+// Patch is a database model for a single entry in a patchset
24+// This usually corresponds to a git commit.
25+type Patch struct {
26+	ID             int64     `db:"id"`
27+	Pubkey         string    `db:"pubkey"`
28+	PatchRequestID int64     `db:"patch_request_id"`
29+	AuthorName     string    `db:"author_name"`
30+	AuthorEmail    string    `db:"author_email"`
31+	Title          string    `db:"title"`
32+	Body           string    `db:"body"`
33+	CommitSha      string    `db:"commit_sha"`
34+	CommitDate     time.Time `db:"commit_date"`
35+	Review         bool      `db:"review"`
36+	RawText        string    `db:"raw_text"`
37+	CreatedAt      time.Time `db:"created_at"`
38+}
39+
40+// Comment is a database model for a non-patch comment within a PatchRequest
41+type Comment struct {
42+	ID             int64     `db:"id"`
43+	Pubkey         string    `db:"pubkey"`
44+	PatchRequestID int64     `db:"patch_request_id"`
45+	Text           string    `db:"text"`
46+	CreatedAt      time.Time `db:"created_at"`
47+	UpdatedAt      time.Time `db:"updated_at"`
48+}
49+
50+type GitDB interface {
51+}
52+
53 // DB is the interface for a pico/git database.
54 type DB struct {
55 	*sqlx.DB
M mdw.go
+167, -120
  1@@ -7,7 +7,9 @@ import (
  2 	"io"
  3 	"os"
  4 	"path/filepath"
  5+	"regexp"
  6 	"strconv"
  7+	"strings"
  8 	"time"
  9 
 10 	"github.com/bluekeyes/go-gitdiff/gitdiff"
 11@@ -63,7 +65,136 @@ func flagSet(sesh ssh.Session, cmdName string) *flag.FlagSet {
 12 	return cmd
 13 }
 14 
 15-func GitServerMiddleware(be *Backend) wish.Middleware {
 16+type PrCmd struct {
 17+	Session ssh.Session
 18+	Backend *Backend
 19+	Repo    string
 20+	Pubkey  string
 21+}
 22+
 23+func (pr *PrCmd) PrintPatches(prID int64) {
 24+	patches := []*Patch{}
 25+	pr.Backend.DB.Select(
 26+		&patches,
 27+		"SELECT * FROM patches WHERE patch_request_id=?",
 28+		prID,
 29+	)
 30+	if len(patches) == 0 {
 31+		wish.Printf(pr.Session, "no patches found for Patch Request ID: %d\n", prID)
 32+		return
 33+	}
 34+
 35+	if len(patches) == 1 {
 36+		wish.Println(pr.Session, patches[0].RawText)
 37+		return
 38+	}
 39+
 40+	for _, patch := range patches {
 41+		wish.Printf(pr.Session, "%s\n\n\n", patch.RawText)
 42+	}
 43+}
 44+
 45+func (cmd *PrCmd) SubmitPatch(prID int64) {
 46+	pr := PatchRequest{}
 47+	cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
 48+	if pr.ID == 0 {
 49+		wish.Fatalln(cmd.Session, "patch request does not exist")
 50+		return
 51+	}
 52+
 53+	review := false
 54+	if pr.Pubkey != cmd.Pubkey {
 55+		review = true
 56+	}
 57+
 58+	// need to read io.Reader from session twice
 59+	var buf bytes.Buffer
 60+	tee := io.TeeReader(cmd.Session, &buf)
 61+
 62+	_, preamble, err := gitdiff.Parse(tee)
 63+	try(cmd.Session, err)
 64+	header, err := gitdiff.ParsePatchHeader(preamble)
 65+	try(cmd.Session, err)
 66+
 67+	_, err = cmd.Backend.DB.Exec(
 68+		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, title, body, commit_sha, commit_date, review, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 69+		cmd.Pubkey,
 70+		prID,
 71+		header.Author.Name,
 72+		header.Author.Email,
 73+		header.Title,
 74+		header.Body,
 75+		header.SHA,
 76+		header.CommitterDate,
 77+		review,
 78+		buf.String(),
 79+	)
 80+	try(cmd.Session, err)
 81+
 82+	wish.Printf(cmd.Session, "submitted review!\n")
 83+}
 84+
 85+func (cmd *PrCmd) SubmitPatchRequest(repoName string) {
 86+	err := git.EnsureWithin(cmd.Backend.ReposDir(), cmd.Backend.RepoName(repoName))
 87+	try(cmd.Session, err)
 88+	_, err = os.Stat(filepath.Join(cmd.Backend.ReposDir(), cmd.Backend.RepoName(repoName)))
 89+	if os.IsNotExist(err) {
 90+		wish.Fatalln(cmd.Session, "repo does not exist")
 91+		return
 92+	}
 93+
 94+	// need to read io.Reader from session twice
 95+	var buf bytes.Buffer
 96+	tee := io.TeeReader(cmd.Session, &buf)
 97+
 98+	_, preamble, err := gitdiff.Parse(tee)
 99+	try(cmd.Session, err)
100+	header, err := gitdiff.ParsePatchHeader(preamble)
101+	try(cmd.Session, err)
102+	prName := header.Title
103+	prDesc := header.Body
104+
105+	var prID int64
106+	row := cmd.Backend.DB.QueryRow(
107+		"INSERT INTO patch_requests (pubkey, repo_id, name, text, updated_at) VALUES(?, ?, ?, ?, ?) RETURNING id",
108+		cmd.Pubkey,
109+		repoName,
110+		prName,
111+		prDesc,
112+		time.Now(),
113+	)
114+	row.Scan(&prID)
115+	if prID == 0 {
116+		wish.Fatal(cmd.Session, "could not create patch request")
117+		return
118+	}
119+	try(cmd.Session, err)
120+
121+	_, err = cmd.Backend.DB.Exec(
122+		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, title, body, commit_sha, commit_date, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
123+		cmd.Pubkey,
124+		prID,
125+		header.Author.Name,
126+		header.Author.Email,
127+		header.Title,
128+		header.Body,
129+		header.SHA,
130+		header.CommitterDate,
131+		buf.String(),
132+	)
133+	try(cmd.Session, err)
134+
135+	wish.Printf(
136+		cmd.Session,
137+		"created patch request!\nID: %d\nTitle: %s\n",
138+		prID,
139+		prName,
140+	)
141+}
142+
143+func GitPatchRequestMiddleware(be *Backend) wish.Middleware {
144+	isNumRe := regexp.MustCompile(`^\d+$`)
145+
146 	return func(next ssh.Handler) ssh.Handler {
147 		return func(sesh ssh.Session) {
148 			pubkey := be.Pubkey(sesh.PublicKey())
149@@ -88,131 +219,47 @@ func GitServerMiddleware(be *Backend) wish.Middleware {
150 					}
151 				}
152 			} else if cmd == "pr" {
153-				prCmd := flagSet(sesh, "pr")
154-				repo := prCmd.String("repo", "", "repository to target")
155-				out := prCmd.Bool("stdout", false, "print patchset to stdout")
156-				id := prCmd.Int("id", 0, "patch request id")
157+				// PATCH REQUEST STATUS:
158+				// APPROVED
159+				// CLOSED
160+				// REVIEWED
161 
162-				repoName := utils.SanitizeRepo(*repo)
163+				// ssh git.sh ls
164+				// git format-patch --stdout | ssh git.sh pr test
165+				// git format-patch --stdout | ssh git.sh pr 123
166+				// ssh git.sh pr ls
167+				// ssh git.sh pr 123 --approve
168+				// ssh git.sh pr 123 --close
169+				// ssh git.sh pr 123 --stdout | git am -3
170+				// echo "here is a comment" | ssh git.sh pr 123 --comment
171 
172-				if *out == true {
173-					prID, err := strconv.ParseInt(args[2], 10, 64)
174-					try(sesh, err)
175-
176-					patches := []*Patch{}
177-					be.DB.Select(
178-						&patches,
179-						"SELECT * FROM patches WHERE patch_request_id=?",
180-						prID,
181-					)
182-					if len(patches) == 0 {
183-						wish.Printf(sesh, "no patches found for Patch Request ID: %d\n", prID)
184-						return
185-					}
186-
187-					if len(patches) == 1 {
188-						wish.Println(sesh, patches[0].RawText)
189-						return
190-					}
191-
192-					for _, patch := range patches {
193-						wish.Printf(sesh, "%s\n\n\n", patch.RawText)
194-					}
195-				} else if *id != 0 {
196-					prID := *id
197-					pr := PatchRequest{}
198-					be.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
199-					if pr.ID == 0 {
200-						wish.Fatalln(sesh, "patch request does not exist")
201-						return
202-					}
203-
204-					review := false
205-					if pr.Pubkey != pubkey {
206-						review = true
207-					}
208-
209-					// need to read io.Reader from session twice
210-					var buf bytes.Buffer
211-					tee := io.TeeReader(sesh, &buf)
212-
213-					_, preamble, err := gitdiff.Parse(tee)
214-					try(sesh, err)
215-					header, err := gitdiff.ParsePatchHeader(preamble)
216-					try(sesh, err)
217-
218-					_, err = be.DB.Exec(
219-						"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, title, body, commit_sha, commit_date, review, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
220-						pubkey,
221-						prID,
222-						header.Author.Name,
223-						header.Author.Email,
224-						header.Title,
225-						header.Body,
226-						header.SHA,
227-						header.CommitterDate,
228-						review,
229-						buf.String(),
230-					)
231+				prCmd := flagSet(sesh, "pr")
232+				subCmd := strings.TrimSpace(args[2])
233+				repoName := ""
234+				var prID int64
235+				var err error
236+				if isNumRe.MatchString(subCmd) {
237+					prID, err = strconv.ParseInt(args[2], 10, 64)
238 					try(sesh, err)
239-
240-					wish.Printf(sesh, "submitted review!\n")
241 				} else {
242-					err := git.EnsureWithin(be.ReposDir(), be.RepoName(repoName))
243-					try(sesh, err)
244-					_, err = os.Stat(filepath.Join(be.ReposDir(), be.RepoName(repoName)))
245-					if os.IsNotExist(err) {
246-						wish.Fatalln(sesh, "repo does not exist")
247-						return
248-					}
249-
250-					// need to read io.Reader from session twice
251-					var buf bytes.Buffer
252-					tee := io.TeeReader(sesh, &buf)
253-
254-					_, preamble, err := gitdiff.Parse(tee)
255-					try(sesh, err)
256-					header, err := gitdiff.ParsePatchHeader(preamble)
257-					try(sesh, err)
258-					prName := header.Title
259-					prDesc := header.Body
260-
261-					var prID int64
262-					row := be.DB.QueryRow(
263-						"INSERT INTO patch_requests (pubkey, repo_id, name, text, updated_at) VALUES(?, ?, ?, ?, ?) RETURNING id",
264-						pubkey,
265-						repoName,
266-						prName,
267-						prDesc,
268-						time.Now(),
269-					)
270-					row.Scan(&prID)
271-					if prID == 0 {
272-						wish.Fatal(sesh, "could not create patch request")
273-						return
274-					}
275-					try(sesh, err)
276+					repoName = utils.SanitizeRepo(subCmd)
277+				}
278+				out := prCmd.Bool("stdout", false, "print patchset to stdout")
279 
280-					_, err = be.DB.Exec(
281-						"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, title, body, commit_sha, commit_date, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
282-						pubkey,
283-						prID,
284-						header.Author.Name,
285-						header.Author.Email,
286-						header.Title,
287-						header.Body,
288-						header.SHA,
289-						header.CommitterDate,
290-						buf.String(),
291-					)
292-					try(sesh, err)
293+				pr := &PrCmd{
294+					Session: sesh,
295+					Backend: be,
296+					Pubkey:  pubkey,
297+				}
298 
299-					wish.Printf(
300-						sesh,
301-						"created patch request!\nID: %d\nTitle: %s\n",
302-						prID,
303-						prName,
304-					)
305+				if *out == true {
306+					pr.PrintPatches(prID)
307+				} else if prID != 0 {
308+					pr.SubmitPatch(prID)
309+				} else if subCmd == "ls" {
310+					wish.Println(sesh, "list all patch requests")
311+				} else if repoName != "" {
312+					pr.SubmitPatchRequest(repoName)
313 				}
314 
315 				return
D models.go
+0, -45
 1@@ -1,45 +0,0 @@
 2-package git
 3-
 4-import (
 5-	"time"
 6-)
 7-
 8-// PatchRequest is a database model for patches submitted to a Repo
 9-type PatchRequest struct {
10-	ID        int64     `db:"id"`
11-	Pubkey    string    `db:"pubkey"`
12-	RepoID    int64     `db:"repo_id"`
13-	Name      string    `db:"name"`
14-	Text      string    `db:"text"`
15-	CreatedAt time.Time `db:"created_at"`
16-	UpdatedAt time.Time `db:"updated_at"`
17-}
18-
19-// Patch is a database model for a single entry in a patchset
20-// This usually corresponds to a git commit.
21-type Patch struct {
22-	ID             int64     `db:"id"`
23-	Pubkey         string    `db:"pubkey"`
24-	PatchRequestID int64     `db:"patch_request_id"`
25-	AuthorName     string    `db:"author_name"`
26-	AuthorEmail    string    `db:"author_email"`
27-	Title          string    `db:"title"`
28-	Body           string    `db:"body"`
29-	CommitSha      string    `db:"commit_sha"`
30-	CommitDate     time.Time `db:"commit_date"`
31-	RawText        string    `db:"raw_text"`
32-	CreatedAt      time.Time `db:"created_at"`
33-}
34-
35-// Comment is a database model for a non-patch comment within a PatchRequest
36-type Comment struct {
37-	ID             int64     `db:"id"`
38-	UserID         int64     `db:"user_id"`
39-	PatchRequestID int64     `db:"patch_request_id"`
40-	Text           string    `db:"text"`
41-	CreatedAt      time.Time `db:"created_at"`
42-	UpdatedAt      time.Time `db:"updated_at"`
43-}
44-
45-type GitDB interface {
46-}
D patch_handler.go
+0, -88
 1@@ -1,88 +0,0 @@
 2-package git
 3-
 4-import (
 5-	"fmt"
 6-	"io"
 7-	"log/slog"
 8-	"os"
 9-	"path/filepath"
10-
11-	"github.com/charmbracelet/ssh"
12-	"github.com/picosh/send/send/utils"
13-)
14-
15-type UploadHandler struct {
16-	Cfg    *GitCfg
17-	Logger *slog.Logger
18-}
19-
20-func NewUploadHandler(cfg *GitCfg, logger *slog.Logger) *UploadHandler {
21-	return &UploadHandler{
22-		Cfg:    cfg,
23-		Logger: logger,
24-	}
25-}
26-
27-func (h *UploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
28-	fmt.Println("read")
29-	cleanFilename := filepath.Base(entry.Filepath)
30-
31-	if cleanFilename == "" || cleanFilename == "." {
32-		return nil, nil, os.ErrNotExist
33-	}
34-
35-	return nil, nil, os.ErrNotExist
36-}
37-
38-func (h *UploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
39-	fmt.Println("list")
40-	var fileList []os.FileInfo
41-	cleanFilename := filepath.Base(fpath)
42-
43-	if cleanFilename == "" || cleanFilename == "." || cleanFilename == "/" {
44-		name := cleanFilename
45-		if name == "" {
46-			name = "/"
47-		}
48-
49-		fileList = append(fileList, &utils.VirtualFile{
50-			FName:  name,
51-			FIsDir: true,
52-		})
53-	} else {
54-	}
55-
56-	return fileList, nil
57-}
58-
59-func (h *UploadHandler) GetLogger() *slog.Logger {
60-	return h.Logger
61-}
62-
63-func (h *UploadHandler) Validate(s ssh.Session) error {
64-	fmt.Println("validate")
65-	return nil
66-}
67-
68-func (h *UploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
69-	fmt.Println("write")
70-	logger := h.GetLogger()
71-	user := s.User()
72-
73-	filename := filepath.Base(entry.Filepath)
74-	logger = logger.With(
75-		"user", user,
76-		"filepath", entry.Filepath,
77-		"size", entry.Size,
78-		"filename", filename,
79-	)
80-
81-	var text []byte
82-	if b, err := io.ReadAll(entry.Reader); err == nil {
83-		text = b
84-	}
85-
86-	fmt.Println(string(text))
87-
88-	return "", nil
89-}
M ssh.go
+1, -8
 1@@ -12,9 +12,6 @@ import (
 2 
 3 	"github.com/charmbracelet/ssh"
 4 	"github.com/charmbracelet/wish"
 5-	wishrsync "github.com/picosh/send/send/rsync"
 6-	"github.com/picosh/send/send/scp"
 7-	"github.com/picosh/send/send/sftp"
 8 )
 9 
10 func authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
11@@ -33,7 +30,6 @@ func GitSshServer() {
12 
13 	cfg := NewGitCfg()
14 	logger := slog.Default()
15-	handler := NewUploadHandler(cfg, logger)
16 	dbh, err := Open(":memory:", logger)
17 	if err != nil {
18 		panic(err)
19@@ -53,11 +49,8 @@ func GitSshServer() {
20 			filepath.Join(cfg.DataPath, "term_info_ed25519"),
21 		),
22 		wish.WithPublicKeyAuth(authHandler),
23-		sftp.SSHOption(handler),
24 		wish.WithMiddleware(
25-			scp.Middleware(handler),
26-			wishrsync.Middleware(handler),
27-			GitServerMiddleware(be),
28+			GitPatchRequestMiddleware(be),
29 		),
30 	)
31