repos / git-pr

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

commit
0083d5e
parent
6ac35cb
author
Eric Bower
date
2024-05-31 18:43:09 -0400 EDT
feat: `pr add --force` to replace patchset

style: various design tweaks
feat: list repo ID with `pr ls`
5 files changed,  +135, -32
M cli.go
M pr.go
M cli.go
+58, -20
  1@@ -94,21 +94,31 @@ Here's how it works:
  2 				Usage: "Manage Patch Requests (PR)",
  3 				Subcommands: []*cli.Command{
  4 					{
  5-						Name:  "ls",
  6-						Usage: "List all PRs",
  7+						Name:      "ls",
  8+						Usage:     "List all PRs",
  9+						Args:      true,
 10+						ArgsUsage: "[repoID]",
 11 						Action: func(cCtx *cli.Context) error {
 12-							prs, err := pr.GetPatchRequests()
 13+							repoID := cCtx.Args().First()
 14+							var prs []*PatchRequest
 15+							var err error
 16+							if repoID == "" {
 17+								prs, err = pr.GetPatchRequests()
 18+							} else {
 19+								prs, err = pr.GetPatchRequestsByRepoID(repoID)
 20+							}
 21 							if err != nil {
 22 								return err
 23 							}
 24 
 25 							writer := NewTabWriter(sesh)
 26-							fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
 27+							fmt.Fprintln(writer, "ID\tRepoID\tName\tStatus\tDate")
 28 							for _, req := range prs {
 29 								fmt.Fprintf(
 30 									writer,
 31-									"%d\t%s\t[%s]\t%s\n",
 32+									"%d\t%s\t%s\t[%s]\t%s\n",
 33 									req.ID,
 34+									req.RepoID,
 35 									req.Name,
 36 									req.Status,
 37 									req.CreatedAt.Format(time.RFC3339Nano),
 38@@ -119,8 +129,10 @@ Here's how it works:
 39 						},
 40 					},
 41 					{
 42-						Name:  "create",
 43-						Usage: "Submit a new PR",
 44+						Name:      "create",
 45+						Usage:     "Submit a new PR",
 46+						Args:      true,
 47+						ArgsUsage: "[repoID]",
 48 						Action: func(cCtx *cli.Context) error {
 49 							repoID := cCtx.Args().First()
 50 							request, err := pr.SubmitPatchRequest(repoID, pubkey, sesh)
 51@@ -137,8 +149,10 @@ Here's how it works:
 52 						},
 53 					},
 54 					{
 55-						Name:  "print",
 56-						Usage: "Print the patches for a PR",
 57+						Name:      "print",
 58+						Usage:     "Print the patches for a PR",
 59+						Args:      true,
 60+						ArgsUsage: "[prID]",
 61 						Action: func(cCtx *cli.Context) error {
 62 							prID, err := getPrID(cCtx.Args().First())
 63 							if err != nil {
 64@@ -166,8 +180,10 @@ Here's how it works:
 65 						},
 66 					},
 67 					{
 68-						Name:  "stats",
 69-						Usage: "Print PR with diff stats",
 70+						Name:      "stats",
 71+						Usage:     "Print PR with diff stats",
 72+						Args:      true,
 73+						ArgsUsage: "[prID]",
 74 						Action: func(cCtx *cli.Context) error {
 75 							prID, err := getPrID(cCtx.Args().First())
 76 							if err != nil {
 77@@ -217,8 +233,10 @@ Here's how it works:
 78 						},
 79 					},
 80 					{
 81-						Name:  "summary",
 82-						Usage: "List patches in PRs",
 83+						Name:      "summary",
 84+						Usage:     "List patches in PRs",
 85+						Args:      true,
 86+						ArgsUsage: "[prID]",
 87 						Action: func(cCtx *cli.Context) error {
 88 							prID, err := getPrID(cCtx.Args().First())
 89 							if err != nil {
 90@@ -268,8 +286,10 @@ Here's how it works:
 91 						},
 92 					},
 93 					{
 94-						Name:  "accept",
 95-						Usage: "Accept a PR",
 96+						Name:      "accept",
 97+						Usage:     "Accept a PR",
 98+						Args:      true,
 99+						ArgsUsage: "[prID]",
100 						Action: func(cCtx *cli.Context) error {
101 							prID, err := getPrID(cCtx.Args().First())
102 							if err != nil {
103@@ -285,8 +305,10 @@ Here's how it works:
104 						},
105 					},
106 					{
107-						Name:  "close",
108-						Usage: "Close a PR",
109+						Name:      "close",
110+						Usage:     "Close a PR",
111+						Args:      true,
112+						ArgsUsage: "[prID]",
113 						Action: func(cCtx *cli.Context) error {
114 							prID, err := getPrID(cCtx.Args().First())
115 							if err != nil {
116@@ -307,8 +329,10 @@ Here's how it works:
117 						},
118 					},
119 					{
120-						Name:  "reopen",
121-						Usage: "Reopen a PR",
122+						Name:      "reopen",
123+						Usage:     "Reopen a PR",
124+						Args:      true,
125+						ArgsUsage: "[prID]",
126 						Action: func(cCtx *cli.Context) error {
127 							prID, err := getPrID(cCtx.Args().First())
128 							if err != nil {
129@@ -337,6 +361,10 @@ Here's how it works:
130 								Name:  "review",
131 								Usage: "mark patch as a review",
132 							},
133+							&cli.BoolFlag{
134+								Name:  "force",
135+								Usage: "replace patchset with new patchset -- including reviews",
136+							},
137 						},
138 						Action: func(cCtx *cli.Context) error {
139 							prID, err := getPrID(cCtx.Args().First())
140@@ -345,6 +373,7 @@ Here's how it works:
141 							}
142 							isAdmin := be.IsAdmin(sesh.PublicKey())
143 							isReview := cCtx.Bool("review")
144+							isReplace := cCtx.Bool("force")
145 							var req PatchRequest
146 							err = be.DB.Get(&req, "SELECT * FROM patch_requests WHERE id=?", prID)
147 							if err != nil {
148@@ -355,7 +384,16 @@ Here's how it works:
149 								return fmt.Errorf("unauthorized, you are not the owner of this PR")
150 							}
151 
152-							patches, err := pr.SubmitPatchSet(prID, pubkey, isReview, sesh)
153+							op := OpNormal
154+							if isReview {
155+								wish.Println(sesh, "Marking new patchset as a review")
156+								op = OpReview
157+							} else if isReplace {
158+								wish.Println(sesh, "Replacing current patchset with new one")
159+								op = OpReplace
160+							}
161+
162+							patches, err := pr.SubmitPatchSet(prID, pubkey, op, sesh)
163 							if err != nil {
164 								return err
165 							}
M pr.go
+68, -6
  1@@ -15,16 +15,25 @@ import (
  2 
  3 var ErrPatchExists = errors.New("patch already exists for patch request")
  4 
  5+type PatchsetOp int
  6+
  7+const (
  8+	OpNormal PatchsetOp = iota
  9+	OpReview
 10+	OpReplace
 11+)
 12+
 13 type GitPatchRequest interface {
 14 	GetRepos() ([]Repo, error)
 15 	GetRepoByID(repoID string) (*Repo, error)
 16 	SubmitPatchRequest(repoID string, pubkey string, patchset io.Reader) (*PatchRequest, error)
 17-	SubmitPatchSet(prID int64, pubkey string, review bool, patchset io.Reader) ([]*Patch, error)
 18+	SubmitPatchSet(prID int64, pubkey string, op PatchsetOp, patchset io.Reader) ([]*Patch, error)
 19 	GetPatchRequestByID(prID int64) (*PatchRequest, error)
 20 	GetPatchRequests() ([]*PatchRequest, error)
 21 	GetPatchRequestsByRepoID(repoID string) ([]*PatchRequest, error)
 22 	GetPatchesByPrID(prID int64) ([]*Patch, error)
 23 	UpdatePatchRequest(prID int64, status string) error
 24+	DeletePatchesByPrID(prID int64) error
 25 }
 26 
 27 type PrCmd struct {
 28@@ -188,7 +197,7 @@ func (cmd PrCmd) parsePatchSet(patchset io.Reader) ([]*Patch, error) {
 29 	return patches, nil
 30 }
 31 
 32-func (cmd PrCmd) createPatch(tx *sqlx.Tx, patch *Patch) (int64, error) {
 33+func (cmd PrCmd) createPatch(tx *sqlx.Tx, review bool, patch *Patch) (int64, error) {
 34 	patchExists := []Patch{}
 35 	_ = cmd.Backend.DB.Select(&patchExists, "SELECT * FROM patches WHERE patch_request_id = ? AND content_sha = ?", patch.PatchRequestID, patch.ContentSha)
 36 	if len(patchExists) > 0 {
 37@@ -197,7 +206,7 @@ func (cmd PrCmd) createPatch(tx *sqlx.Tx, patch *Patch) (int64, error) {
 38 
 39 	var patchID int64
 40 	row := tx.QueryRow(
 41-		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, content_sha, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
 42+		"INSERT INTO patches (pubkey, patch_request_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, content_sha, review, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
 43 		patch.Pubkey,
 44 		patch.PatchRequestID,
 45 		patch.AuthorName,
 46@@ -208,6 +217,7 @@ func (cmd PrCmd) createPatch(tx *sqlx.Tx, patch *Patch) (int64, error) {
 47 		patch.BodyAppendix,
 48 		patch.CommitSha,
 49 		patch.ContentSha,
 50+		review,
 51 		patch.RawText,
 52 	)
 53 	err := row.Scan(&patchID)
 54@@ -262,7 +272,7 @@ func (cmd PrCmd) SubmitPatchRequest(repoID string, pubkey string, patchset io.Re
 55 	for _, patch := range patches {
 56 		patch.Pubkey = pubkey
 57 		patch.PatchRequestID = prID
 58-		_, err = cmd.createPatch(tx, patch)
 59+		_, err = cmd.createPatch(tx, false, patch)
 60 		if err != nil {
 61 			return nil, err
 62 		}
 63@@ -278,7 +288,52 @@ func (cmd PrCmd) SubmitPatchRequest(repoID string, pubkey string, patchset io.Re
 64 	return &pr, err
 65 }
 66 
 67-func (cmd PrCmd) SubmitPatchSet(prID int64, pubkey string, review bool, patchset io.Reader) ([]*Patch, error) {
 68+func (cmd PrCmd) SubmitPatchSet(prID int64, pubkey string, op PatchsetOp, patchset io.Reader) ([]*Patch, error) {
 69+	fin := []*Patch{}
 70+	tx, err := cmd.Backend.DB.Beginx()
 71+	if err != nil {
 72+		return fin, err
 73+	}
 74+
 75+	defer func() {
 76+		_ = tx.Rollback()
 77+	}()
 78+
 79+	patches, err := cmd.parsePatchSet(patchset)
 80+	if err != nil {
 81+		return fin, err
 82+	}
 83+
 84+	if op == OpReplace {
 85+		err = cmd.DeletePatchesByPrID(prID)
 86+		if err != nil {
 87+			return fin, err
 88+		}
 89+	}
 90+
 91+	for _, patch := range patches {
 92+		patch.Pubkey = pubkey
 93+		patch.PatchRequestID = prID
 94+		patchID, err := cmd.createPatch(tx, op == OpReview, patch)
 95+		if err == nil {
 96+			patch.ID = patchID
 97+			fin = append(fin, patch)
 98+		} else {
 99+			if !errors.Is(ErrPatchExists, err) {
100+				return fin, err
101+			}
102+		}
103+	}
104+
105+	err = tx.Commit()
106+	if err != nil {
107+		return fin, err
108+	}
109+
110+	return fin, err
111+}
112+
113+func (cmd PrCmd) ReplacePatchSet(prID int64, pubkey string, patchset io.Reader) ([]*Patch, error) {
114 	fin := []*Patch{}
115 	tx, err := cmd.Backend.DB.Beginx()
116 	if err != nil {
117@@ -297,7 +352,7 @@ func (cmd PrCmd) SubmitPatchSet(prID int64, pubkey string, review bool, patchset
118 	for _, patch := range patches {
119 		patch.Pubkey = pubkey
120 		patch.PatchRequestID = prID
121-		patchID, err := cmd.createPatch(tx, patch)
122+		patchID, err := cmd.createPatch(tx, false, patch)
123 		if err == nil {
124 			patch.ID = patchID
125 			fin = append(fin, patch)
126@@ -315,3 +370,10 @@ func (cmd PrCmd) SubmitPatchSet(prID int64, pubkey string, review bool, patchset
127 
128 	return fin, err
129 }
130+
131+func (cmd PrCmd) DeletePatchesByPrID(prID int64) error {
132+	_, err := cmd.Backend.DB.Exec(
133+		"DELETE FROM patches WHERE patch_request_id=?", prID,
134+	)
135+	return err
136+}
M tmpl/pr-detail.html
+2, -2
 1@@ -21,8 +21,8 @@
 2         <date>{{.AuthorDate}}</date>
 3       </div>
 4 
 5-      <div>
 6-        <div>{{.Body}}</div>
 7+      <div class="my">
 8+        <pre>{{.Body}}</pre>
 9       </div>
10     </div>
11     {{else}}
M tmpl/repo-detail.html
+1, -1
1@@ -13,7 +13,7 @@
2     <div>Submit patch request: <code>git format-patch -1 HEAD --stdout | ssh pr.pico.sh pr create {{.ID}}</code></div>
3 	</div>
4 </header>
5-<main>
6+<main class="group">
7   {{range .Prs}}
8   <article class="box">
9     <div><a href="{{.Url}}">{{.Text}}</a> <code>{{.Status}}</code></div>
M tmpl/repo-list.html
+6, -3
 1@@ -10,10 +10,13 @@
 2   <h1 class="text-2xl mb">Repos</h1>
 3 </header>
 4 <main>
 5-  <ul>
 6+  <div class="group">
 7   {{range .Repos}}
 8-    <li><a href="{{.Url}}">{{.Text}}</a></li>
 9+    <div class="box">
10+      <h3 class="text-lg"><a href="{{.Url}}">{{.Text}}</a></h3>
11+      <div class="text-sm">{{.Desc}}</div>
12+    </div>
13   {{end}}
14-  </ul>
15+  </div>
16 </main>
17 {{end}}