forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: implement patch updates in pulls

Changed files
+185 -56
appview
db
pages
templates
repo
issues
pulls
state
+5
appview/db/pulls.go
···
return count, nil
}
+
+
func EditPatch(e Execer, repoAt syntax.ATURI, pullId int, patch string) error {
+
_, err := e.Exec(`update pulls set patch = ? where repo_at = ? and pull_id = ?`, patch, repoAt, pullId)
+
return err
+
}
+4 -4
appview/pages/templates/repo/issues/issue.html
···
{{ define "title" }}
-
{{ .Issue.Title }} ·
+
{{ .Issue.Title }} · issue #{{ .Issue.IssueId }} ·
{{ .RepoInfo.FullName }}
{{ end }}
{{ define "repoContent" }}
-
<header>
-
<p class="text-2xl">
+
<header class="pb-4">
+
<h1 class="text-2xl">
{{ .Issue.Title }}
<span class="text-gray-500">#{{ .Issue.IssueId }}</span>
-
</p>
+
</h1>
</header>
{{ $bgColor := "bg-gray-800" }}
+135 -34
appview/pages/templates/repo/pulls/pull.html
···
{{ end }}
{{ define "repoContent" }}
-
<h1>
-
{{ .Pull.Title }}
-
<span class="text-gray-400">#{{ .Pull.PullId }}</span>
-
</h1>
+
+
<header class="pb-4">
+
<h1 class="text-2xl">
+
{{ .Pull.Title }}
+
<span class="text-gray-500">#{{ .Pull.PullId }}</span>
+
</h1>
+
</header>
+
{{ $bgColor := "bg-gray-800" }}
{{ $icon := "ban" }}
{{ if eq .State "open" }}
···
{{ end }}
</section>
-
<div>
+
<div class="flex flex-col justify-end mt-4">
<details>
<summary
-
class="list-none cursor-pointer sticky top-0 bg-white rounded-sm px-3 py-2 border border-gray-200 flex items-center text-gray-700 hover:bg-gray-50 transition-colors"
+
class="list-none cursor-pointer sticky top-0 bg-white rounded-sm px-3 py-2 border border-gray-200 flex items-center text-gray-700 hover:bg-gray-50 transition-colors mt-auto"
>
<i data-lucide="code" class="w-4 h-4 mr-2"></i>
<span>patch</span>
</summary>
-
<pre class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm">
+
<div class="relative">
+
<pre
+
id="patch-preview"
+
class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm"
+
>
{{- .Pull.Patch -}}
-
</pre>
+
</pre
+
>
+
<form
+
id="patch-form"
+
hx-patch="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/patch"
+
hx-swap="none"
+
>
+
<textarea
+
id="patch"
+
name="patch"
+
class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden"
+
>
+
{{- .Pull.Patch -}}</textarea
+
>
+
+
<div class="flex gap-2 justify-end mt-2">
+
<button
+
id="edit-patch-btn"
+
type="button"
+
class="btn btn-sm"
+
onclick="togglePatchEdit(true)"
+
>
+
<i data-lucide="edit" class="w-4 h-4 mr-1"></i>Edit
+
</button>
+
<button
+
id="save-patch-btn"
+
type="submit"
+
class="btn btn-sm bg-green-500 hidden"
+
>
+
<i data-lucide="save" class="w-4 h-4 mr-1"></i>Save
+
</button>
+
<button
+
id="cancel-patch-btn"
+
type="button"
+
class="btn btn-sm bg-gray-300 hidden"
+
onclick="togglePatchEdit(false)"
+
>
+
Cancel
+
</button>
+
</div>
+
</form>
+
+
<div id="pull-error" class="error"></div>
+
<div id="pull-success" class="success"></div>
+
</div>
+
<script>
+
function togglePatchEdit(editMode) {
+
const preview = document.getElementById("patch-preview");
+
const editor = document.getElementById("patch");
+
const editBtn = document.getElementById("edit-patch-btn");
+
const saveBtn = document.getElementById("save-patch-btn");
+
const cancelBtn =
+
document.getElementById("cancel-patch-btn");
+
+
if (editMode) {
+
preview.classList.add("hidden");
+
editor.classList.remove("hidden");
+
editBtn.classList.add("hidden");
+
saveBtn.classList.remove("hidden");
+
cancelBtn.classList.remove("hidden");
+
} else {
+
preview.classList.remove("hidden");
+
editor.classList.add("hidden");
+
editBtn.classList.remove("hidden");
+
saveBtn.classList.add("hidden");
+
cancelBtn.classList.add("hidden");
+
}
+
}
+
+
document
+
.getElementById("save-patch-btn")
+
.addEventListener("click", function () {
+
togglePatchEdit(false);
+
});
+
</script>
</details>
</div>
-
-
<div class="mt-4">
-
{{ if .MergeCheck }}
-
<div class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }}bg-red-50 border-red-200{{ else }}bg-green-50 border-green-200{{ end }}">
-
<div class="flex items-center gap-2 rounded-sm {{ if .MergeCheck.IsConflicted }}text-red-500{{ else }}text-green-500 {{ end }}">
-
{{ if .MergeCheck.IsConflicted }}
-
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
-
<span class="font-medium">merge conflicts detected</span>
+
+
{{ if .MergeCheck }}
+
<div class="mt-4" id="merge-check">
+
<div
+
class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }}
+
bg-red-50 border-red-200
{{ else }}
-
<i data-lucide="check-circle" class="w-4 h-4"></i>
-
<span class="font-medium">ready to merge</span>
+
bg-green-50 border-green-200
+
{{ end }}"
+
>
+
<div
+
class="flex items-center gap-2 rounded-sm {{ if .MergeCheck.IsConflicted }}
+
text-red-500
+
{{ else }}
+
text-green-500
+
{{ end }}"
+
>
+
{{ if .MergeCheck.IsConflicted }}
+
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
+
<span class="font-medium"
+
>merge conflicts detected</span
+
>
+
{{ else }}
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
+
<span class="font-medium">ready to merge</span>
+
{{ end }}
+
</div>
+
+
{{ if .MergeCheck.IsConflicted }}
+
<div class="mt-2">
+
<ul class="text-sm space-y-1">
+
{{ range .MergeCheck.Conflicts }}
+
<li class="flex items-center">
+
<i
+
data-lucide="file-warning"
+
class="w-3 h-3 mr-1.5 text-red-500"
+
></i>
+
<span class="font-mono"
+
>{{ slice .Filename 0 (sub (len .Filename) 2) }}</span
+
>
+
</li>
+
{{ end }}
+
</ul>
+
</div>
{{ end }}
</div>
-
-
{{ if .MergeCheck.IsConflicted }}
-
<div class="mt-2">
-
<ul class="text-sm space-y-1">
-
{{ range .MergeCheck.Conflicts }}
-
<li class="flex items-center">
-
<i data-lucide="file-warning" class="w-3 h-3 mr-1.5 text-red-500"></i>
-
<span class="font-mono">{{ slice .Filename 0 (sub (len .Filename) 2) }}</span>
-
</li>
-
{{ end }}
-
</ul>
-
</div>
-
{{ end }}
</div>
-
{{ end }}
-
</div>
+
{{ end }}
{{ end }}
{{ define "repoAfter" }}
···
</div>
</div>
{{ end }}
+
</section>
{{ if .LoggedInUser }}
···
></i>
<span class="text-black">{{ $action }}</span>
</button>
-
<div id="pull-action" class="error"></div>
</form>
{{ end }}
{{ end }}
+36 -14
appview/state/repo.go
···
}
}
-
// MergeCheck gets called async, every time the patch diff is updated in a pull.
-
func (s *State) MergeCheck(w http.ResponseWriter, r *http.Request) {
+
func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
-
s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
+
return
+
}
+
+
prId := chi.URLParam(r, "pull")
+
prIdInt, err := strconv.Atoi(prId)
+
if err != nil {
+
http.Error(w, "bad pr id", http.StatusBadRequest)
+
log.Println("failed to parse pr id", err)
return
}
patch := r.FormValue("patch")
-
targetBranch := r.FormValue("targetBranch")
+
if patch == "" {
+
s.pages.Notice(w, "pull-error", "Patch is required.")
+
return
+
}
-
if patch == "" || targetBranch == "" {
-
s.pages.Notice(w, "pull", "Patch and target branch are required.")
+
err = db.EditPatch(s.db, f.RepoAt, prIdInt, patch)
+
if err != nil {
+
log.Println("failed to update patch", err)
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
// Get target branch after patch update
+
pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
+
if err != nil {
+
log.Println("failed to get pull information", err)
+
s.pages.Notice(w, "pull-success", "Patch updated successfully.")
+
return
+
}
+
+
targetBranch := pull.TargetBranch
+
+
// Perform merge check
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+
s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s", f.Knot)
-
s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+
s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
return
}
resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch)
if err != nil {
log.Println("failed to check mergeability", err)
-
s.pages.Notice(w, "pull", "Unable to check for mergeability. Try again later.")
+
s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
return
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("failed to read knotserver response body")
-
s.pages.Notice(w, "pull", "Unable to check for mergeability. Try again later.")
+
s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
return
}
···
err = json.Unmarshal(respBody, &mergeCheckResponse)
if err != nil {
log.Println("failed to unmarshal merge check response", err)
-
s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+
s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.")
return
}
-
// TODO: this has to return a html fragment
-
w.Header().Set("Content-Type", "application/json")
-
json.NewEncoder(w).Encode(mergeCheckResponse)
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt))
+
return
}
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
+1
appview/state/router.go
···
r.Use(AuthMiddleware(s))
r.Get("/new", s.NewPull)
r.Post("/new", s.NewPull)
+
r.Patch("/{pull}/patch", s.EditPatch)
// r.Post("/{pull}/comment", s.PullComment)
// r.Post("/{pull}/close", s.ClosePull)
// r.Post("/{pull}/reopen", s.ReopenPull)
+4 -4
input.css
···
@layer base {
html {
-
letter-spacing: -0.01em;
-
word-spacing: -0.07em;
-
font-size: 14px;
+
letter-spacing: -0.01em;
+
word-spacing: -0.07em;
+
font-size: 14px;
}
a {
@apply no-underline text-black hover:underline hover:text-gray-800;
···
@apply py-1 text-red-400;
}
.success {
-
@apply py-1 text-black;
+
@apply py-1 text-green-400;
}
}
}