repos / git-pr

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

commit
caaef17
parent
02823e3
author
Eric Bower
date
2025-08-28 09:51:02 -0400 EDT
feat: range-diff tool

This tool enables anyone to quick paste their patches and have us
perform a range-diff on them
4 files changed,  +141, -4
M web.go
M .gitignore
+1, -0
1@@ -15,3 +15,4 @@ __debug_bin
2 /public/
3 test.db
4 review.patch
5+.aider*
M tmpl/components/range-diff.html
+6, -4
 1@@ -58,10 +58,11 @@
 2                 {{range .Files}}
 3                   <div class="flex gap">
 4                     <div class="flex-1" style="width: 48%;">
 5+                      <h3 class="text-md">old</h3>
 6                       <div>
 7                         {{if .OldFile}}
 8-                          {{if .OldFile.OldName}}<code>{{.OldFile.OldName}}</code>{{end}}
 9-                          {{if .OldFile.NewName}}<code>{{.OldFile.NewName}}</code>{{end}}
10+                          {{if .OldFile.OldName}}old:<code>{{.OldFile.OldName}}</code>{{end}}
11+                          {{if .OldFile.NewName}}new:<code>{{.OldFile.NewName}}</code>{{end}}
12                         {{end}}
13                       </div>
14                       <pre class="m-0">{{- range .Diff -}}
15@@ -79,9 +80,10 @@
16                     </div>
17 
18                     <div class="flex-1" style="width: 48%;">
19+                      <h3 class="text-md">new</h3>
20                       <div>
21-                        {{if .NewFile.OldName}}<code>{{.NewFile.OldName}}</code>{{end}}
22-                        {{if .NewFile.NewName}}<code>{{.NewFile.NewName}}</code>{{end}}
23+                        {{if .NewFile.OldName}}old:<code>{{.NewFile.OldName}}</code>{{end}}
24+                        {{if .NewFile.NewName}}new:<code>{{.NewFile.NewName}}</code>{{end}}
25                       </div>
26                       <pre class="m-0">{{- range .Diff -}}
27                         {{- if eq .OuterType "insert" -}}
A tmpl/pages/tool.html
+53, -0
 1@@ -0,0 +1,53 @@
 2+{{template "base" .}}
 3+
 4+{{define "title"}}range-diff tool{{end}}
 5+
 6+{{define "meta"}}
 7+<meta property="og:title" content="range-diff tool" />
 8+<meta property="og:url" content="https://{{.MetaData.URL}}/tool" />
 9+<meta property="og:type" content="object" />
