repos / git-pr

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

commit
457c062
parent
967d908
author
Eric Bower
date
2024-06-24 12:55:02 -0400 EDT
docs: copy
7 files changed,  +155, -73
M pr.go
M web.go
M pr.go
+42, -0
 1@@ -25,6 +25,7 @@ const (
 2 
 3 type GitPatchRequest interface {
 4 	GetRepos() ([]Repo, error)
 5+	GetReposWithLatestPr() ([]RepoWithLatestPr, error)
 6 	GetRepoByID(repoID string) (*Repo, error)
 7 	SubmitPatchRequest(repoID string, pubkey string, patchset io.Reader) (*PatchRequest, error)
 8 	SubmitPatchSet(prID int64, pubkey string, op PatchsetOp, patchset io.Reader) ([]*Patch, error)
 9@@ -48,10 +49,51 @@ type PrCmd struct {
10 var _ GitPatchRequest = PrCmd{}
11 var _ GitPatchRequest = (*PrCmd)(nil)
12 
13+type PrWithRepo struct {
14+	LastUpdatedPrID int64
15+	RepoID          string
16+}
17+
18+type RepoWithLatestPr struct {
19+	*Repo
20+	PatchRequest *PatchRequest
21+}
22+
23 func (pr PrCmd) GetRepos() ([]Repo, error) {
24 	return pr.Backend.Cfg.Repos, nil
25 }
26 
27+func (pr PrCmd) GetReposWithLatestPr() ([]RepoWithLatestPr, error) {
28+	repos := []RepoWithLatestPr{}
29+	prs := []PatchRequest{}
30+	err := pr.Backend.DB.Select(&prs, "SELECT * FROM patch_requests GROUP BY repo_id ORDER BY updated_at DESC")
31+	if err != nil {
32+		return repos, err
33+	}
34+
35+	// we want recently modified repos to be on top
36+	for _, prq := range prs {
37+		for _, repo := range pr.Backend.Cfg.Repos {
38+			if prq.RepoID == repo.ID {
39+				repos = append(repos, RepoWithLatestPr{
40+					Repo:         &repo,
41+					PatchRequest: &prq,
42+				})
43+			}
44+		}
45+	}
46+
47+	for _, repo := range pr.Backend.Cfg.Repos {
48+		for _, curRepo := range repos {
49+			if curRepo.ID == repo.ID {
50+				continue
51+			}
52+		}
53+	}
54+
55+	return repos, nil
56+}
57+
58 func (pr PrCmd) GetRepoByID(repoID string) (*Repo, error) {
59 	repos, err := pr.GetRepos()
60 	if err != nil {
M tmpl/pr-detail.html
+21, -25
 1@@ -14,16 +14,28 @@
 2 <main class="group">
 3   <div class="group">
 4     {{range $idx, $val := .Patches}}
 5-    <div class="box">
 6-      <h2 class="text-lg m-0 p-0 mb">
 7-        <code>#{{$idx}}</code>
 8-        {{if $val.Review}}<code>REVIEW</code>{{end}}
 9-        <a href="#{{$val.Url}}">{{$val.Title}}</a>
10-      </h2>
11-      <div class="group-h text-sm">
12-        <code>{{$val.AuthorName}} &lt;{{$val.AuthorEmail}}&gt;</code>
13-        <date>{{$val.AuthorDate}}</date>
14+    <div class="box group" id="{{$val.Url}}">
15+      <div>
16+        <h2 class="text-lg m-0 p-0 mb">
17+          <code>#{{$idx}}</code>
18+          {{if $val.Review}}<code>REVIEW</code>{{end}}
19+          <a href="#{{$val.Url}}">{{$val.Title}}</a>
20+        </h2>
21+
22+        <div class="group-h text-sm">
23+          <code>{{$val.AuthorName}} &lt;{{$val.AuthorEmail}}&gt;</code>
24+          <date>{{$val.AuthorDate}}</date>
25+        </div>
26       </div>
27+
28+      {{if $val.Body}}<pre class="m-0">{{$val.Body}}</pre>{{end}}
29+
30+      <pre class="m-0">{{$val.BodyAppendix}}</pre>
31+
32+      <details>
33+        <summary>Patch</summary>
34+        <div>{{$val.DiffStr}}</div>
35+      </details>
36     </div>
37     {{else}}
38     <div class="box">
39@@ -31,22 +43,6 @@
40     </div>
41     {{end}}
42   </div>
43-
44-  <hr class="w-full" />
45-
46-  <div class="group">
47-    {{range .Patches}}
48-    <div class="box" id="{{.Url}}">
49-      <div class="group-h">
50-        <h2 class="text-lg m-0 p-0">
51-          {{if .Review}}<code>REVIEW</code>{{end}}
52-          <a href="#{{.Url}}">{{.Title}}</a>
53-        </h2>
54-      </div>
55-      <div>{{.DiffStr}}</div>
56-    </div>
57-    {{end}}
58-  </div>
59 </main>
60 
61 <hr />
M tmpl/pr-header.html
+12, -1
 1@@ -11,7 +11,18 @@
 2     <code>{{.Pr.Pubkey}}</code>
 3   </div>
 4 
 5-    <pre># add changes to patch request
 6+  <details>
 7+    <summary>Help</summary>
 8+    <div class="group">
 9+      <pre class="m-0"># add changes to patch request
10 git format-patch main --stdout | ssh pr.pico.sh pr add {{.Pr.ID}}</pre>
11+      <pre class="m-0"># checkout all patches
12+ssh pr.pico.sh pr print {{.Pr.ID}} | git am -3</pre>
13+      <pre class="m-0"># checkout specific patch
14+ssh pr.pico.sh pr print {{.Pr.ID}} --filter n | git am -3</pre>
15+      <pre class="m-0"># checkout patch range
16+ssh pr.pico.sh pr print {{.Pr.ID}} --filter n:y | git am -3</pre>
17+    </div>
18+  </details>
19 </header>
20 {{end}}
A tmpl/pr-list-item.html
+10, -0
 1@@ -0,0 +1,10 @@
 2+{{define "pr-list-item"}}
 3+<div>
 4+  <div><a href="{{.Url}}">{{.Text}}</a> <code>{{.Status}}</code></div>
 5+  <div>
 6+    <code>#{{.ID}}</code>
 7+    <span>opened on <date>{{.Date}}</date> by </span>
 8+    <code>{{.Pubkey}}</code>
 9+  </div>
10+</div>
11+{{end}}
M tmpl/repo-detail.html
+19, -39
 1@@ -12,57 +12,37 @@
 2 <header>
 3   <h1 class="text-2xl mb"><a href="/">repos</a> / {{.ID}}</h1>
 4   <div class="group">
 5-    <div><code>git clone {{.CloneAddr}}</code></div>
 6-    <pre># submit a new patch request
 7+    <div>
 8+      <code>git clone {{.CloneAddr}}</code>
 9+    </div>
10+
11+    <details>
12+      <summary>Help</summary>
13+      <pre class="m-0"># submit a new patch request
14 git format-patch main --stdout | ssh pr.pico.sh pr create {{.ID}}</pre>
15+    </details>
16 	</div>
17 </header>
18 <main class="group">
19   <h3 class="text-lg">Open PRs</h3>
20-
21   {{range .OpenPrs}}
22-  <article class="box">
23-    <div><a href="{{.Url}}">{{.Text}}</a> <code>{{.Status}}</code></div>
24-    <div class="text-sm">
25-      <code>#{{.ID}}</code>
26-      <span>opened on <date>{{.Date}}</date> by </span>
27-      <code>{{.Pubkey}}</code>
28-    </div>
29-  </article>
30+    <div class="box">{{template "pr-list-item" .}}</div>
31   {{end}}
32 
33   {{if .AcceptedPrs}}
34-  <hr class="my w-full" />
35-
36-  <h3 class="text-lg">Accepted PRs</h3>
37-
38-  {{range .AcceptedPrs}}
39-  <article class="box">
40-    <div><a href="{{.Url}}">{{.Text}}</a> <code>{{.Status}}</code></div>
41-    <div class="text-sm">
42-      <code>#{{.ID}}</code>
43-      <span>opened on <date>{{.Date}}</date> by </span>
44-      <code>{{.Pubkey}}</code>
45-    </div>
46-  </article>
47-  {{end}}
48+    <hr class="my w-full" />
49+    <h3 class="text-lg">Accepted PRs</h3>
50+    {{range .AcceptedPrs}}
51+      <div class="box">{{template "pr-list-item" .}}</div>
52+    {{end}}
53   {{end}}
54 
55   {{if .ClosedPrs}}
56-  <hr class="my w-full" />
57-
58-  <h3 class="text-lg">Closed PRs</h3>
59-
60-  {{range .ClosedPrs}}
61-  <article class="box">
62-    <div><a href="{{.Url}}">{{.Text}}</a> <code>{{.Status}}</code></div>
63-    <div class="text-sm">
64-      <code>#{{.ID}}</code>
65-      <span>opened on <date>{{.Date}}</date> by </span>
66-      <code>{{.Pubkey}}</code>
67-    </div>
68-  </article>
69-  {{end}}
70+    <hr class="my w-full" />
71+    <h3 class="text-lg">Closed PRs</h3>
72+    {{range .ClosedPrs}}
73+      <div class="box">{{template "pr-list-item" .}}</div>
74+    {{end}}
75   {{end}}
76 </main>
77 
M tmpl/repo-list.html
+31, -5
 1@@ -9,18 +9,44 @@
 2 {{end}}
 3 
 4 {{define "body"}}
 5-<header>
 6-  <h1 class="text-2xl mb">Repos</h1>
 7-  <p>A new git collaboration service.</p>
 8+<header class="group">
 9+  <h1 class="text-2xl">Repos</h1>
10+  <div>A new git collaboration service.</div>
11+  <pre class="m-0">ssh pr.pico.sh help</pre>
12+  <details>
13+    <summary>How do Patch Requests work?</summary>
14+      <div>
15+        Patch requests (PR) are the simplest way to submit, review, and accept changes to your git repository.
16+        Here's how it works:
17+      </div>
18+
19+      <ol>
20+        <li>External contributor clones repo (<code>git-clone</code>)</li>
21+        <li>External contributor makes a code change (<code>git-add</code> & <code>git-commit</code>)</li>
22+        <li>External contributor generates patches (<code>git-format-patch</code>)</li>
23+        <li>External contributor submits a PR to SSH server</li>
24+        <li>Owner receives RSS notification that there's a new PR</li>
25+        <li>Owner applies patches locally (<code>git-am</code>) from SSH server</li>
26+        <li>Owner makes suggestions in code! (<code>git-add</code> & <code>git-commit</code>)</li>
27+        <li>Owner submits review by piping patch to SSH server (<code>git-format-patch</code>)</li>
28+        <li>External contributor receives RSS notification of the PR review</li>
29+        <li>External contributor re-applies patches (<code>git-am</code>)</li>
30+        <li>External contributor reviews and removes comments in code!</li>
31+        <li>External contributor submits another patch (<code>git-format-patch</code>)</li>
32+        <li>Owner applies patches locally (<code>git-am</code>)</li>
33+        <li>Owner marks PR as accepted and pushes code to main (<code>git-push</code>)</li>
34+      </ol>
35+  </details>
36 </header>
37 <main>
38   <div class="group">
39-    <pre>ssh pr.pico.sh help</pre>
40-
41     {{range .Repos}}
42     <div class="box">
43       <h3 class="text-lg m-0 p-0"><a href="{{.Url}}">{{.Text}}</a></h3>
44       <div class="text-sm">{{.Desc}}</div>
45+      {{if .LatestPr}}
46+        <div class="text-xs mt box-sm">{{template "pr-list-item" .LatestPr}}</div>
47+      {{end}}
48     </div>
49     {{end}}
50   </div>
M web.go
+20, -3
 1@@ -71,6 +71,7 @@ func getTemplate(file string) *template.Template {
 2 			tmplFS,
 3 			filepath.Join("tmpl", file),
 4 			filepath.Join("tmpl", "pr-header.html"),
 5+			filepath.Join("tmpl", "pr-list-item.html"),
 6 			filepath.Join("tmpl", "base.html"),
 7 		),
 8 	)
 9@@ -84,7 +85,8 @@ type LinkData struct {
10 
11 type RepoData struct {
12 	LinkData
13-	Desc string
14+	Desc     string
15+	LatestPr PrListData
16 }
17 
18 type RepoListData struct {
19@@ -98,7 +100,7 @@ func repoListHandler(w http.ResponseWriter, r *http.Request) {
20 		return
21 	}
22 
23-	repos, err := web.Pr.GetRepos()
24+	repos, err := web.Pr.GetReposWithLatestPr()
25 	if err != nil {
26 		web.Pr.Backend.Logger.Error("cannot get repos", "err", err)
27 		w.WriteHeader(http.StatusInternalServerError)
28@@ -107,12 +109,28 @@ func repoListHandler(w http.ResponseWriter, r *http.Request) {
29 
30 	repoData := []RepoData{}
31 	for _, repo := range repos {
32+		var ls PrListData
33+		if repo.PatchRequest != nil {
34+			curpr := repo.PatchRequest
35+			ls = PrListData{
36+				ID:     curpr.ID,
37+				Pubkey: curpr.Pubkey,
38+				LinkData: LinkData{
39+					Url:  template.URL(fmt.Sprintf("/prs/%d", curpr.ID)),
40+					Text: curpr.Name,
41+				},
42+				Date:   curpr.CreatedAt.Format(time.RFC3339),
43+				Status: curpr.Status,
44+			}
45+		}
46+
47 		d := RepoData{
48 			Desc: repo.Desc,
49 			LinkData: LinkData{
50 				Url:  template.URL("/repos/" + repo.ID),
51 				Text: repo.ID,
52 			},
53+			LatestPr: ls,
54 		}
55 		repoData = append(repoData, d)
56 	}
57@@ -433,7 +451,6 @@ func StartWebServer() {
58 	}
59 	formatter := formatterHtml.New(
60 		formatterHtml.WithLineNumbers(true),
61-		formatterHtml.WithLinkableLineNumbers(true, ""),
62 		formatterHtml.WithClasses(true),
63 	)
64 	web := &WebCtx{