repos / git-pr

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

commit
7338b44
parent
c5d9c3c
author
Eric Bower
date
2025-03-28 10:47:13 -0400 EDT
feat: allow config `desc` to add a description box to index page

refactor: consolidate docs page onto index

This change will allow maintainers to add a description box to the top
of their git-pr instance as an introduction to their service.
7 files changed,  +241, -337
M cfg.go
M web.go
M cfg.go
+2, -0
 1@@ -27,6 +27,7 @@ type GitCfg struct {
 2 	CreateRepo string          `koanf:"create_repo"`
 3 	Theme      string          `koanf:"theme"`
 4 	TimeFormat string          `koanf:"time_format"`
 5+	Desc       string          `koanf:"desc"`
 6 	Logger     *slog.Logger
 7 }
 8 
 9@@ -113,6 +114,7 @@ func NewGitCfg(logger *slog.Logger) *GitCfg {
10 		"theme", out.Theme,
11 		"time_format", out.TimeFormat,
12 		"create_repo", out.CreateRepo,
13+		"desc", out.Desc,
14 	)
15 
16 	for _, pubkey := range out.AdminsStr {
M git-pr.toml
+1, -0
1@@ -11,3 +11,4 @@ time_format = "2006-01-02"
2 #   admin: only admins
3 #   user: admins and users
4 create_repo = "user"
5+desc = ""
M static/git-pr.css
+1, -1
1@@ -4,7 +4,7 @@ body {
2 }
3 
4 pre {
5-  font-size: 1rem;
6+  padding: var(--grid-height);
7 }
8 
9 table, tr {
D tmpl/docs.html
+0, -283
  1@@ -1,283 +0,0 @@
  2-{{template "base" .}}
  3-
  4-{{define "title"}}git-pr{{end}}
  5-
  6-{{define "meta"}}
  7-<link rel="alternate" type="application/atom+xml"
  8-      title="RSS feed for git collaboration server"
  9-      href="/rss" />
 10-{{end}}
 11-
 12-{{define "body"}}
 13-<header class="group">
 14-  <h1 class="text-2xl"><a href="/">DASHBOARD</a> / docs</h1>
 15-  <div>
 16-    <span>A pastebin supercharged for git collaboration</span> &middot;
 17-    <a href="https://github.com/picosh/git-pr">github</a> &middot;
 18-    <a href="https://youtu.be/d28Dih-BBUw">demo video</a>
 19-  </div>
 20-  <pre class="m-0">ssh {{.MetaData.URL}} help</pre>
 21-</header>
 22-
 23-<main class="group">
 24-  <details>
 25-    <summary>Intro</summary>
 26-
 27-    <div>
 28-      <p>
 29-        We are trying to build the simplest git collaboration tool. The goal is to make
 30-        self-hosting as simple as running an SSH server -- all without
 31-        sacrificing external collaborators time and energy.
 32-      </p>
 33-
 34-      <blockquote>
 35-        <code>git format-patch</code> isn't the problem and pull requests aren't the solution.
 36-      </blockquote>
 37-
 38-      <p>
 39-        We are combining mailing list and pull request workflows. In order to build the
 40-        simplest collaboration tool, we needed something as simple as generating patches
 41-        but the ease-of-use of pull requests.
 42-      </p>
 43-
 44-      <p>
 45-        The goal is not to create another code forge, the goal is to create a very
 46-        simple self-hosted git solution with the ability to collaborate with external
 47-        contributors. All the code owner needs to setup a running git server:
 48-      </p>
 49-
 50-      <ul><li>A single golang binary</li></ul>
 51-
 52-      <div>
 53-        All an external contributor needs is:
 54-      </div>
 55-
 56-      <ul>
 57-        <li>An SSH keypair</li>
 58-        <li>An SSH client</li>
 59-      </ul>
 60-
 61-      <div>Then everyone subscribes to our RSS feeds to receive updates to patch requests.</div>
 62-
 63-      <h2 class="text-xl">the problem</h2>
 64-
 65-      <p>
 66-        Email is great as a decentralized system to send and receive changes (patchsets)
 67-        to a git repo. However, onboarding a new user to a mailing list, properly
 68-        setting up their email client, and then finally submitting the code contribution
 69-        is enough to make many developers give up. Further, because we are leveraging
 70-        the email protocol for collaboration, we are limited by its feature-set. For
 71-        example, it is not possible to make edits to emails, everyone has a different
 72-        client, those clients have different limitations around plain text email and
 73-        downloading patches from it.
 74-      </p>
 75-
 76-      <p>
 77-        Github pull requests are easy to use, easy to edit, and easy to manage. The
 78-        downside is it forces the user to be inside their website to perform reviews.
 79-        For quick changes, this is great, but when you start reading code within a web
 80-        browser, there are quite a few downsides. At a certain point, it makes more
 81-        sense to review code inside your local development environment, IDE, etc. There
 82-        are tools and plugins that allow users to review PRs inside their IDE, but it
 83-        requires a herculean effort to make it usable.
 84-      </p>
 85-
 86-      <p>
 87-        Further, self-hosted solutions that mimic a pull request require a lot of
 88-        infrastructure in order to manage it. A database, a web site connected to git,
 89-        admin management, and services to manage it all. Another big point of friction:
 90-        before an external user submits a code change, they first need to create an
 91-        account and then login. This adds quite a bit of friction for a self-hosted
 92-        solution, not only for an external contributor, but also for the code owner who
 93-        has to provision the infra. Often times they also have to fork the repo within
 94-        the code forge before submitting a PR. Then they never make a contribution ever
 95-        again and keep a forked repo around forever. That seems silly.
 96-      </p>
 97-
 98-      <h2 class="text-xl">introducing patch requests (PR)</h2>
 99-
100-      <p>
101-        Instead, we want to create a self-hosted git "server" that can handle sending
102-        and receiving patches without the cumbersome nature of setting up email or the
103-        limitations imposed by the email protocol. Further, we want the primary workflow
104-        to surround the local development environment. Github is bringing the IDE to the
105-        browser in order to support their workflow, we want to flip that idea on its
106-        head by making code reviews a first-class citizen inside your local development
107-        environment.
108-      </p>
109-
110-      <p>
111-        We see this as a hybrid between the github workflow of a pull request and
112-        sending and receiving patches over email.
113-      </p>
114-
115-      <p>
116-        The basic idea is to leverage an SSH app to handle most of the interaction
117-        between contributor and owner of a project. Everything can be done completely
118-        within the terminal, in a way that is ergonomic and fully featured.
119-      </p>
120-
121-      <p>
122-        Notifications would happen with RSS and all state mutations would result in the
123-        generation of static web assets so it can all be hosted using a simple file web
124-        server.
125-      </p>
126-
127-      <h3 class="text-lg">format-patch workflow</h3>
128-
129-      <p>
130-        The fundamental collaboration tool here is <code>format-patch</code>. Whether you a
131-        submitting code changes or you are reviewing code changes, it all happens in
132-        code. Both contributor and owner are simply creating new commits and generating
133-        patches on top of each other. This obviates the need to have a web viewer where
134-        the reviewing can "comment" on a line of code block. There's no need, apply the
135-        contributor's patches, write comments or code changes, generate a new patch,
136-        send the patch to the git server as a "review." This flow also works the exact
137-        same if two users are collaborating on a set of changes.
138-      </p>
139-
140-      <p>
141-        This also solves the problem of sending multiple patchsets for the same code
142-        change. There's a single, central Patch Request where all changes and
143-        collaboration happens.
144-      </p>
145-
146-      <p>
147-        We could figure out a way to leverage <code>git notes</code> for reviews / comments, but
148-        honestly, that solution feels brutal and outside the comfort level of most git
149-        users. Just send reviews as code and write comments in the programming language
150-        you are using. It's the job of the contributor to "address" those comments and
151-        then remove them in subsequent patches. This is the forcing function to address
152-        all comments: the patch won't be merged if there are comment unaddressed in
153-        code; they cannot be ignored or else they will be upstreamed erroneously.
154-      </p>
155-    </div>
156-  </details>
157-
158-  <details>
159-    <summary>How do Patch Requests work?</summary>
160-      <div>
161-        Patch requests (PR) are the simplest way to submit, review, and accept changes to your git repository.
162-        Here's how it works:
163-      </div>
164-
165-      <ol>
166-        <li>External contributor clones repo (<code>git-clone</code>)</li>
167-        <li>External contributor makes a code change (<code>git-add</code> & <code>git-commit</code>)</li>
168-        <li>External contributor generates patches (<code>git-format-patch</code>)</li>
169-        <li>External contributor submits a PR to SSH server</li>
170-        <li>Owner receives RSS notification that there's a new PR</li>
171-        <li>Owner applies patches locally (<code>git-am</code>) from SSH server</li>
172-        <li>Owner makes suggestions in code! (<code>git-add</code> & <code>git-commit</code>)</li>
173-        <li>Owner submits review by piping patch to SSH server (<code>git-format-patch</code>)</li>
174-        <li>External contributor receives RSS notification of the PR review</li>
175-        <li>External contributor re-applies patches (<code>git-am</code>)</li>
176-        <li>External contributor reviews and removes comments in code!</li>
177-        <li>External contributor submits another patch (<code>git-format-patch</code>)</li>
178-        <li>Owner applies patches locally (<code>git-am</code>)</li>
179-        <li>Owner marks PR as accepted and pushes code to main (<code>git-push</code>)</li>
180-      </ol>
181-
182-      <div>Example commands</div>
183-
184-      <pre># Owner hosts repo `test.git` using github
185-
186-# Contributor clones repo
187-git clone git@github.com:picosh/test.git
188-
189-# Contributor wants to make a change
190-# Contributor makes changes via commits
191-git add -A && git commit -m "fix: some bugs"
192-
193-# Contributor runs:
194-git format-patch origin/main --stdout | ssh {{.MetaData.URL}} pr create test
195-# > Patch Request has been created (ID: 1)
196-
197-# Owner can checkout patch:
198-ssh {{.MetaData.URL}} pr print 1 | git am -3
199-# Owner can comment (IN CODE), commit, then send another format-patch
200-# on top of the PR:
201-git format-patch origin/main --stdout | ssh {{.MetaData.URL}} pr add --review 1
202-# UI clearly marks patch as a review
203-
204-# Contributor can checkout reviews
205-ssh {{.MetaData.URL}} pr print 1 | git am -3
206-
207-# Owner can reject a pr:
208-ssh {{.MetaData.URL}} pr close 1
209-
210-# Owner can accept a pr:
211-ssh {{.MetaData.URL}} pr accept 1
212-
213-# Owner can prep PR for upstream:
214-git rebase -i origin/main
215-
216-# Then push to upstream
217-git push origin main
218-
219-# Done!
220-</pre>
221-  </details>
222-
223-  <details>
224-    <summary>What's a repo?</summary>
225-
226-    <div>
227-      A repo is designed to mimick a git repo, but it's really just a tag.  When
228-      submitting a patch request, if the user does not provide a repo name then
229-      the default "bin" will be selected.  When a user creates a repo they become
230-      the repo owner and have special privileges.
231-    </div>
232-  </details>
233-
234-  <details>
235-    <summary>Can anyone use this service?</summary>
236-
237-    <div>
238-      This service is a public space for anyone to freely create "repos" and
239-      collaborate with users.  Anyone is able to add patchsets to a patch request
240-      and anyone is able to review any other patch requests, regardless of repo.
241-    </div>
242-  </details>
243-
244-  <details>
245-    <summary>First time user experience</summary>
246-
247-    <div>
248-      Using this service for the first time?  Creating a patch request is simple:
249-    </div>
250-
251-    <pre>git format-patch main --stdout | ssh pr.pico.sh pr create {repo}</pre>
252-
253-    <div>When running that command we will automatically create a user and a repo if one doesn't exist.</div>
254-
255-    <div>Want to submit a v2 of the patch request?</div>
256-
257-    <pre>git format-patch main --stdout | ssh pr.pico.sh pr add {prID}</pre>
258-  </details>
259-
260-  <details>
261-    <summary>How do I receive notifications?</summary>
262-
263-    <div>
264-      We have different RSS feeds depending on the use case.  This is how you
265-      can receive notifications for when someone submits or reviews patch requests.
266-    </div>
267-  </details>
268-
269-  <details>
270-    <summary>Alternative git collaboration systems</summary>
271-
272-    <div>
273-      <ol>
274-        <li><a href="https://gerritcodereview.com/">Gerrit</a></li>
275-        <li><a href="https://we.phorge.it/">Phorge</a> (fork of Phabricator)</li>
276-        <li><a href="https://graphite.dev/docs/cli-quick-start">Graphite</a></li>
277-        <li><a href="https://codeapprove.com/">CodeApprove</a></li>
278-        <li><a href="https://reviewable.io/">Reviewable</a></li>
279-      </ol>
280-    </div>
281-  </details>
282-</main>
283-
284-{{end}}
M tmpl/index.html
+215, -19
  1@@ -10,38 +10,234 @@
  2 
  3 {{define "body"}}
  4 <header class="group">
  5-  <h1 class="text-2xl">patchbin</h1>
  6+  <h1 class="text-2xl">git-pr</h1>
  7   <div>
  8     <span>A pastebin supercharged for git collaboration</span> &middot;
  9-    <a href="/docs">docs</a>
 10+    <a href="https://github.com/picosh/git-pr">github</a> &middot;
 11+    <a href="https://youtu.be/d28Dih-BBUw">demo video</a>
 12   </div>
 13 
 14+  {{if .MetaData.Desc}}
 15   <div class="box-sm">
 16-    <div>
 17-      Welcome to <a href="https://pico.sh">pico's</a> managed patchbin service!
 18-      This is a <strong>public</strong> service that is free to anyone who wants
 19-      to collaborate on git patches.  The idea is simple: submit a patchset to
 20-      our service and let anyone collaborate on it by submitting follow-up patchsets.
 21-      Using this service for the first time?  Creating a patch request is simple:
 22-    </div>
 23+    <div>{{.MetaData.Desc}}</div>
 24+  </div>
 25+  {{end}}
 26 
 27-    <pre class="text-sm">git format-patch main --stdout | ssh pr.pico.sh pr create {repo}</pre>
 28+  <details>
 29+    <summary>Intro</summary>
 30 
 31     <div>
 32-      When running that command we will automatically create a user and a repo
 33-      if one doesn't exist. Once the patches have been submitted you'll receive
 34-      a link that you can send to a reviewer.  Anyone can review patch requests.
 35-      Want to submit a v2 of the patch request?
 36+      <p>
 37+        We are trying to build the simplest git collaboration tool. The goal is to make
 38+        self-hosting as simple as running an SSH server -- all without
 39+        sacrificing external collaborators time and energy.
 40+      </p>
 41+
 42+      <blockquote>
 43+        <code>git format-patch</code> isn't the problem and pull requests aren't the solution.
 44+      </blockquote>
 45+
 46+      <p>
 47+        We are combining mailing list and pull request workflows. In order to build the
 48+        simplest collaboration tool, we needed something as simple as generating patches
 49+        but the ease-of-use of pull requests.
 50+      </p>
 51+
 52+      <p>
 53+        The goal is not to create another code forge, the goal is to create a very
 54+        simple self-hosted git solution with the ability to collaborate with external
 55+        contributors. All the code owner needs to setup a running git server:
 56+      </p>
 57+
 58+      <ul><li>A single golang binary</li></ul>
 59+
 60+      <div>
 61+        All an external contributor needs is:
 62+      </div>
 63+
 64+      <ul>
 65+        <li>An SSH keypair</li>
 66+        <li>An SSH client</li>
 67+      </ul>
 68+
 69+      <p>Then everyone subscribes to our RSS feeds to receive updates to patch requests.</p>
 70+
 71+      <h2 class="text-xl">the problem</h2>
 72+
 73+      <p>
 74+        Email is great as a decentralized system to send and receive changes (patchsets)
 75+        to a git repo. However, onboarding a new user to a mailing list, properly
 76+        setting up their email client, and then finally submitting the code contribution
 77+        is enough to make many developers give up. Further, because we are leveraging
 78+        the email protocol for collaboration, we are limited by its feature-set. For
 79+        example, it is not possible to make edits to emails, everyone has a different
 80+        client, those clients have different limitations around plain text email and
 81+        downloading patches from it.
 82+      </p>
 83+
 84+      <p>
 85+        Github pull requests are easy to use, easy to edit, and easy to manage. The
 86+        downside is it forces the user to be inside their website to perform reviews.
 87+        For quick changes, this is great, but when you start reading code within a web
 88+        browser, there are quite a few downsides. At a certain point, it makes more
 89+        sense to review code inside your local development environment, IDE, etc. There
 90+        are tools and plugins that allow users to review PRs inside their IDE, but it
 91+        requires a herculean effort to make it usable.
 92+      </p>
 93+
 94+      <p>
 95+        Further, self-hosted solutions that mimic a pull request require a lot of
 96+        infrastructure in order to manage it. A database, a web site connected to git,
 97+        admin management, and services to manage it all. Another big point of friction:
 98+        before an external user submits a code change, they first need to create an
 99+        account and then login. This adds quite a bit of friction for a self-hosted
100+        solution, not only for an external contributor, but also for the code owner who
101+        has to provision the infra. Often times they also have to fork the repo within
102+        the code forge before submitting a PR. Then they never make a contribution ever
103+        again and keep a forked repo around forever. That seems silly.
104+      </p>
105+
106+      <h2 class="text-xl">introducing patch requests (PR)</h2>
107+
108+      <p>
109+        Instead, we want to create a self-hosted git "server" that can handle sending
110+        and receiving patches without the cumbersome nature of setting up email or the
111+        limitations imposed by the email protocol. Further, we want the primary workflow
112+        to surround the local development environment. Github is bringing the IDE to the
113+        browser in order to support their workflow, we want to flip that idea on its
114+        head by making code reviews a first-class citizen inside your local development
115+        environment.
116+      </p>
117+
118+      <p>
119+        We see this as a hybrid between the github workflow of a pull request and
120+        sending and receiving patches over email.
121+      </p>
122+
123+      <p>
124+        The basic idea is to leverage an SSH app to handle most of the interaction
125+        between contributor and owner of a project. Everything can be done completely
126+        within the terminal, in a way that is ergonomic and fully featured.
127+      </p>
128+
129+      <p>
130+        Notifications would happen with RSS and all state mutations would result in the
131+        generation of static web assets so it can all be hosted using a simple file web
132+        server.
133+      </p>
134+
135+      <h3 class="text-lg">format-patch workflow</h3>
136+
137+      <p>
138+        The fundamental collaboration tool here is <code>format-patch</code>. Whether you a
139+        submitting code changes or you are reviewing code changes, it all happens in
140+        code. Both contributor and owner are simply creating new commits and generating
141+        patches on top of each other. This obviates the need to have a web viewer where
142+        the reviewing can "comment" on a line of code block. There's no need, apply the
143+        contributor's patches, write comments or code changes, generate a new patch,
144+        send the patch to the git server as a "review." This flow also works the exact
145+        same if two users are collaborating on a set of changes.
146+      </p>
147+
148+      <p>
149+        This also solves the problem of sending multiple patchsets for the same code
150+        change. There's a single, central Patch Request where all changes and
151+        collaboration happens.
152+      </p>
153+
154+      <p>
155+        We could figure out a way to leverage <code>git notes</code> for reviews / comments, but
156+        honestly, that solution feels brutal and outside the comfort level of most git
157+        users. Just send reviews as code and write comments in the programming language
158+        you are using. It's the job of the contributor to "address" those comments and
159+        then remove them in subsequent patches. This is the forcing function to address
160+        all comments: the patch won't be merged if there are comment unaddressed in
161+        code; they cannot be ignored or else they will be upstreamed erroneously.
162+      </p>
163     </div>
164+  </details>
165+
166+  <details>
167+    <summary>How do Patch Requests work?</summary>
168+      <div>
169+        Patch requests (PR) are the simplest way to submit, review, and accept changes to your git repository.
170+        Here's how it works:
171+      </div>
172+
173+      <ol>
174+        <li>External contributor clones repo (<code>git-clone</code>)</li>
175+        <li>External contributor makes a code change (<code>git-add</code> & <code>git-commit</code>)</li>
176+        <li>External contributor generates patches (<code>git-format-patch</code>)</li>
177+        <li>External contributor submits a PR to SSH server</li>
178+        <li>Owner receives RSS notification that there's a new PR</li>
179+        <li>Owner applies patches locally (<code>git-am</code>) from SSH server</li>
180+        <li>Owner makes suggestions in code! (<code>git-add</code> & <code>git-commit</code>)</li>
181+        <li>Owner submits review by piping patch to SSH server (<code>git-format-patch</code>)</li>
182+        <li>External contributor receives RSS notification of the PR review</li>
183+        <li>External contributor re-applies patches (<code>git-am</code>)</li>
184+        <li>External contributor reviews and removes comments in code!</li>
185+        <li>External contributor submits another patch (<code>git-format-patch</code>)</li>
186+        <li>Owner applies patches locally (<code>git-am</code>)</li>
187+        <li>Owner marks PR as accepted and pushes code to main (<code>git-push</code>)</li>
188+      </ol>
189+
190+      <div>Example commands</div>
191+
192+      <pre># Owner hosts repo `test.git` using github
193+
194+# Contributor clones repo
195+git clone git@github.com:picosh/test.git
196+
197+# Contributor wants to make a change
198+# Contributor makes changes via commits
199+git add -A && git commit -m "fix: some bugs"
200+
201+# Contributor runs:
202+git format-patch origin/main --stdout | ssh {{.MetaData.URL}} pr create test
203+# > Patch Request has been created (ID: 1)
204+
205+# Owner can checkout patch:
206+ssh {{.MetaData.URL}} pr print 1 | git am -3
207+
208+# Owner can comment (IN CODE), commit, then send another format-patch
209+# on top of the PR:
210+git format-patch origin/main --stdout | ssh {{.MetaData.URL}} pr add --review 1
211+# UI clearly marks patch as a review
212 
213-    <pre class="text-sm">git format-patch main --stdout | ssh pr.pico.sh pr add {prID}</pre>
214+# Contributor can checkout reviews
215+ssh {{.MetaData.URL}} print pr-1 | git am -3
216+
217+# Owner can reject a pr:
218+ssh {{.MetaData.URL}} pr close 1
219+
220+# Owner can accept a pr:
221+ssh {{.MetaData.URL}} pr accept 1
222+
223+# Owner can prep PR for upstream:
224+git rebase -i origin/main
225+
226+# Then push to upstream
227+git push origin main
228+
229+# Done!
230+</pre>
231+  </details>
232+
233+  <details>
234+    <summary>First time user?</summary>
235 
236     <div>
237-      Downloading a patchset is easy as well:
238+      Using this service for the first time?  Creating a patch request is simple:
239     </div>
240 
241-    <pre class="text-sm">ssh pr.pico.sh print pr-{prID}</pre>
242-  </div>
243+    <pre>git format-patch main --stdout | ssh {{.MetaData.URL}} pr create {repo}</pre>
244+
245+    <div>When running that command we will automatically create a user and a repo if one doesn't exist.</div>
246+
247+    <div>Want to submit a v2 of the patch request?</div>
248+
249+    <pre>git format-patch main --stdout | ssh {{.MetaData.URL}} pr add {prID}</pre>
250+  </details>
251 </header>
252 
253 <main>
254@@ -57,6 +253,6 @@
255 </main>
256 
257 <footer class="mt">
258-  <div><a href="/rss">rss</a></div>
259+  <a href="/rss">rss</a>
260 </footer>
261 {{end}}
M tmpl/pr-header.html
+17, -12
 1@@ -17,18 +17,23 @@
 2   <details>
 3     <summary>Help</summary>
 4     <div class="group">
 5-      <pre class="m-0"># checkout latest patchset
 6-ssh {{.MetaData.URL}} print pr-{{.Pr.ID}} | git am -3</pre>
 7-      <pre class="m-0"># checkout any patchset in a patch request
 8-ssh {{.MetaData.URL}} print ps-X | git am -3</pre>
 9-      <pre class="m-0"># add changes to patch request
10-git format-patch {{.Branch}} --stdout | ssh {{.MetaData.URL}} pr add {{.Pr.ID}}</pre>
11-      <pre class="m-0"># add review to patch request
12-git format-patch {{.Branch}} --stdout | ssh {{.MetaData.URL}} pr add --review {{.Pr.ID}}</pre>
13-      <pre class="m-0"># accept PR
14-ssh {{.MetaData.URL}} pr accept {{.Pr.ID}}</pre>
15-      <pre class="m-0"># close PR
16-ssh {{.MetaData.URL}} pr close {{.Pr.ID}}</pre>
17+      checkout latest patchset:
18+      <pre class="m-0">ssh {{.MetaData.URL}} print pr-{{.Pr.ID}} | git am -3</pre>
19+
20+      checkout any patchset in a patch request:
21+      <pre class="m-0">ssh {{.MetaData.URL}} print ps-X | git am -3</pre>
22+
23+      add changes to patch request:
24+      <pre class="m-0">git format-patch {{.Branch}} --stdout | ssh {{.MetaData.URL}} pr add {{.Pr.ID}}</pre>
25+
26+      add review to patch request:
27+      <pre class="m-0">git format-patch {{.Branch}} --stdout | ssh {{.MetaData.URL}} pr add --review {{.Pr.ID}}</pre>
28+
29+      accept PR:
30+      <pre class="m-0">ssh {{.MetaData.URL}} pr accept {{.Pr.ID}}</pre>
31+
32+      close PR:
33+      <pre class="m-0">ssh {{.MetaData.URL}} pr close {{.Pr.ID}}</pre>
34     </div>
35   </details>
36 </header>
M web.go
+5, -22
 1@@ -287,25 +287,6 @@ func getPrTableData(web *WebCtx, prs []*PatchRequest, query url.Values) ([]*PrLi
 2 	return prdata, nil
 3 }
 4 
 5-func docsHandler(w http.ResponseWriter, r *http.Request) {
 6-	web, err := getWebCtx(r)
 7-	if err != nil {
 8-		w.WriteHeader(http.StatusInternalServerError)
 9-		return
10-	}
11-
12-	w.Header().Set("content-type", "text/html")
13-	tmpl := getTemplate("docs.html")
14-	err = tmpl.ExecuteTemplate(w, "docs.html", BasicData{
15-		MetaData: MetaData{
16-			URL: web.Backend.Cfg.Url,
17-		},
18-	})
19-	if err != nil {
20-		web.Backend.Logger.Error("cannot execute template", "err", err)
21-	}
22-}
23-
24 func indexHandler(w http.ResponseWriter, r *http.Request) {
25 	web, err := getWebCtx(r)
26 	if err != nil {
27@@ -349,7 +330,8 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
28 		NumClosed:   numClosed,
29 		Prs:         prdata,
30 		MetaData: MetaData{
31-			URL: web.Backend.Cfg.Url,
32+			URL:  web.Backend.Cfg.Url,
33+			Desc: template.HTML(web.Backend.Cfg.Desc),
34 		},
35 	})
36 	if err != nil {
37@@ -366,7 +348,8 @@ type UserData struct {
38 }
39 
40 type MetaData struct {
41-	URL string
42+	URL  string
43+	Desc template.HTML
44 }
45 
46 type PrListData struct {
47@@ -1122,6 +1105,7 @@ func StartWebServer(cfg *GitCfg) {
48 	}
49 	formatter := formatterHtml.New(
50 		formatterHtml.WithLineNumbers(true),
51+		formatterHtml.LineNumbersInTable(true),
52 		formatterHtml.WithClasses(true),
53 		formatterHtml.WithLinkableLineNumbers(true, "gitpr"),
54 	)
55@@ -1147,7 +1131,6 @@ func StartWebServer(cfg *GitCfg) {
56 	http.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
57 	http.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
58 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
59-	http.HandleFunc("GET /docs", ctxMdw(ctx, docsHandler))
60 	http.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
61 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
62 	embedFS, err := getEmbedFS(embedStaticFS, "static")