10+<meta property="og:site_name" content="{{.MetaData.URL}}" />
11+{{end}}
12+
13+{{define "body"}}
14+  {{if .PatchsetData.RangeDiff}}
15+    <div class="p-1">
16+      {{template "range-diff" .}}
17+    </div>
18+  {{else}}
19+    <main class="flex justify-center">
20+      <div class="container mt">
21+        <div>
22+          <h1 class="text-xl">range-diff</h1>
23+          <div>
24+            A tool to compare two commit ranges (e.g. two versions of a branch) by cross-referencing similar commits and then diff'ing them.
25+            This means the old patches and the new patches do not need to be in the same order.
26+            You can read the <a href="https://git-scm.com/docs/git-range-diff">git <code>range-diff</code> docs</a> to learn more about it.
27+          </div>
28+          <div class="mb">
29+            In order to use this tool, you need to print both versions of a branch by running <a href="https://git-scm.com/docs/git-format-patch">git <code>format-patch</code></a>.
30+          </div>
31+          <pre>git switch branch-1
32+git format-patch --stdout origin/main # paste into old box
33+git switch branch-2
34+git format-patch --stdout origin/main # paste into new box</pre>
35+        </div>
36+
37+        <form method="post" action="/tool" style="text-align: right;">
38+          <div class="flex gap items-center">
39+            <div class="flex-1" >
40+              <h2 class="text-lg">old patchset</h2>
41+              <textarea name="prev_patchset" style="width: 100%; height: 100vh; max-height: 500px;"></textarea>
42+            </div>
43+            <div>&gt;&gt;</div>
44+            <div class="flex-1" >
45+              <h2 class="text-lg">new patchset</h2>
46+              <textarea name="next_patchset" style="width: 100%; height: 100vh; max-height: 500px;"></textarea>
47+            </div>
48+          </div>
49+          <button type="submit" class="btn mt" style="background-color: transparent;">submit</button>
50+        </form>
51+      </div>
52+    </main>
53+  {{end}}
54+{{end}}
M web.go
+81, -0
  1@@ -34,6 +34,7 @@ var (
  2 	prTmpl    = getTemplate("pr.html")
  3 	userTmpl  = getTemplate("user.html")
  4 	repoTmpl  = getTemplate("repo.html")
  5+	toolTmpl  = getTemplate("tool.html")
  6 )
  7 
  8 func getTemplate(page string) *template.Template {
  9@@ -567,6 +568,12 @@ type PrDetailData struct {
 10 	MetaData
 11 }
 12 
 13+type ToolData struct {
 14+	Patchset     *Patchset
 15+	PatchsetData *PatchsetData
 16+	MetaData
 17+}
 18+
 19 func createPrDetail(page string) http.HandlerFunc {
 20 	return func(w http.ResponseWriter, r *http.Request) {
 21 		id := r.PathValue("id")
 22@@ -873,6 +880,78 @@ func createPrDetail(page string) http.HandlerFunc {
 23 	}
 24 }
 25 
 26+func toolHandlerGet(w http.ResponseWriter, r *http.Request) {
 27+	web, err := getWebCtx(r)
 28+	if err != nil {
 29+		w.WriteHeader(http.StatusUnprocessableEntity)
 30+		return
 31+	}
 32+
 33+	err = toolTmpl.Execute(w, ToolData{
 34+		MetaData: MetaData{
 35+			URL: web.Backend.Cfg.Url,
 36+		},
 37+		Patchset: &Patchset{
 38+			ID: 0,
 39+		},
 40+		PatchsetData: &PatchsetData{
 41+			RangeDiff: []*RangeDiffOutput{},
 42+		},
 43+	})
 44+	if err != nil {
 45+		web.Backend.Logger.Error("cannot execute template", "err", err)
 46+	}
 47+}
 48+
 49+func toolHandlerPost(w http.ResponseWriter, r *http.Request) {
 50+	web, err := getWebCtx(r)
 51+	if err != nil {
 52+		web.Backend.Logger.Error("web ctx not found", "err", err)
 53+		w.WriteHeader(http.StatusUnprocessableEntity)
 54+		return
 55+	}
 56+
 57+	if err := r.ParseForm(); err != nil {
 58+		web.Backend.Logger.Error("parse form", "err", err)
 59+		http.Error(w, "Failed to parse form", http.StatusBadRequest)
 60+		return
 61+	}
 62+
 63+	prevPs := r.PostFormValue("prev_patchset")
 64+	prevPs = strings.ReplaceAll(prevPs, "\r", "")
 65+	nextPs := r.PostFormValue("next_patchset")
 66+	nextPs = strings.ReplaceAll(nextPs, "\r", "")
 67+
 68+	prevPatchset, err := ParsePatchset(strings.NewReader(prevPs))
 69+	if err != nil {
 70+		web.Backend.Logger.Error("parse prev patchset", "err", err)
 71+		w.WriteHeader(http.StatusUnprocessableEntity)
 72+		return
 73+	}
 74+	nextPatchset, err := ParsePatchset(strings.NewReader(nextPs))
 75+	if err != nil {
 76+		web.Backend.Logger.Error("parse next patchset", "err", err)
 77+		w.WriteHeader(http.StatusUnprocessableEntity)
 78+		return
 79+	}
 80+	rangeDiff := RangeDiff(prevPatchset, nextPatchset)
 81+
 82+	err = toolTmpl.Execute(w, ToolData{
 83+		MetaData: MetaData{
 84+			URL: web.Backend.Cfg.Url,
 85+		},
 86+		Patchset: &Patchset{
 87+			ID: 0,
 88+		},
 89+		PatchsetData: &PatchsetData{
 90+			RangeDiff: rangeDiff,
 91+		},
 92+	})
 93+	if err != nil {
 94+		web.Backend.Logger.Error("cannot execute template", "err", err)
 95+	}
 96+}
 97+
 98 func rssHandler(w http.ResponseWriter, r *http.Request) {
 99 	web, err := getWebCtx(r)
100 	if err != nil {
101@@ -1127,6 +1206,8 @@ func GitWebServer(cfg *GitCfg) http.Handler {
102 	mux.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
103 	mux.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
104 	mux.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
105+	mux.HandleFunc("GET /tool", ctxMdw(ctx, toolHandlerGet))
106+	mux.HandleFunc("POST /tool", ctxMdw(ctx, toolHandlerPost))
107 	mux.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
108 	mux.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
109 	embedFS, err := getEmbedFS(embedStaticFS, "static")