repos / git-pr

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

commit
d60f2ca
parent
04302e8
author
Eric Bower
date
2024-05-02 00:07:40 -0400 EDT
mostly working
6 files changed,  +124, -77
M db.go
M go.mod
M go.sum
M mdw.go
M ssh.go
A backend.go
+29, -0
 1@@ -0,0 +1,29 @@
 2+package git
 3+
 4+import (
 5+	"log/slog"
 6+	"path/filepath"
 7+
 8+	"github.com/charmbracelet/ssh"
 9+	gossh "golang.org/x/crypto/ssh"
10+	// ssgit "github.com/charmbracelet/soft-serve/git"
11+	// "github.com/charmbracelet/soft-serve/pkg/utils"
12+)
13+
14+type Backend struct {
15+	Logger *slog.Logger
16+	DB     *DB
17+	Cfg    *GitCfg
18+}
19+
20+func (be *Backend) ReposDir() string {
21+	return filepath.Join(be.Cfg.DataPath, "repos")
22+}
23+
24+func (be *Backend) RepoName(name string) string {
25+	return name + ".git"
26+}
27+
28+func (be *Backend) Pubkey(pk ssh.PublicKey) string {
29+	return gossh.FingerprintSHA256(pk)
30+}
M db.go
+13, -44
 1@@ -14,55 +14,28 @@ type DB struct {
 2 }
 3 
 4 var schema = `
 5-CREATE TABLE IF NOT EXISTS users (
 6-  id INTEGER PRIMARY KEY AUTOINCREMENT,
 7-  name TEXT NOT NULL UNIQUE,
 8-  admin BOOLEAN NOT NULL,
 9-  public_key TEXT NOT NULL UNIQUE,
10-  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
11-  updated_at DATETIME NOT NULL
12-);
13-
14-CREATE TABLE IF NOT EXISTS repos (
15-  id INTEGER PRIMARY KEY AUTOINCREMENT,
16-  name TEXT NOT NULL UNIQUE,
17-  description TEXT NOT NULL UNIQUE,
18-  private BOOLEAN NOT NULL,
19-  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
20-  updated_at DATETIME NOT NULL
21-);
22-
23 CREATE TABLE IF NOT EXISTS patch_requests (
24   id INTEGER PRIMARY KEY AUTOINCREMENT,
25-  user_id INTEGER NOT NULL,
26-  repo_id INTEGER NOT NULL,
27+  pubkey TEXT NOT NULL,
28+  repo_id TEXT NOT NULL,
29   name TEXT NOT NULL,
30+  text TEXT NOT NULL,
31   created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
32-  updated_at DATETIME NOT NULL,
33-  CONSTRAINT user_id_fk
34-  FOREIGN KEY(user_id) REFERENCES users(id)
35-  ON DELETE CASCADE
36-  ON UPDATE CASCADE,
37-  CONSTRAINT repo_id_fk
38-  FOREIGN KEY(repo_id) REFERENCES repos(id)
39-  ON DELETE CASCADE
40-  ON UPDATE CASCADE
41+  updated_at DATETIME NOT NULL
42 );
43 
44 CREATE TABLE IF NOT EXISTS patches (
45   id INTEGER PRIMARY KEY AUTOINCREMENT,
46-  user_id INTEGER NOT NULL,
47+  pubkey TEXT NOT NULL,
48   patch_request_id INTEGER NOT NULL,
49-  from_name TEXT NOT NULL,
50-  from_email TEXT NOT NULL,
51-  subject TEXT NOT NULL,
52-  text TEXT NOT NULL,
53-  date DATETIME NOT NULL,
54+  author_name TEXT NOT NULL,
55+  author_email TEXT NOT NULL,
56+  title TEXT NOT NULL,
57+  body TEXT NOT NULL,
58+  commit_sha TEXT NOT NULL,
59+  commit_date DATETIME NOT NULL,
60+  raw_text TEXT NOT NULL,
61   created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
62-  CONSTRAINT user_id_fk
63-  FOREIGN KEY(user_id) REFERENCES users(id)
64-  ON DELETE CASCADE
65-  ON UPDATE CASCADE,
66   CONSTRAINT pr_id_fk
67   FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
68   ON DELETE CASCADE
69@@ -71,15 +44,11 @@ CREATE TABLE IF NOT EXISTS patches (
70 
71 CREATE TABLE IF NOT EXISTS comments (
72   id INTEGER PRIMARY KEY AUTOINCREMENT,
73-  user_id INTEGER NOT NULL,
74+  pubkey TEXT NOT NULL,
75   patch_request_id INTEGER NOT NULL,
76   text TEXT NOT NULL,
77   created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
78   updated_at DATETIME NOT NULL,
79-  CONSTRAINT user_id_fk
80-  FOREIGN KEY(user_id) REFERENCES users(id)
81-  ON DELETE CASCADE
82-  ON UPDATE CASCADE,
83   CONSTRAINT pr_id_fk
84   FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
85   ON DELETE CASCADE
M go.mod
+1, -0
1@@ -15,6 +15,7 @@ require (
2 	github.com/antoniomika/go-rsync-receiver v0.0.0-20231110145728-c94949e1ab7d // indirect
3 	github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2 // indirect
4 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
5+	github.com/bluekeyes/go-gitdiff v0.7.2 // indirect
6 	github.com/caarlos0/env/v10 v10.0.0 // indirect
7 	github.com/charmbracelet/bubbletea v0.25.0 // indirect
8 	github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20231027181609-f7ff6baf2ed0 // indirect
M go.sum
+2, -0
1@@ -8,6 +8,8 @@ github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2 h1:3w5KT
2 github.com/aymanbagabas/git-module v1.8.4-0.20231101154130-8d27204ac6d2/go.mod h1:d4gQ7/3/S2sPq4NnKdtAgUOVr6XtLpWFtxyVV5/+76U=
3 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5+github.com/bluekeyes/go-gitdiff v0.7.2 h1:42jrcVZdjjxXtVsFNYTo/I6T1ZvIiQL+iDDLiH904hw=
6+github.com/bluekeyes/go-gitdiff v0.7.2/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
7 github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
8 github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
9 github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
M mdw.go
+72, -31
  1@@ -1,27 +1,30 @@
  2 package git
  3 
  4 import (
  5+	"bytes"
  6 	"fmt"
  7+	"io"
  8 	"path/filepath"
  9+	"time"
 10 
 11-	ssgit "github.com/charmbracelet/soft-serve/git"
 12+	"github.com/bluekeyes/go-gitdiff/gitdiff"
 13 	"github.com/charmbracelet/soft-serve/pkg/git"
 14 	"github.com/charmbracelet/soft-serve/pkg/utils"
 15 	"github.com/charmbracelet/ssh"
 16 	"github.com/charmbracelet/wish"
 17 )
 18 
 19-func gitServiceCommands(sesh ssh.Session, cfg *GitCfg, cmd, repo string) error {
 20+func gitServiceCommands(sesh ssh.Session, be *Backend, cmd, repo string) error {
 21 	name := utils.SanitizeRepo(repo)
 22 	// git bare repositories should end in ".git"
 23 	// https://git-scm.com/docs/gitrepository-layout
 24-	repoDir := name + ".git"
 25-	reposDir := filepath.Join(cfg.DataPath, "repos")
 26-	err := git.EnsureWithin(reposDir, repoDir)
 27+	repoName := name + ".git"
 28+	reposDir := be.ReposDir()
 29+	err := git.EnsureWithin(reposDir, repoName)
 30 	if err != nil {
 31 		return err
 32 	}
 33-	repoPath := filepath.Join(reposDir, repoDir)
 34+	repoPath := filepath.Join(reposDir, repoName)
 35 	serviceCmd := git.ServiceCommand{
 36 		Stdin:  sesh,
 37 		Stdout: sesh,
 38@@ -45,45 +48,83 @@ func gitServiceCommands(sesh ssh.Session, cfg *GitCfg, cmd, repo string) error {
 39 	return nil
 40 }
 41 
 42-func createRepo(cfg *GitCfg, rawName string) (*Repo, error) {
 43-	name := utils.SanitizeRepo(rawName)
 44-	if err := utils.ValidateRepo(name); err != nil {
 45-		return nil, err
 46-	}
 47-	reposDir := filepath.Join(cfg.DataPath, "repos")
 48-
 49-	repo := name + ".git"
 50-	rp := filepath.Join(reposDir, repo)
 51-	_, err := ssgit.Init(rp, true)
 52+func try(sesh ssh.Session, err error) {
 53 	if err != nil {
 54-		return nil, err
 55+		wish.Fatal(sesh, err)
 56 	}
 57 }
 58 
 59-func GitServerMiddleware(cfg *GitCfg, dbh *DB) wish.Middleware {
 60+func GitServerMiddleware(be *Backend) wish.Middleware {
 61 	return func(next ssh.Handler) ssh.Handler {
 62 		return func(sesh ssh.Session) {
 63+			pubkey := be.Pubkey(sesh.PublicKey())
 64 			args := sesh.Command()
 65 			cmd := args[0]
 66-			fmt.Println(cmd)
 67 
 68 			if cmd == "git-receive-pack" || cmd == "git-upload-pack" {
 69 				repoName := args[1]
 70-				err := gitServiceCommands(sesh, cfg, cmd, repoName)
 71-				if err != nil {
 72-					wish.Fatal(sesh, err.Error())
 73-					return
 74-				}
 75+				err := gitServiceCommands(sesh, be, cmd, repoName)
 76+				try(sesh, err)
 77 			} else if cmd == "help" {
 78 				wish.Println(sesh, "commands: [help, git-receive-pack, git-upload-pack]")
 79 			} else if cmd == "pr" {
 80-				repoName := args[1]
 81-				fmt.Println(repoName)
 82-				// dbpool.GetRepoByName(repoName)
 83-				// pr, err := dbpool.InsertPatchRequest(userID, repoID, name)
 84-				// dbpool.InsertPatches(userID, pr.ID, patches)
 85-				// id := fmt.Sprintf("%s/%s", repoName, pr.ID)
 86-				// wish.Printf("Patch Request ID: %s", id)
 87+				if len(args) < 2 {
 88+					wish.Fatal(sesh, "must provide repo name")
 89+					return
 90+				}
 91+				repoName := utils.SanitizeRepo(args[1])
 92+				err := git.EnsureWithin(be.ReposDir(), be.RepoName(repoName))
 93+				try(sesh, err)
 94+
 95+				// need to read io.Reader from session twice
 96+				var buf bytes.Buffer
 97+				tee := io.TeeReader(sesh, &buf)
 98+
 99+				_, preamble, err := gitdiff.Parse(tee)
100+				try(sesh, err)
101+				header, err := gitdiff.ParsePatchHeader(preamble)
102+				try(sesh, err)
103+				prName := header.Title
104+				prDesc := header.Body
105+
106+				var prID int64
107+				row := be.DB.QueryRow(
108+					"INSERT INTO patch_requests (pubkey, repo_id, name, text, updated_at) VALUES(?, ?, ?, ?, ?) RETURNING id",
109+					pubkey,
110+					repoName,
111+					prName,
112+					prDesc,
113+					time.Now(),
114+				)
115+				row.Scan(&prID)
116+				if prID == 0 {
117+					wish.Fatal(sesh, "could not create patch request")
118+					return
119+				}
120+				try(sesh, err)
121+
122+				_, err = be.DB.Exec(
123+					"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, title, body, commit_sha, commit_date, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
124+					pubkey,
125+					prID,
126+					header.Author.Name,
127+					header.Author.Email,
128+					header.Title,
129+					header.Body,
130+					header.SHA,
131+					header.CommitterDate,
132+					buf.String(),
133+				)
134+				try(sesh, err)
135+
136+				wish.Printf(
137+					sesh,
138+					"Create Patch Request!\nID: %d\nTitle: %s\n",
139+					prID,
140+					prName,
141+				)
142+
143+				return
144 			} else {
145 				fmt.Println("made it here")
146 				next(sesh)
M ssh.go
+7, -2
 1@@ -34,11 +34,16 @@ func GitSshServer() {
 2 	cfg := NewGitCfg()
 3 	logger := slog.Default()
 4 	handler := NewUploadHandler(cfg, logger)
 5-	dbh, err := Open(":memory:", logger)
 6+	dbh, err := Open(":memory", logger)
 7 	if err != nil {
 8 		panic(err)
 9 	}
10 	dbh.Migrate()
11+	be := &Backend{
12+		DB:     dbh,
13+		Logger: logger,
14+		Cfg:    cfg,
15+	}
16 
17 	s, err := wish.NewServer(
18 		wish.WithAddress(
19@@ -52,7 +57,7 @@ func GitSshServer() {
20 		wish.WithMiddleware(
21 			scp.Middleware(handler),
22 			wishrsync.Middleware(handler),
23-			GitServerMiddleware(cfg, dbh),
24+			GitServerMiddleware(be),
25 		),
26 	)
27