forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+13 -3
appview/db/pulls.go
···
return len(p.Submissions) - 1
}
-
func (p *Pull) IsSameRepoBranch() bool {
+
func (p *Pull) IsPatchBased() bool {
+
return p.PullSource == nil
+
}
+
+
func (p *Pull) IsBranchBased() bool {
if p.PullSource != nil {
if p.PullSource.RepoAt != nil {
return p.PullSource.RepoAt == &p.RepoAt
···
return false
}
-
func (p *Pull) IsPatch() bool {
-
return p.PullSource == nil
+
func (p *Pull) IsForkBased() bool {
+
if p.PullSource != nil {
+
if p.PullSource.RepoAt != nil {
+
// make sure repos are different
+
return p.PullSource.RepoAt != &p.RepoAt
+
}
+
}
+
return false
}
func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
+4
appview/pages/funcmap.go
···
"add": func(a, b int) int {
return a + b
},
+
// the absolute state of go templates
+
"add64": func(a, b int64) int64 {
+
return a + b
+
},
"sub": func(a, b int) int {
return a - b
},
+80 -41
appview/pages/pages.go
···
func NewPages() *Pages {
templates := make(map[string]*template.Template)
-
// Walk through embedded templates directory and parse all .html files
+
var fragmentPaths []string
+
// First, collect all fragment paths
err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
-
if !d.IsDir() && strings.HasSuffix(path, ".html") {
-
name := strings.TrimPrefix(path, "templates/")
-
name = strings.TrimSuffix(name, ".html")
+
if d.IsDir() {
+
return nil
+
}
+
+
if !strings.HasSuffix(path, ".html") {
+
return nil
+
}
+
+
if !strings.Contains(path, "fragments/") {
+
return nil
+
}
+
+
name := strings.TrimPrefix(path, "templates/")
+
name = strings.TrimSuffix(name, ".html")
+
+
tmpl, err := template.New(name).
+
Funcs(funcMap()).
+
ParseFS(Files, path)
+
if err != nil {
+
log.Fatalf("setting up fragment: %v", err)
+
}
+
+
templates[name] = tmpl
+
fragmentPaths = append(fragmentPaths, path)
+
log.Printf("loaded fragment: %s", name)
+
return nil
+
})
+
if err != nil {
+
log.Fatalf("walking template dir for fragments: %v", err)
+
}
-
// add fragments as templates
-
if strings.HasPrefix(path, "templates/fragments/") {
-
tmpl, err := template.New(name).
-
Funcs(funcMap()).
-
ParseFS(Files, path)
-
if err != nil {
-
return fmt.Errorf("setting up fragment: %w", err)
-
}
+
// Then walk through and setup the rest of the templates
+
err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error {
+
if err != nil {
+
return err
+
}
-
templates[name] = tmpl
-
log.Printf("loaded fragment: %s", name)
-
}
+
if d.IsDir() {
+
return nil
+
}
-
// layouts and fragments are applied first
-
if !strings.HasPrefix(path, "templates/layouts/") &&
-
!strings.HasPrefix(path, "templates/fragments/") {
-
// Add the page template on top of the base
-
tmpl, err := template.New(name).
-
Funcs(funcMap()).
-
ParseFS(Files, "templates/layouts/*.html", "templates/fragments/*.html", path)
-
if err != nil {
-
return fmt.Errorf("setting up template: %w", err)
-
}
+
if !strings.HasSuffix(path, "html") {
+
return nil
+
}
-
templates[name] = tmpl
-
log.Printf("loaded template: %s", name)
-
}
+
// Skip fragments as they've already been loaded
+
if strings.Contains(path, "fragments/") {
+
return nil
+
}
+
// Skip layouts
+
if strings.Contains(path, "layouts/") {
return nil
}
+
+
name := strings.TrimPrefix(path, "templates/")
+
name = strings.TrimSuffix(name, ".html")
+
+
// Add the page template on top of the base
+
allPaths := []string{}
+
allPaths = append(allPaths, "templates/layouts/*.html")
+
allPaths = append(allPaths, fragmentPaths...)
+
allPaths = append(allPaths, path)
+
tmpl, err := template.New(name).
+
Funcs(funcMap()).
+
ParseFS(Files, allPaths...)
+
if err != nil {
+
return fmt.Errorf("setting up template: %w", err)
+
}
+
+
templates[name] = tmpl
+
log.Printf("loaded template: %s", name)
return nil
})
if err != nil {
···
}
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
-
return p.executePlain("fragments/follow", w, params)
+
return p.executePlain("user/fragments/follow", w, params)
}
type RepoActionsFragmentParams struct {
···
}
func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error {
-
return p.executePlain("fragments/repoActions", w, params)
+
return p.executePlain("repo/fragments/repoActions", w, params)
}
type RepoDescriptionParams struct {
···
}
func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
-
return p.executePlain("fragments/editRepoDescription", w, params)
+
return p.executePlain("repo/fragments/editRepoDescription", w, params)
}
func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {
-
return p.executePlain("fragments/repoDescription", w, params)
+
return p.executePlain("repo/fragments/repoDescription", w, params)
}
type RepoInfo struct {
···
}
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
-
return p.executePlain("fragments/editIssueComment", w, params)
+
return p.executePlain("repo/issues/fragments/editIssueComment", w, params)
}
type SingleIssueCommentParams struct {
···
}
func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error {
-
return p.executePlain("fragments/issueComment", w, params)
+
return p.executePlain("repo/issues/fragments/issueComment", w, params)
}
type RepoNewPullParams struct {
···
}
func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error {
-
return p.executePlain("fragments/pullPatchUpload", w, params)
+
return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params)
}
type PullCompareBranchesParams struct {
···
}
func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error {
-
return p.executePlain("fragments/pullCompareBranches", w, params)
+
return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params)
}
type PullCompareForkParams struct {
···
}
func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error {
-
return p.executePlain("fragments/pullCompareForks", w, params)
+
return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params)
}
type PullCompareForkBranchesParams struct {
···
}
func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error {
-
return p.executePlain("fragments/pullCompareForksBranches", w, params)
+
return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params)
}
type PullResubmitParams struct {
···
}
func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
-
return p.executePlain("fragments/pullResubmit", w, params)
+
return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
}
type PullActionsParams struct {
···
}
func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
-
return p.executePlain("fragments/pullActions", w, params)
+
return p.executePlain("repo/pulls/fragments/pullActions", w, params)
}
type PullNewCommentParams struct {
···
}
func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
-
return p.executePlain("fragments/pullNewComment", w, params)
+
return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
}
func (p *Pages) Static() http.Handler {
-33
appview/pages/templates/fragments/cloneInstructions.html
···
-
{{ define "fragments/cloneInstructions" }}
-
<section class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4">
-
<div class="flex flex-col gap-2">
-
<strong>push</strong>
-
<div class="md:pl-4 overflow-x-auto whitespace-nowrap">
-
<code class="dark:text-gray-100">git remote add origin git@{{.RepoInfo.Knot}}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code>
-
</div>
-
</div>
-
-
<div class="flex flex-col gap-2">
-
<strong>clone</strong>
-
<div class="md:pl-4 flex flex-col gap-2">
-
-
<div class="flex items-center gap-3">
-
<span class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white">HTTP</span>
-
<div class="overflow-x-auto whitespace-nowrap flex-1">
-
<code class="dark:text-gray-100">git clone https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code>
-
</div>
-
</div>
-
-
<div class="flex items-center gap-3">
-
<span class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white">SSH</span>
-
<div class="overflow-x-auto whitespace-nowrap flex-1">
-
<code class="dark:text-gray-100">git clone git@{{.RepoInfo.Knot}}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code>
-
</div>
-
</div>
-
</div>
-
</div>
-
-
-
<p class="py-2 text-gray-500 dark:text-gray-400">Note that for self-hosted knots, clone URLs may be different based on your setup.</p>
-
</section>
-
{{ end }}
-116
appview/pages/templates/fragments/diff.html
···
-
{{ define "fragments/diff" }}
-
{{ $repo := index . 0 }}
-
{{ $diff := index . 1 }}
-
{{ $commit := $diff.Commit }}
-
{{ $stat := $diff.Stat }}
-
{{ $diff := $diff.Diff }}
-
-
{{ $this := $commit.This }}
-
{{ $parent := $commit.Parent }}
-
-
{{ $last := sub (len $diff) 1 }}
-
{{ range $idx, $hunk := $diff }}
-
{{ with $hunk }}
-
<section class="mt-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
-
<div id="file-{{ .Name.New }}">
-
<div id="diff-file">
-
<details open>
-
<summary class="list-none cursor-pointer sticky top-0">
-
<div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between">
-
<div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto" style="direction: rtl;">
-
{{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }}
-
-
<div class="flex gap-2 items-center" style="direction: ltr;">
-
{{ if .IsNew }}
-
<span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span>
-
{{ else if .IsDelete }}
-
<span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">DELETED</span>
-
{{ else if .IsCopy }}
-
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">COPIED</span>
-
{{ else if .IsRename }}
-
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">RENAMED</span>
-
{{ else }}
-
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">MODIFIED</span>
-
{{ end }}
-
-
{{ if .IsDelete }}
-
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}>
-
{{ .Name.Old }}
-
</a>
-
{{ else if (or .IsCopy .IsRename) }}
-
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $parent}}href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}"{{end}}>
-
{{ .Name.Old }}
-
</a>
-
{{ i "arrow-right" "w-4 h-4" }}
-
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}>
-
{{ .Name.New }}
-
</a>
-
{{ else }}
-
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}>
-
{{ .Name.New }}
-
</a>
-
{{ end }}
-
</div>
-
</div>
-
-
{{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }}
-
<div id="right-side-items" class="p-2 flex items-center">
-
<a title="top of file" href="#file-{{ .Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a>
-
{{ if gt $idx 0 }}
-
{{ $prev := index $diff (sub $idx 1) }}
-
<a title="previous file" href="#file-{{ $prev.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a>
-
{{ end }}
-
-
{{ if lt $idx $last }}
-
{{ $next := index $diff (add $idx 1) }}
-
<a title="next file" href="#file-{{ $next.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a>
-
{{ end }}
-
</div>
-
-
</div>
-
</summary>
-
-
<div class="transition-all duration-700 ease-in-out">
-
{{ if .IsDelete }}
-
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
-
This file has been deleted in this commit.
-
</p>
-
{{ else }}
-
{{ if .IsBinary }}
-
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
-
This is a binary file and will not be displayed.
-
</p>
-
{{ else }}
-
<pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none">{{- .Header -}}</div>{{- range .Lines -}}
-
{{- if eq .Op.String "+" -}}
-
<div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full">
-
<div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div>
-
<div class="p-1 whitespace-pre">{{ .Line }}</div>
-
</div>
-
{{- end -}}
-
{{- if eq .Op.String "-" -}}
-
<div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full">
-
<div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div>
-
<div class="p-1 whitespace-pre">{{ .Line }}</div>
-
</div>
-
{{- end -}}
-
{{- if eq .Op.String " " -}}
-
<div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full">
-
<div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div>
-
<div class="p-1 whitespace-pre">{{ .Line }}</div>
-
</div>
-
{{- end -}}
-
{{- end -}}
-
{{- end -}}</div></div></pre>
-
{{- end -}}
-
{{ end }}
-
</div>
-
-
</details>
-
-
</div>
-
</div>
-
</section>
-
{{ end }}
-
{{ end }}
-
{{ end }}
-52
appview/pages/templates/fragments/editIssueComment.html
···
-
{{ define "fragments/editIssueComment" }}
-
{{ with .Comment }}
-
<div id="comment-container-{{.CommentId}}">
-
<div class="flex items-center gap-2 mb-2 text-gray-500 text-sm">
-
{{ $owner := didOrHandle $.LoggedInUser.Did $.LoggedInUser.Handle }}
-
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
-
-
<!-- show user "hats" -->
-
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
-
{{ if $isIssueAuthor }}
-
<span class="before:content-['ยท']"></span>
-
<span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center">
-
author
-
</span>
-
{{ end }}
-
-
<span class="before:content-['ยท']"></span>
-
<a
-
href="#{{ .CommentId }}"
-
class="text-gray-500 hover:text-gray-500 hover:underline no-underline"
-
id="{{ .CommentId }}">
-
{{ .Created | timeFmt }}
-
</a>
-
-
<button
-
class="btn px-2 py-1 flex items-center gap-2 text-sm"
-
hx-post="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
-
hx-include="#edit-textarea-{{ .CommentId }}"
-
hx-target="#comment-container-{{ .CommentId }}"
-
hx-swap="outerHTML">
-
{{ i "check" "w-4 h-4" }}
-
</button>
-
<button
-
class="btn px-2 py-1 flex items-center gap-2 text-sm"
-
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
-
hx-target="#comment-container-{{ .CommentId }}"
-
hx-swap="outerHTML">
-
{{ i "x" "w-4 h-4" }}
-
</button>
-
<span id="comment-{{.CommentId}}-status"></span>
-
</div>
-
-
<div>
-
<textarea
-
id="edit-textarea-{{ .CommentId }}"
-
name="body"
-
class="w-full p-2 border rounded min-h-[100px]">{{ .Body }}</textarea>
-
</div>
-
</div>
-
{{ end }}
-
{{ end }}
-
-11
appview/pages/templates/fragments/editRepoDescription.html
···
-
{{ define "fragments/editRepoDescription" }}
-
<form hx-put="/{{ .RepoInfo.FullName }}/description" hx-target="this" hx-swap="outerHTML" class="flex flex-wrap gap-2">
-
<input type="text" class="p-1" name="description" value="{{ .RepoInfo.Description }}">
-
<button type="submit" class="btn p-2 flex items-center gap-2 no-underline text-sm">
-
{{ i "check" "w-3 h-3" }} save
-
</button>
-
<button type="button" class="btn p-2 flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description" >
-
{{ i "x" "w-3 h-3" }} cancel
-
</button>
-
</form>
-
{{ end }}
-17
appview/pages/templates/fragments/follow.html
···
-
{{ define "fragments/follow" }}
-
<button id="followBtn"
-
class="btn mt-2 w-full"
-
-
{{ if eq .FollowStatus.String "IsNotFollowing" }}
-
hx-post="/follow?subject={{.UserDid}}"
-
{{ else }}
-
hx-delete="/follow?subject={{.UserDid}}"
-
{{ end }}
-
-
hx-trigger="click"
-
hx-target="#followBtn"
-
hx-swap="outerHTML"
-
>
-
{{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
-
</button>
-
{{ end }}
-60
appview/pages/templates/fragments/issueComment.html
···
-
{{ define "fragments/issueComment" }}
-
{{ with .Comment }}
-
<div id="comment-container-{{.CommentId}}">
-
<div class="flex items-center gap-2 mb-2 text-gray-500 text-sm">
-
{{ $owner := index $.DidHandleMap .OwnerDid }}
-
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
-
-
<!-- show user "hats" -->
-
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
-
{{ if $isIssueAuthor }}
-
<span class="before:content-['ยท']"></span>
-
<span class="rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
-
author
-
</span>
-
{{ end }}
-
-
<span class="before:content-['ยท']"></span>
-
<a
-
href="#{{ .CommentId }}"
-
class="text-gray-500 hover:text-gray-500 hover:underline no-underline"
-
id="{{ .CommentId }}">
-
{{ if .Deleted }}
-
deleted {{ .Deleted | timeFmt }}
-
{{ else if .Edited }}
-
edited {{ .Edited | timeFmt }}
-
{{ else }}
-
{{ .Created | timeFmt }}
-
{{ end }}
-
</a>
-
-
{{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }}
-
{{ if and $isCommentOwner (not .Deleted) }}
-
<button
-
class="btn px-2 py-1 text-sm"
-
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
-
hx-swap="outerHTML"
-
hx-target="#comment-container-{{.CommentId}}"
-
>
-
{{ i "pencil" "w-4 h-4" }}
-
</button>
-
<button
-
class="btn px-2 py-1 text-sm text-red-500"
-
hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
-
hx-confirm="Are you sure you want to delete your comment?"
-
hx-swap="outerHTML"
-
hx-target="#comment-container-{{.CommentId}}"
-
>
-
{{ i "trash-2" "w-4 h-4" }}
-
</button>
-
{{ end }}
-
-
</div>
-
{{ if not .Deleted }}
-
<div class="prose dark:prose-invert">
-
{{ .Body | markdown }}
-
</div>
-
{{ end }}
-
</div>
-
{{ end }}
-
{{ end }}
-91
appview/pages/templates/fragments/pullActions.html
···
-
{{ define "fragments/pullActions" }}
-
{{ $lastIdx := sub (len .Pull.Submissions) 1 }}
-
{{ $roundNumber := .RoundNumber }}
-
-
{{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }}
-
{{ $isMerged := .Pull.State.IsMerged }}
-
{{ $isClosed := .Pull.State.IsClosed }}
-
{{ $isOpen := .Pull.State.IsOpen }}
-
{{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }}
-
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
-
{{ $isLastRound := eq $roundNumber $lastIdx }}
-
{{ $isSameRepoBranch := .Pull.IsSameRepoBranch }}
-
{{ $isUpToDate := .ResubmitCheck.No }}
-
<div class="relative w-fit">
-
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
-
<div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2">
-
<button
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment"
-
hx-target="#actions-{{$roundNumber}}"
-
hx-swap="outerHtml"
-
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline">
-
{{ i "message-square-plus" "w-4 h-4" }}
-
<span>comment</span>
-
</button>
-
{{ if and $isPushAllowed $isOpen $isLastRound }}
-
{{ $disabled := "" }}
-
{{ if $isConflicted }}
-
{{ $disabled = "disabled" }}
-
{{ end }}
-
<button
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
-
hx-swap="none"
-
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
-
class="btn p-2 flex items-center gap-2" {{ $disabled }}>
-
{{ i "git-merge" "w-4 h-4" }}
-
<span>merge</span>
-
</button>
-
{{ end }}
-
-
{{ if and $isPullAuthor $isOpen $isLastRound }}
-
{{ $disabled := "" }}
-
{{ if $isUpToDate }}
-
{{ $disabled = "disabled" }}
-
{{ end }}
-
<button id="resubmitBtn"
-
{{ if not .Pull.IsPatch }}
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
-
{{ else }}
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
-
hx-target="#actions-{{$roundNumber}}"
-
hx-swap="outerHtml"
-
{{ end }}
-
-
hx-disabled-elt="#resubmitBtn"
-
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }}
-
-
{{ if $disabled }}
-
title="Update this branch to resubmit this pull request"
-
{{ else }}
-
title="Resubmit this pull request"
-
{{ end }}
-
>
-
{{ i "rotate-ccw" "w-4 h-4" }}
-
<span>resubmit</span>
-
</button>
-
{{ end }}
-
-
{{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }}
-
<button
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close"
-
hx-swap="none"
-
class="btn p-2 flex items-center gap-2">
-
{{ i "ban" "w-4 h-4" }}
-
<span>close</span>
-
</button>
-
{{ end }}
-
-
{{ if and (or $isPullAuthor $isPushAllowed) $isClosed $isLastRound }}
-
<button
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen"
-
hx-swap="none"
-
class="btn p-2 flex items-center gap-2">
-
{{ i "circle-dot" "w-4 h-4" }}
-
<span>reopen</span>
-
</button>
-
{{ end }}
-
</div>
-
</div>
-
{{ end }}
-
-
-20
appview/pages/templates/fragments/pullCompareBranches.html
···
-
{{ define "fragments/pullCompareBranches" }}
-
<div id="patch-upload">
-
<label for="targetBranch" class="dark:text-white"
-
>select a branch</label
-
>
-
<div class="flex flex-wrap gap-2 items-center">
-
<select
-
name="sourceBranch"
-
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
>
-
<option disabled selected>source branch</option>
-
{{ range .Branches }}
-
<option value="{{ .Reference.Name }}" class="py-1">
-
{{ .Reference.Name }}
-
</option>
-
{{ end }}
-
</select>
-
</div>
-
</div>
-
{{ end }}
-42
appview/pages/templates/fragments/pullCompareForks.html
···
-
{{ define "fragments/pullCompareForks" }}
-
<div id="patch-upload">
-
<label for="forkSelect" class="dark:text-white"
-
>select a fork to compare</label
-
>
-
<div class="flex flex-wrap gap-4 items-center mb-4">
-
<div class="flex flex-wrap gap-2 items-center">
-
<select
-
id="forkSelect"
-
name="fork"
-
required
-
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
hx-get="/{{ $.RepoInfo.FullName }}/pulls/new/fork-branches"
-
hx-target="#branch-selection"
-
hx-vals='{"fork": this.value}'
-
hx-swap="innerHTML"
-
onchange="document.getElementById('hiddenForkInput').value = this.value;"
-
>
-
<option disabled selected>select a fork</option>
-
{{ range .Forks }}
-
<option value="{{ .Name }}" class="py-1">
-
{{ .Name }}
-
</option>
-
{{ end }}
-
</select>
-
-
<input
-
type="hidden"
-
id="hiddenForkInput"
-
name="fork"
-
value=""
-
/>
-
</div>
-
-
<div id="branch-selection">
-
<div class="text-sm text-gray-500 dark:text-gray-400">
-
Select a fork first to view available branches
-
</div>
-
</div>
-
</div>
-
</div>
-
{{ end }}
-15
appview/pages/templates/fragments/pullCompareForksBranches.html
···
-
{{ define "fragments/pullCompareForksBranches" }}
-
<div class="flex flex-wrap gap-2 items-center">
-
<select
-
name="sourceBranch"
-
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
>
-
<option disabled selected>source branch</option>
-
{{ range .SourceBranches }}
-
<option value="{{ .Reference.Name }}" class="py-1">
-
{{ .Reference.Name }}
-
</option>
-
{{ end }}
-
</select>
-
</div>
-
{{ end }}
-32
appview/pages/templates/fragments/pullNewComment.html
···
-
{{ define "fragments/pullNewComment" }}
-
<div
-
id="pull-comment-card-{{ .RoundNumber }}"
-
class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2">
-
<div class="text-sm text-gray-500 dark:text-gray-400">
-
{{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }}
-
</div>
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment"
-
hx-swap="none"
-
class="w-full flex flex-wrap gap-2">
-
<textarea
-
name="body"
-
class="w-full p-2 rounded border border-gray-200"
-
placeholder="Add to the discussion..."></textarea>
-
<button type="submit" class="btn flex items-center gap-2">
-
{{ i "message-square" "w-4 h-4" }} comment
-
</button>
-
<button
-
type="button"
-
class="btn flex items-center gap-2"
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions"
-
hx-swap="outerHTML"
-
hx-target="#pull-comment-card-{{ .RoundNumber }}">
-
{{ i "x" "w-4 h-4" }}
-
<span>cancel</span>
-
</button>
-
<div id="pull-comment"></div>
-
</form>
-
</div>
-
{{ end }}
-
-14
appview/pages/templates/fragments/pullPatchUpload.html
···
-
{{ define "fragments/pullPatchUpload" }}
-
<div id="patch-upload">
-
<textarea
-
name="patch"
-
id="patch"
-
rows="12"
-
class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
placeholder="diff --git a/file.txt b/file.txt
-
index 1234567..abcdefg 100644
-
--- a/file.txt
-
+++ b/file.txt"
-
></textarea>
-
</div>
-
{{ end }}
-52
appview/pages/templates/fragments/pullResubmit.html
···
-
{{ define "fragments/pullResubmit" }}
-
<div
-
id="resubmit-pull-card"
-
class="rounded relative border bg-amber-50 dark:bg-amber-900 border-amber-200 dark:border-amber-500 px-6 py-2">
-
-
<div class="flex items-center gap-2 text-amber-500 dark:text-amber-50">
-
{{ i "pencil" "w-4 h-4" }}
-
<span class="font-medium">resubmit your patch</span>
-
</div>
-
-
<div class="mt-2 text-sm text-gray-700 dark:text-gray-200">
-
You can update this patch to address any reviews.
-
This will begin a new round of reviews,
-
but you'll still be able to view your previous submissions and feedback.
-
</div>
-
-
<div class="mt-4 flex flex-col">
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
-
hx-swap="none"
-
class="w-full flex flex-wrap gap-2">
-
<textarea
-
name="patch"
-
class="w-full p-2 mb-2"
-
placeholder="Paste your updated patch here."
-
rows="15"
-
>{{.Pull.LatestPatch}}</textarea>
-
<button
-
type="submit"
-
class="btn flex items-center gap-2"
-
{{ if or .Pull.State.IsClosed }}
-
disabled
-
{{ end }}>
-
{{ i "rotate-ccw" "w-4 h-4" }}
-
<span>resubmit</span>
-
</button>
-
<button
-
type="button"
-
class="btn flex items-center gap-2"
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .Pull.LastRoundNumber }}/actions"
-
hx-swap="outerHTML"
-
hx-target="#resubmit-pull-card">
-
{{ i "x" "w-4 h-4" }}
-
<span>cancel</span>
-
</button>
-
</form>
-
-
<div id="resubmit-error" class="error"></div>
-
<div id="resubmit-success" class="success"></div>
-
</div>
-
</div>
-
{{ end }}
-41
appview/pages/templates/fragments/repoActions.html
···
-
{{ define "fragments/repoActions" }}
-
<div class="flex items-center gap-2 z-auto">
-
<button id="starBtn"
-
class="btn disabled:opacity-50 disabled:cursor-not-allowed"
-
-
{{ if .IsStarred }}
-
hx-delete="/star?subject={{.RepoAt}}&countHint={{.Stats.StarCount}}"
-
{{ else }}
-
hx-post="/star?subject={{.RepoAt}}&countHint={{.Stats.StarCount}}"
-
{{ end }}
-
-
hx-trigger="click"
-
hx-target="#starBtn"
-
hx-swap="outerHTML"
-
hx-disabled-elt="#starBtn"
-
>
-
<div class="flex gap-2 items-center">
-
{{ if .IsStarred }}
-
{{ i "star" "w-4 h-4 fill-current" }}
-
{{ else }}
-
{{ i "star" "w-4 h-4" }}
-
{{ end }}
-
<span>
-
{{ .Stats.StarCount }}
-
</span>
-
</div>
-
</button>
-
{{ if .DisableFork }}
-
<button class="btn no-underline hover:no-underline flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" disabled title="Empty repositories cannot be forked">
-
{{ i "git-fork" "w-4 h-4"}}
-
fork
-
</button>
-
{{ else }}
-
<a class="btn no-underline hover:no-underline flex items-center gap-2" href="/{{ .FullName }}/fork">
-
{{ i "git-fork" "w-4 h-4"}}
-
fork
-
</a>
-
{{ end }}
-
</div>
-
{{ end }}
-
-15
appview/pages/templates/fragments/repoDescription.html
···
-
{{ define "fragments/repoDescription" }}
-
<span id="repo-description" class="flex flex-wrap items-center gap-2" hx-target="this" hx-swap="outerHTML">
-
{{ if .RepoInfo.Description }}
-
{{ .RepoInfo.Description }}
-
{{ else }}
-
<span class="italic">this repo has no description</span>
-
{{ end }}
-
-
{{ if .RepoInfo.Roles.IsOwner }}
-
<button class="btn p-2 flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description/edit">
-
{{ i "pencil" "w-3 h-3" }} edit
-
</button>
-
{{ end }}
-
</span>
-
{{ end }}
+2 -2
appview/pages/templates/layouts/repobase.html
···
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
</div>
-
{{ template "fragments/repoActions" .RepoInfo }}
+
{{ template "repo/fragments/repoActions" .RepoInfo }}
</div>
-
{{ template "fragments/repoDescription" . }}
+
{{ template "repo/fragments/repoDescription" . }}
</section>
<section class="min-h-screen flex flex-col drop-shadow-sm">
<nav class="w-full pl-4 overflow-auto">
+1 -1
appview/pages/templates/repo/blob.html
···
{{ else }}
<div class="overflow-auto relative">
{{ if .ShowRendered }}
-
<div id="blob-contents" class="prose dark:prose-invert p-6">{{ .RenderedContents }}</div>
+
<div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div>
{{ else }}
<div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ $.Contents | escapeHtml }}</div>
{{ end }}
+2 -21
appview/pages/templates/repo/commit.html
···
{{ $repo := .RepoInfo.FullName }}
{{ $commit := .Diff.Commit }}
-
{{ $stat := .Diff.Stat }}
-
{{ $diff := .Diff.Diff }}
<section class="commit dark:text-white">
<div id="commit-message">
···
<div>
<p class="pb-2">{{ index $messageParts 0 }}</p>
{{ if gt (len $messageParts) 1 }}
-
<p class="mt-1 cursor-text pb-2 text-sm">{{ nl2br (unwrapText (index $messageParts 1)) }}</p>
+
<p class="mt-1 cursor-text pb-2 text-sm">{{ nl2br (index $messageParts 1) }}</p>
{{ end }}
</div>
</div>
···
{{ end }}
<span class="px-1 select-none before:content-['\00B7']"></span>
{{ timeFmt $commit.Author.When }}
-
<span class="px-1 select-none before:content-['\00B7']"></span>
-
<span>{{ $stat.FilesChanged }}</span> files <span class="font-mono">(+{{ $stat.Insertions }}, -{{ $stat.Deletions }})</span>
<span class="px-1 select-none before:content-['\00B7']"></span>
</p>
···
</p>
</div>
-
<div class="diff-stat">
-
<br>
-
<strong class="text-sm uppercase mb-4 dark:text-gray-200">Changed files</strong>
-
<div class="overflow-x-auto">
-
{{ range $diff }}
-
<ul class="dark:text-gray-200">
-
{{ if .IsDelete }}
-
<li><a href="#file-{{ .Name.Old }}" class="dark:hover:text-gray-300">{{ .Name.Old }}</a></li>
-
{{ else }}
-
<li><a href="#file-{{ .Name.New }}" class="dark:hover:text-gray-300">{{ .Name.New }}</a></li>
-
{{ end }}
-
</ul>
-
{{ end }}
-
</div>
-
</div>
</section>
{{end}}
{{ define "repoAfter" }}
-
{{ template "fragments/diff" (list .RepoInfo.FullName .Diff) }}
+
{{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }}
{{end}}
+1 -1
appview/pages/templates/repo/empty.html
···
{{ end }}
{{ define "repoAfter" }}
-
{{ template "fragments/cloneInstructions" . }}
+
{{ template "repo/fragments/cloneInstructions" . }}
{{ end }}
+51
appview/pages/templates/repo/fragments/cloneInstructions.html
···
+
{{ define "repo/fragments/cloneInstructions" }}
+
<section
+
class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4"
+
>
+
<div class="flex flex-col gap-2">
+
<strong>push</strong>
+
<div class="md:pl-4 overflow-x-auto whitespace-nowrap">
+
<code class="dark:text-gray-100"
+
>git remote add origin
+
git@{{ .RepoInfo.Knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code
+
>
+
</div>
+
</div>
+
+
<div class="flex flex-col gap-2">
+
<strong>clone</strong>
+
<div class="md:pl-4 flex flex-col gap-2">
+
<div class="flex items-center gap-3">
+
<span
+
class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white"
+
>HTTP</span
+
>
+
<div class="overflow-x-auto whitespace-nowrap flex-1">
+
<code class="dark:text-gray-100"
+
>git clone
+
https://tangled.sh/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code
+
>
+
</div>
+
</div>
+
+
<div class="flex items-center gap-3">
+
<span
+
class="bg-gray-100 dark:bg-gray-700 p-1 mr-1 font-mono text-sm rounded select-none dark:text-white"
+
>SSH</span
+
>
+
<div class="overflow-x-auto whitespace-nowrap flex-1">
+
<code class="dark:text-gray-100"
+
>git clone
+
git@{{ .RepoInfo.Knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code
+
>
+
</div>
+
</div>
+
</div>
+
</div>
+
+
<p class="py-2 text-gray-500 dark:text-gray-400">
+
Note that for self-hosted knots, clone URLs may be different based
+
on your setup.
+
</p>
+
</section>
+
{{ end }}
+175
appview/pages/templates/repo/fragments/diff.html
···
+
{{ define "repo/fragments/diff" }}
+
{{ $repo := index . 0 }}
+
{{ $diff := index . 1 }}
+
{{ $commit := $diff.Commit }}
+
{{ $stat := $diff.Stat }}
+
{{ $diff := $diff.Diff }}
+
+
{{ $this := $commit.This }}
+
{{ $parent := $commit.Parent }}
+
+
<section class="mt-6 p-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
+
<div class="diff-stat">
+
<div class="flex gap-2 items-center">
+
<strong class="text-sm uppercase dark:text-gray-200">Changed files</strong>
+
{{ block "statPill" $stat }} {{ end }}
+
</div>
+
<div class="overflow-x-auto">
+
{{ range $diff }}
+
<ul class="dark:text-gray-200">
+
{{ if .IsDelete }}
+
<li><a href="#file-{{ .Name.Old }}" class="dark:hover:text-gray-300">{{ .Name.Old }}</a></li>
+
{{ else }}
+
<li><a href="#file-{{ .Name.New }}" class="dark:hover:text-gray-300">{{ .Name.New }}</a></li>
+
{{ end }}
+
</ul>
+
{{ end }}
+
</div>
+
</div>
+
</section>
+
+
{{ $last := sub (len $diff) 1 }}
+
{{ range $idx, $hunk := $diff }}
+
{{ with $hunk }}
+
<section class="mt-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
+
<div id="file-{{ .Name.New }}">
+
<div id="diff-file">
+
<details open>
+
<summary class="list-none cursor-pointer sticky top-0">
+
<div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between">
+
<div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto" style="direction: rtl;">
+
{{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }}
+
+
<div class="flex gap-2 items-center" style="direction: ltr;">
+
{{ if .IsNew }}
+
<span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span>
+
{{ else if .IsDelete }}
+
<span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">DELETED</span>
+
{{ else if .IsCopy }}
+
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">COPIED</span>
+
{{ else if .IsRename }}
+
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">RENAMED</span>
+
{{ else }}
+
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">MODIFIED</span>
+
{{ end }}
+
+
{{ block "statPill" .Stats }} {{ end }}
+
+
{{ if .IsDelete }}
+
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}>
+
{{ .Name.Old }}
+
</a>
+
{{ else if (or .IsCopy .IsRename) }}
+
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $parent}}href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}"{{end}}>
+
{{ .Name.Old }}
+
</a>
+
{{ i "arrow-right" "w-4 h-4" }}
+
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}>
+
{{ .Name.New }}
+
</a>
+
{{ else }}
+
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}>
+
{{ .Name.New }}
+
</a>
+
{{ end }}
+
</div>
+
</div>
+
+
{{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }}
+
<div id="right-side-items" class="p-2 flex items-center">
+
<a title="top of file" href="#file-{{ .Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a>
+
{{ if gt $idx 0 }}
+
{{ $prev := index $diff (sub $idx 1) }}
+
<a title="previous file" href="#file-{{ $prev.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a>
+
{{ end }}
+
+
{{ if lt $idx $last }}
+
{{ $next := index $diff (add $idx 1) }}
+
<a title="next file" href="#file-{{ $next.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a>
+
{{ end }}
+
</div>
+
+
</div>
+
</summary>
+
+
<div class="transition-all duration-700 ease-in-out">
+
{{ if .IsDelete }}
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
+
This file has been deleted.
+
</p>
+
{{ else if .IsCopy }}
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
+
This file has been copied.
+
</p>
+
{{ else if .IsRename }}
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
+
This file has been renamed.
+
</p>
+
{{ else if .IsBinary }}
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
+
This is a binary file and will not be displayed.
+
</p>
+
{{ else }}
+
{{ $name := .Name.New }}
+
<pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div>
+
{{- $oldStart := .OldPosition -}}
+
{{- $newStart := .NewPosition -}}
+
{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 scroll-mt-10 target:border target:border-amber-500 target:rounded " -}}
+
{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}}
+
{{- $lineNrSepStyle1 := "" -}}
+
{{- $lineNrSepStyle2 := "pr-2" -}}
+
{{- range .Lines -}}
+
{{- if eq .Op.String "+" -}}
+
<div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full items-center">
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div>
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div>
+
<div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div>
+
<div class="px-2">{{ .Line }}</div>
+
</div>
+
{{- $newStart = add64 $newStart 1 -}}
+
{{- end -}}
+
{{- if eq .Op.String "-" -}}
+
<div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full items-center">
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div>
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div>
+
<div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div>
+
<div class="px-2">{{ .Line }}</div>
+
</div>
+
{{- $oldStart = add64 $oldStart 1 -}}
+
{{- end -}}
+
{{- if eq .Op.String " " -}}
+
<div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full items-center">
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div>
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div>
+
<div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div>
+
<div class="px-2">{{ .Line }}</div>
+
</div>
+
{{- $newStart = add64 $newStart 1 -}}
+
{{- $oldStart = add64 $oldStart 1 -}}
+
{{- end -}}
+
{{- end -}}
+
{{- end -}}</div></div></pre>
+
{{- end -}}
+
</div>
+
+
</details>
+
+
</div>
+
</div>
+
</section>
+
{{ end }}
+
{{ end }}
+
{{ end }}
+
+
{{ define "statPill" }}
+
<div class="flex items-center font-mono text-sm">
+
{{ if and .Insertions .Deletions }}
+
<span class="rounded-l p-1 select-none bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400">+{{ .Insertions }}</span>
+
<span class="rounded-r p-1 select-none bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400">-{{ .Deletions }}</span>
+
{{ else if .Insertions }}
+
<span class="rounded p-1 select-none bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400">+{{ .Insertions }}</span>
+
{{ else if .Deletions }}
+
<span class="rounded p-1 select-none bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400">-{{ .Deletions }}</span>
+
{{ end }}
+
</div>
+
{{ end }}
+11
appview/pages/templates/repo/fragments/editRepoDescription.html
···
+
{{ define "repo/fragments/editRepoDescription" }}
+
<form hx-put="/{{ .RepoInfo.FullName }}/description" hx-target="this" hx-swap="outerHTML" class="flex flex-wrap gap-2">
+
<input type="text" class="p-1" name="description" value="{{ .RepoInfo.Description }}">
+
<button type="submit" class="btn p-1 flex items-center gap-2 no-underline text-sm">
+
{{ i "check" "w-3 h-3" }} save
+
</button>
+
<button type="button" class="btn p-1 flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description" >
+
{{ i "x" "w-3 h-3" }} cancel
+
</button>
+
</form>
+
{{ end }}
+47
appview/pages/templates/repo/fragments/repoActions.html
···
+
{{ define "repo/fragments/repoActions" }}
+
<div class="flex items-center gap-2 z-auto">
+
<button
+
id="starBtn"
+
class="btn disabled:opacity-50 disabled:cursor-not-allowed"
+
{{ if .IsStarred }}
+
hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
+
{{ else }}
+
hx-post="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
+
{{ end }}
+
+
hx-trigger="click"
+
hx-target="#starBtn"
+
hx-swap="outerHTML"
+
hx-disabled-elt="#starBtn"
+
>
+
<div class="flex gap-2 items-center">
+
{{ if .IsStarred }}
+
{{ i "star" "w-4 h-4 fill-current" }}
+
{{ else }}
+
{{ i "star" "w-4 h-4" }}
+
{{ end }}
+
<span class="text-sm">
+
{{ .Stats.StarCount }}
+
</span>
+
</div>
+
</button>
+
{{ if .DisableFork }}
+
<button
+
class="btn text-sm no-underline hover:no-underline flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
+
disabled
+
title="Empty repositories cannot be forked"
+
>
+
{{ i "git-fork" "w-4 h-4" }}
+
fork
+
</button>
+
{{ else }}
+
<a
+
class="btn text-sm no-underline hover:no-underline flex items-center gap-2"
+
href="/{{ .FullName }}/fork"
+
>
+
{{ i "git-fork" "w-4 h-4" }}
+
fork
+
</a>
+
{{ end }}
+
</div>
+
{{ end }}
+15
appview/pages/templates/repo/fragments/repoDescription.html
···
+
{{ define "repo/fragments/repoDescription" }}
+
<span id="repo-description" class="flex flex-wrap items-center gap-2 text-sm" hx-target="this" hx-swap="outerHTML">
+
{{ if .RepoInfo.Description }}
+
{{ .RepoInfo.Description }}
+
{{ else }}
+
<span class="italic">this repo has no description</span>
+
{{ end }}
+
+
{{ if .RepoInfo.Roles.IsOwner }}
+
<button class="flex items-center gap-2 no-underline text-sm" hx-get="/{{ .RepoInfo.FullName }}/description/edit">
+
{{ i "pencil" "w-3 h-3" }}
+
</button>
+
{{ end }}
+
</span>
+
{{ end }}
+207 -172
appview/pages/templates/repo/index.html
···
{{ define "title" }}{{ .RepoInfo.FullName }} at {{ .Ref }}{{ end }}
-
{{ define "extrameta" }}
-
<meta name="vcs:clone" content="https://tangled.sh/{{ .RepoInfo.FullName }}"/>
-
<meta name="forge:summary" content="https://tangled.sh/{{ .RepoInfo.FullName }}">
-
<meta name="forge:dir" content="https://tangled.sh/{{ .RepoInfo.FullName }}/tree/{ref}/{path}">
-
<meta name="forge:file" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}">
-
<meta name="forge:line" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}#L{line}">
-
<meta name="go-import" content="tangled.sh/{{ .RepoInfo.FullNameWithoutAt }} git https://tangled.sh/{{ .RepoInfo.FullName }}">
+
<meta
+
name="vcs:clone"
+
content="https://tangled.sh/{{ .RepoInfo.FullName }}"
+
/>
+
<meta
+
name="forge:summary"
+
content="https://tangled.sh/{{ .RepoInfo.FullName }}"
+
/>
+
<meta
+
name="forge:dir"
+
content="https://tangled.sh/{{ .RepoInfo.FullName }}/tree/{ref}/{path}"
+
/>
+
<meta
+
name="forge:file"
+
content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}"
+
/>
+
<meta
+
name="forge:line"
+
content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}#L{line}"
+
/>
+
<meta
+
name="go-import"
+
content="tangled.sh/{{ .RepoInfo.FullNameWithoutAt }} git https://tangled.sh/{{ .RepoInfo.FullName }}"
+
/>
{{ end }}
-
{{ define "repoContent" }}
<main>
-
{{ block "branchSelector" . }} {{ end }}
+
{{ block "branchSelector" . }}{{ end }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
-
{{ block "fileTree" . }} {{ end }}
-
{{ block "commitLog" . }} {{ end }}
+
{{ block "fileTree" . }}{{ end }}
+
{{ block "commitLog" . }}{{ end }}
</div>
</main>
{{ end }}
{{ define "branchSelector" }}
-
<div class="flex justify-between pb-5">
-
<select
-
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
-
class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
-
>
-
<optgroup label="branches" class="bold text-sm">
-
{{ range .Branches }}
-
<option
-
value="{{ .Reference.Name }}"
-
class="py-1"
-
{{ if eq .Reference.Name $.Ref }}
-
selected
-
{{ end }}
-
>
-
{{ .Reference.Name }}
-
</option>
-
{{ end }}
-
</optgroup>
-
<optgroup label="tags" class="bold text-sm">
-
{{ range .Tags }}
-
<option
-
value="{{ .Reference.Name }}"
-
class="py-1"
-
{{ if eq .Reference.Name $.Ref }}
-
selected
-
{{ end }}
-
>
-
{{ .Reference.Name }}
-
</option>
-
{{ else }}
-
<option class="py-1" disabled>no tags found</option>
-
{{ end }}
-
</optgroup>
-
</select>
-
<a
-
href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}"
-
class="ml-2 no-underline flex items-center gap-2 text-sm uppercase font-bold dark:text-white"
-
>
-
{{ i "logs" "w-4 h-4" }}
-
{{ .TotalCommits }}
-
{{ if eq .TotalCommits 1 }}commit{{ else }}commits{{ end }}
-
</a>
-
</div>
+
<div class="flex justify-between pb-5">
+
<select
+
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
+
>
+
<optgroup label="branches" class="bold text-sm">
+
{{ range .Branches }}
+
<option
+
value="{{ .Reference.Name }}"
+
class="py-1"
+
{{ if eq .Reference.Name $.Ref }}
+
selected
+
{{ end }}
+
>
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</optgroup>
+
<optgroup label="tags" class="bold text-sm">
+
{{ range .Tags }}
+
<option
+
value="{{ .Reference.Name }}"
+
class="py-1"
+
{{ if eq .Reference.Name $.Ref }}
+
selected
+
{{ end }}
+
>
+
{{ .Reference.Name }}
+
</option>
+
{{ else }}
+
<option class="py-1" disabled>no tags found</option>
+
{{ end }}
+
</optgroup>
+
</select>
+
<a
+
href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}"
+
class="ml-2 no-underline flex items-center gap-2 text-sm uppercase font-bold dark:text-white"
+
>
+
{{ i "logs" "w-4 h-4" }}
+
{{ .TotalCommits }}
+
{{ if eq .TotalCommits 1 }}commit{{ else }}commits{{ end }}
+
</a>
+
</div>
{{ end }}
{{ define "fileTree" }}
-
<div id="file-tree" class="col-span-1 pr-2 md:border-r md:border-gray-200 dark:md:border-gray-700">
-
{{ $containerstyle := "py-1" }}
-
{{ $linkstyle := "no-underline hover:underline dark:text-white" }}
+
<div
+
id="file-tree"
+
class="col-span-1 pr-2 md:border-r md:border-gray-200 dark:md:border-gray-700"
+
>
+
{{ $containerstyle := "py-1" }}
+
{{ $linkstyle := "no-underline hover:underline dark:text-white" }}
-
{{ range .Files }}
-
{{ if not .IsFile }}
-
<div class="{{ $containerstyle }}">
-
<div class="flex justify-between items-center">
-
<a
-
href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref | urlquery }}/{{ .Name }}"
-
class="{{ $linkstyle }}"
-
>
-
<div class="flex items-center gap-2">
-
{{ i "folder" "w-3 h-3 fill-current" }}
-
{{ .Name }}
-
</div>
-
</a>
+
{{ range .Files }}
+
{{ if not .IsFile }}
+
<div class="{{ $containerstyle }}">
+
<div class="flex justify-between items-center">
+
<a
+
href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref | urlquery }}/{{ .Name }}"
+
class="{{ $linkstyle }}"
+
>
+
<div class="flex items-center gap-2">
+
{{ i "folder" "w-3 h-3 fill-current" }}
+
{{ .Name }}
+
</div>
+
</a>
-
<time class="text-xs text-gray-500 dark:text-gray-400"
-
>{{ timeFmt .LastCommit.When }}</time
-
>
+
<time class="text-xs text-gray-500 dark:text-gray-400"
+
>{{ timeFmt .LastCommit.When }}</time
+
>
+
</div>
</div>
-
</div>
+
{{ end }}
{{ end }}
-
{{ end }}
-
{{ range .Files }}
-
{{ if .IsFile }}
-
<div class="{{ $containerstyle }}">
-
<div class="flex justify-between items-center">
-
<a
-
href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref | urlquery }}/{{ .Name }}"
-
class="{{ $linkstyle }}"
-
>
-
<div class="flex items-center gap-2">
-
{{ i "file" "w-3 h-3" }}{{ .Name }}
-
</div>
-
</a>
+
{{ range .Files }}
+
{{ if .IsFile }}
+
<div class="{{ $containerstyle }}">
+
<div class="flex justify-between items-center">
+
<a
+
href="/{{ $.RepoInfo.FullName }}/blob/{{ $.Ref | urlquery }}/{{ .Name }}"
+
class="{{ $linkstyle }}"
+
>
+
<div class="flex items-center gap-2">
+
{{ i "file" "w-3 h-3" }}{{ .Name }}
+
</div>
+
</a>
-
<time class="text-xs text-gray-500 dark:text-gray-400"
-
>{{ timeFmt .LastCommit.When }}</time
-
>
+
<time class="text-xs text-gray-500 dark:text-gray-400"
+
>{{ timeFmt .LastCommit.When }}</time
+
>
+
</div>
</div>
-
</div>
+
{{ end }}
{{ end }}
-
{{ end }}
-
</div>
+
</div>
{{ end }}
-
{{ define "commitLog" }}
-
<div id="commit-log" class="hidden md:block md:col-span-1">
-
{{ range .Commits }}
-
<div class="relative px-2 pb-8">
-
<div id="commit-message">
-
{{ $messageParts := splitN .Message "\n\n" 2 }}
-
<div class="text-base cursor-pointer">
-
<div>
-
<div>
-
<a
-
href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}"
-
class="inline no-underline hover:underline dark:text-white"
-
>{{ index $messageParts 0 }}</a
-
>
-
{{ if gt (len $messageParts) 1 }}
+
<div id="commit-log" class="hidden md:block md:col-span-1">
+
{{ range .Commits }}
+
<div class="relative px-2 pb-8">
+
<div id="commit-message">
+
{{ $messageParts := splitN .Message "\n\n" 2 }}
+
<div class="text-base cursor-pointer">
+
<div>
+
<div>
+
<a
+
href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}"
+
class="inline no-underline hover:underline dark:text-white"
+
>{{ index $messageParts 0 }}</a
+
>
+
{{ if gt (len $messageParts) 1 }}
-
<button
-
class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600"
-
hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')"
-
>
-
{{ i "ellipsis" "w-3 h-3" }}
-
</button>
-
{{ end }}
-
</div>
-
{{ if gt (len $messageParts) 1 }}
-
<p
-
class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300"
-
>
-
{{ nl2br (unwrapText (index $messageParts 1)) }}
-
</p>
-
{{ end }}
-
</div>
-
</div>
-
</div>
+
<button
+
class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600"
+
hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')"
+
>
+
{{ i "ellipsis" "w-3 h-3" }}
+
</button>
+
{{ end }}
+
</div>
+
{{ if gt (len $messageParts) 1 }}
+
<p
+
class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300"
+
>
+
{{ nl2br (index $messageParts 1) }}
+
</p>
+
{{ end }}
+
</div>
+
</div>
+
</div>
-
<div class="text-xs text-gray-500 dark:text-gray-400">
-
<span class="font-mono">
-
<a
-
href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}"
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
-
>{{ slice .Hash.String 0 8 }}</a
-
>
-
</span>
-
<span
-
class="mx-2 before:content-['ยท'] before:select-none"
-
></span>
-
<span>
-
{{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }}
-
<a
-
href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
-
>{{ if $didOrHandle }}{{ $didOrHandle }}{{ else }}{{ .Author.Name }}{{ end }}</a
-
>
-
</span>
-
<div
-
class="inline-block px-1 select-none after:content-['ยท']"
-
></div>
-
<span>{{ timeFmt .Author.When }}</span>
-
{{ $tagsForCommit := index $.TagMap .Hash.String }}
-
{{ if gt (len $tagsForCommit) 0 }}
+
<div class="text-xs text-gray-500 dark:text-gray-400">
+
<span class="font-mono">
+
<a
+
href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}"
+
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
+
>{{ slice .Hash.String 0 8 }}</a></span>
+
<span
+
class="mx-2 before:content-['ยท'] before:select-none"
+
></span>
+
<span>
+
{{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }}
+
<a
+
href="{{ if $didOrHandle }}
+
/{{ $didOrHandle }}
+
{{ else }}
+
mailto:{{ .Author.Email }}
+
{{ end }}"
+
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
+
>{{ if $didOrHandle }}
+
{{ $didOrHandle }}
+
{{ else }}
+
{{ .Author.Name }}
+
{{ end }}</a
+
>
+
</span>
<div
class="inline-block px-1 select-none after:content-['ยท']"
></div>
-
{{ end }}
-
{{ range $tagsForCommit }}
-
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
-
{{ . }}
-
</span>
-
{{ end }}
-
</div>
-
</div>
-
{{ end }}
-
</div>
+
<span>{{ timeFmt .Author.When }}</span>
+
{{ $tagsForCommit := index $.TagMap .Hash.String }}
+
{{ if gt (len $tagsForCommit) 0 }}
+
<div
+
class="inline-block px-1 select-none after:content-['ยท']"
+
></div>
+
{{ end }}
+
{{ range $tagsForCommit }}
+
<span
+
class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"
+
>
+
{{ . }}
+
</span>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+
</div>
{{ end }}
-
{{ define "repoAfter" }}
{{- if .HTMLReadme }}
-
<section class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }} prose dark:prose-invert dark:[&_pre]:bg-gray-900 dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 dark:[&_pre]:border dark:[&_pre]:border-gray-700 {{ end }}">
-
<article class="{{ if .Raw }}whitespace-pre{{end}}">
-
{{ if .Raw }}
-
<pre class="dark:bg-gray-900 dark:text-gray-200 dark:border dark:border-gray-700 dark:p-4 dark:rounded">{{ .HTMLReadme }}</pre>
-
{{ else }}
-
{{ .HTMLReadme }}
-
{{ end }}
-
</article>
-
</section>
+
<section
+
class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }}
+
prose dark:prose-invert dark:[&_pre]:bg-gray-900
+
dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900
+
dark:[&_pre]:border dark:[&_pre]:border-gray-700
+
{{ end }}"
+
>
+
<article class="{{ if .Raw }}whitespace-pre{{ end }}">
+
{{ if .Raw }}
+
<pre
+
class="dark:bg-gray-900 dark:text-gray-200 dark:border dark:border-gray-700 dark:p-4 dark:rounded"
+
>
+
{{ .HTMLReadme }}</pre
+
>
+
{{ else }}
+
{{ .HTMLReadme }}
+
{{ end }}
+
</article>
+
</section>
{{- end -}}
-
{{ template "fragments/cloneInstructions" . }}
+
{{ template "repo/fragments/cloneInstructions" . }}
{{ end }}
+52
appview/pages/templates/repo/issues/fragments/editIssueComment.html
···
+
{{ define "repo/issues/fragments/editIssueComment" }}
+
{{ with .Comment }}
+
<div id="comment-container-{{.CommentId}}">
+
<div class="flex items-center gap-2 mb-2 text-gray-500 text-sm">
+
{{ $owner := didOrHandle $.LoggedInUser.Did $.LoggedInUser.Handle }}
+
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
+
+
<!-- show user "hats" -->
+
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
+
{{ if $isIssueAuthor }}
+
<span class="before:content-['ยท']"></span>
+
<span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center">
+
author
+
</span>
+
{{ end }}
+
+
<span class="before:content-['ยท']"></span>
+
<a
+
href="#{{ .CommentId }}"
+
class="text-gray-500 hover:text-gray-500 hover:underline no-underline"
+
id="{{ .CommentId }}">
+
{{ .Created | timeFmt }}
+
</a>
+
+
<button
+
class="btn px-2 py-1 flex items-center gap-2 text-sm"
+
hx-post="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
+
hx-include="#edit-textarea-{{ .CommentId }}"
+
hx-target="#comment-container-{{ .CommentId }}"
+
hx-swap="outerHTML">
+
{{ i "check" "w-4 h-4" }}
+
</button>
+
<button
+
class="btn px-2 py-1 flex items-center gap-2 text-sm"
+
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
+
hx-target="#comment-container-{{ .CommentId }}"
+
hx-swap="outerHTML">
+
{{ i "x" "w-4 h-4" }}
+
</button>
+
<span id="comment-{{.CommentId}}-status"></span>
+
</div>
+
+
<div>
+
<textarea
+
id="edit-textarea-{{ .CommentId }}"
+
name="body"
+
class="w-full p-2 border rounded min-h-[100px]">{{ .Body }}</textarea>
+
</div>
+
</div>
+
{{ end }}
+
{{ end }}
+
+59
appview/pages/templates/repo/issues/fragments/issueComment.html
···
+
{{ define "repo/issues/fragments/issueComment" }}
+
{{ with .Comment }}
+
<div id="comment-container-{{.CommentId}}">
+
<div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm">
+
{{ $owner := index $.DidHandleMap .OwnerDid }}
+
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
+
+
<span class="before:content-['ยท']"></span>
+
<a
+
href="#{{ .CommentId }}"
+
class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-400 hover:underline no-underline"
+
id="{{ .CommentId }}">
+
{{ if .Deleted }}
+
deleted {{ .Deleted | timeFmt }}
+
{{ else if .Edited }}
+
edited {{ .Edited | timeFmt }}
+
{{ else }}
+
{{ .Created | timeFmt }}
+
{{ end }}
+
</a>
+
+
<!-- show user "hats" -->
+
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
+
{{ if $isIssueAuthor }}
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
+
author
+
</span>
+
{{ end }}
+
+
{{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }}
+
{{ if and $isCommentOwner (not .Deleted) }}
+
<button
+
class="btn px-2 py-1 text-sm"
+
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
+
hx-swap="outerHTML"
+
hx-target="#comment-container-{{.CommentId}}"
+
>
+
{{ i "pencil" "w-4 h-4" }}
+
</button>
+
<button
+
class="btn px-2 py-1 text-sm text-red-500"
+
hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
+
hx-confirm="Are you sure you want to delete your comment?"
+
hx-swap="outerHTML"
+
hx-target="#comment-container-{{.CommentId}}"
+
>
+
{{ i "trash-2" "w-4 h-4" }}
+
</button>
+
{{ end }}
+
+
</div>
+
{{ if not .Deleted }}
+
<div class="prose dark:prose-invert">
+
{{ .Body | markdown }}
+
</div>
+
{{ end }}
+
</div>
+
{{ end }}
+
{{ end }}
+112 -42
appview/pages/templates/repo/issues/issue.html
···
{{ end }}
{{ define "repoAfter" }}
-
{{ if gt (len .Comments) 0 }}
-
<section id="comments" class="mt-8 space-y-4 relative">
+
<section id="comments" class="my-2 mt-2 space-y-2 relative">
{{ range $index, $comment := .Comments }}
<div
id="comment-{{ .CommentId }}"
-
class="rounded bg-white px-6 py-4 relative dark:bg-gray-800">
-
{{ if eq $index 0 }}
-
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div>
-
{{ else }}
-
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-700" ></div>
+
class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full md:max-w-3/5 md:w-fit">
+
{{ if gt $index 0 }}
+
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
{{ end }}
-
-
{{ template "fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}}
+
{{ template "repo/issues/fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}}
</div>
{{ end }}
</section>
-
{{ end }}
{{ block "newComment" . }} {{ end }}
-
{{ $isIssueAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Issue.OwnerDid) }}
-
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
-
{{ if or $isIssueAuthor $isRepoCollaborator }}
-
{{ $action := "close" }}
-
{{ $icon := "circle-x" }}
-
{{ $hoverColor := "red" }}
-
{{ if eq .State "closed" }}
-
{{ $action = "reopen" }}
-
{{ $icon = "circle-dot" }}
-
{{ $hoverColor = "green" }}
-
{{ end }}
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/{{ $action }}"
-
hx-swap="none"
-
class="mt-8"
-
>
-
<button type="submit" class="btn hover:bg-{{ $hoverColor }}-300">
-
{{ i $icon "w-4 h-4 mr-2" }}
-
<span class="text-black dark:text-gray-400">{{ $action }}</span>
-
</button>
-
<div id="issue-action" class="error"></div>
-
</form>
-
{{ end }}
{{ end }}
{{ define "newComment" }}
{{ if .LoggedInUser }}
-
<div class="bg-white rounded drop-shadow-sm py-4 px-6 relative w-full flex flex-col gap-2 mt-8 dark:bg-gray-800 dark:text-gray-400">
-
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div>
-
<div class="text-sm text-gray-500 dark:text-gray-400">
+
<form
+
id="comment-form"
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
+
hx-on::after-request="if(event.detail.successful) this.reset()"
+
>
+
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5">
+
<div class="text-sm pb-2 text-gray-500 dark:text-gray-400">
{{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }}
</div>
-
<form hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment">
<textarea
+
id="comment-textarea"
name="body"
class="w-full p-2 rounded border border-gray-200 dark:border-gray-700"
-
placeholder="Add to the discussion..."
+
placeholder="Add to the discussion. Markdown is supported."
+
onkeyup="updateCommentForm()"
></textarea>
-
<button type="submit" class="btn mt-2">comment</button>
<div id="issue-comment"></div>
-
</form>
+
<div id="issue-action" class="error"></div>
</div>
+
+
<div class="flex gap-2 mt-2">
+
<button
+
id="comment-button"
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
+
type="submit"
+
hx-disabled-elt="#comment-button"
+
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline"
+
disabled
+
>
+
{{ i "message-square-plus" "w-4 h-4" }}
+
comment
+
</button>
+
+
{{ $isIssueAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Issue.OwnerDid) }}
+
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
+
{{ if and (or $isIssueAuthor $isRepoCollaborator) (eq .State "open") }}
+
<button
+
id="close-button"
+
type="button"
+
class="btn flex items-center gap-2"
+
hx-trigger="click"
+
>
+
{{ i "ban" "w-4 h-4" }}
+
close
+
</button>
+
<div
+
id="close-with-comment"
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment"
+
hx-trigger="click from:#close-button"
+
hx-disabled-elt="#close-with-comment"
+
hx-target="#issue-comment"
+
hx-vals="js:{body: document.getElementById('comment-textarea').value.trim() !== '' ? document.getElementById('comment-textarea').value : ''}"
+
hx-swap="none"
+
>
+
</div>
+
<div
+
id="close-issue"
+
hx-disabled-elt="#close-issue"
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/close"
+
hx-trigger="click from:#close-button"
+
hx-target="#issue-action"
+
hx-swap="none"
+
>
+
</div>
+
<script>
+
document.addEventListener('htmx:configRequest', function(evt) {
+
if (evt.target.id === 'close-with-comment') {
+
const commentText = document.getElementById('comment-textarea').value.trim();
+
if (commentText === '') {
+
evt.detail.parameters = {};
+
evt.preventDefault();
+
}
+
}
+
});
+
</script>
+
{{ else if and (or $isIssueAuthor $isRepoCollaborator) (eq .State "closed") }}
+
<button
+
type="button"
+
class="btn flex items-center gap-2"
+
hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/reopen"
+
hx-swap="none"
+
>
+
{{ i "refresh-ccw-dot" "w-4 h-4" }}
+
reopen
+
</button>
+
{{ end }}
+
+
<script>
+
function updateCommentForm() {
+
const textarea = document.getElementById('comment-textarea');
+
const commentButton = document.getElementById('comment-button');
+
const closeButton = document.getElementById('close-button');
+
+
if (textarea.value.trim() !== '') {
+
commentButton.removeAttribute('disabled');
+
} else {
+
commentButton.setAttribute('disabled', '');
+
}
+
+
if (closeButton) {
+
if (textarea.value.trim() !== '') {
+
closeButton.innerHTML = '{{ i "ban" "w-4 h-4" }} close with comment';
+
} else {
+
closeButton.innerHTML = '{{ i "ban" "w-4 h-4" }} close';
+
}
+
}
+
}
+
+
document.addEventListener('DOMContentLoaded', function() {
+
updateCommentForm();
+
});
+
</script>
+
</div>
+
</form>
{{ else }}
-
<div class="bg-white dark:bg-gray-800 dark:text-gray-400 rounded drop-shadow-sm px-6 py-4 mt-8">
-
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div>
+
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-fit">
<a href="/login" class="underline">login</a> to join the discussion
</div>
{{ end }}
+3 -3
appview/pages/templates/repo/issues/issues.html
···
<div class="flex justify-between items-center">
<p>
filtering
-
<select class="border px-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value">
+
<select class="border p-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value">
<option value="open" {{ if .FilteringByOpen }}selected{{ end }}>open ({{ .RepoInfo.Stats.IssueCount.Open }})</option>
<option value="closed" {{ if not .FilteringByOpen }}selected{{ end }}>closed ({{ .RepoInfo.Stats.IssueCount.Closed }})</option>
</select>
···
<a
href="/{{ .RepoInfo.FullName }}/issues/new"
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline">
-
{{ i "plus" "w-4 h-4" }}
-
<span>new issue</span>
+
{{ i "circle-plus" "w-4 h-4" }}
+
<span>new</span>
</a>
</div>
<div class="error" id="issues"></div>
+1 -1
appview/pages/templates/repo/log.html
···
<p
class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300"
>
-
{{ nl2br (unwrapText (index $messageParts 1)) }}
+
{{ nl2br (index $messageParts 1) }}
</p>
{{ end }}
</div>
+90
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
+
{{ define "repo/pulls/fragments/pullActions" }}
+
{{ $lastIdx := sub (len .Pull.Submissions) 1 }}
+
{{ $roundNumber := .RoundNumber }}
+
+
{{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }}
+
{{ $isMerged := .Pull.State.IsMerged }}
+
{{ $isClosed := .Pull.State.IsClosed }}
+
{{ $isOpen := .Pull.State.IsOpen }}
+
{{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }}
+
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
+
{{ $isLastRound := eq $roundNumber $lastIdx }}
+
{{ $isSameRepoBranch := .Pull.IsBranchBased }}
+
{{ $isUpToDate := .ResubmitCheck.No }}
+
<div class="relative w-fit">
+
<div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2">
+
<button
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment"
+
hx-target="#actions-{{$roundNumber}}"
+
hx-swap="outerHtml"
+
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline">
+
{{ i "message-square-plus" "w-4 h-4" }}
+
<span>comment</span>
+
</button>
+
{{ if and $isPushAllowed $isOpen $isLastRound }}
+
{{ $disabled := "" }}
+
{{ if $isConflicted }}
+
{{ $disabled = "disabled" }}
+
{{ end }}
+
<button
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
+
hx-swap="none"
+
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
+
class="btn p-2 flex items-center gap-2" {{ $disabled }}>
+
{{ i "git-merge" "w-4 h-4" }}
+
<span>merge</span>
+
</button>
+
{{ end }}
+
+
{{ if and $isPullAuthor $isOpen $isLastRound }}
+
{{ $disabled := "" }}
+
{{ if $isUpToDate }}
+
{{ $disabled = "disabled" }}
+
{{ end }}
+
<button id="resubmitBtn"
+
{{ if not .Pull.IsPatchBased }}
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
+
{{ else }}
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
+
hx-target="#actions-{{$roundNumber}}"
+
hx-swap="outerHtml"
+
{{ end }}
+
+
hx-disabled-elt="#resubmitBtn"
+
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }}
+
+
{{ if $disabled }}
+
title="Update this branch to resubmit this pull request"
+
{{ else }}
+
title="Resubmit this pull request"
+
{{ end }}
+
>
+
{{ i "rotate-ccw" "w-4 h-4" }}
+
<span>resubmit</span>
+
</button>
+
{{ end }}
+
+
{{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }}
+
<button
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close"
+
hx-swap="none"
+
class="btn p-2 flex items-center gap-2">
+
{{ i "ban" "w-4 h-4" }}
+
<span>close</span>
+
</button>
+
{{ end }}
+
+
{{ if and (or $isPullAuthor $isPushAllowed) $isClosed $isLastRound }}
+
<button
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen"
+
hx-swap="none"
+
class="btn p-2 flex items-center gap-2">
+
{{ i "refresh-ccw-dot" "w-4 h-4" }}
+
<span>reopen</span>
+
</button>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+
+
+20
appview/pages/templates/repo/pulls/fragments/pullCompareBranches.html
···
+
{{ define "repo/pulls/fragments/pullCompareBranches" }}
+
<div id="patch-upload">
+
<label for="targetBranch" class="dark:text-white"
+
>select a branch</label
+
>
+
<div class="flex flex-wrap gap-2 items-center">
+
<select
+
name="sourceBranch"
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
>
+
<option disabled selected>source branch</option>
+
{{ range .Branches }}
+
<option value="{{ .Reference.Name }}" class="py-1">
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</select>
+
</div>
+
</div>
+
{{ end }}
+42
appview/pages/templates/repo/pulls/fragments/pullCompareForks.html
···
+
{{ define "repo/pulls/fragments/pullCompareForks" }}
+
<div id="patch-upload">
+
<label for="forkSelect" class="dark:text-white"
+
>select a fork to compare</label
+
>
+
<div class="flex flex-wrap gap-4 items-center mb-4">
+
<div class="flex flex-wrap gap-2 items-center">
+
<select
+
id="forkSelect"
+
name="fork"
+
required
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
hx-get="/{{ $.RepoInfo.FullName }}/pulls/new/fork-branches"
+
hx-target="#branch-selection"
+
hx-vals='{"fork": this.value}'
+
hx-swap="innerHTML"
+
onchange="document.getElementById('hiddenForkInput').value = this.value;"
+
>
+
<option disabled selected>select a fork</option>
+
{{ range .Forks }}
+
<option value="{{ .Name }}" class="py-1">
+
{{ .Name }}
+
</option>
+
{{ end }}
+
</select>
+
+
<input
+
type="hidden"
+
id="hiddenForkInput"
+
name="fork"
+
value=""
+
/>
+
</div>
+
+
<div id="branch-selection">
+
<div class="text-sm text-gray-500 dark:text-gray-400">
+
Select a fork first to view available branches
+
</div>
+
</div>
+
</div>
+
</div>
+
{{ end }}
+15
appview/pages/templates/repo/pulls/fragments/pullCompareForksBranches.html
···
+
{{ define "repo/pulls/fragments/pullCompareForksBranches" }}
+
<div class="flex flex-wrap gap-2 items-center">
+
<select
+
name="sourceBranch"
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
>
+
<option disabled selected>source branch</option>
+
{{ range .SourceBranches }}
+
<option value="{{ .Reference.Name }}" class="py-1">
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</select>
+
</div>
+
{{ end }}
+32
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
···
+
{{ define "repo/pulls/fragments/pullNewComment" }}
+
<div
+
id="pull-comment-card-{{ .RoundNumber }}"
+
class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2">
+
<div class="text-sm text-gray-500 dark:text-gray-400">
+
{{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }}
+
</div>
+
<form
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment"
+
hx-swap="none"
+
class="w-full flex flex-wrap gap-2">
+
<textarea
+
name="body"
+
class="w-full p-2 rounded border border-gray-200"
+
placeholder="Add to the discussion..."></textarea>
+
<button type="submit" class="btn flex items-center gap-2">
+
{{ i "message-square" "w-4 h-4" }} comment
+
</button>
+
<button
+
type="button"
+
class="btn flex items-center gap-2"
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions"
+
hx-swap="outerHTML"
+
hx-target="#pull-comment-card-{{ .RoundNumber }}">
+
{{ i "x" "w-4 h-4" }}
+
<span>cancel</span>
+
</button>
+
<div id="pull-comment"></div>
+
</form>
+
</div>
+
{{ end }}
+
+14
appview/pages/templates/repo/pulls/fragments/pullPatchUpload.html
···
+
{{ define "repo/pulls/fragments/pullPatchUpload" }}
+
<div id="patch-upload">
+
<textarea
+
name="patch"
+
id="patch"
+
rows="12"
+
class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
placeholder="diff --git a/file.txt b/file.txt
+
index 1234567..abcdefg 100644
+
--- a/file.txt
+
+++ b/file.txt"
+
></textarea>
+
</div>
+
{{ end }}
+52
appview/pages/templates/repo/pulls/fragments/pullResubmit.html
···
+
{{ define "repo/pulls/fragments/pullResubmit" }}
+
<div
+
id="resubmit-pull-card"
+
class="rounded relative border bg-amber-50 dark:bg-amber-900 border-amber-200 dark:border-amber-500 px-6 py-2">
+
+
<div class="flex items-center gap-2 text-amber-500 dark:text-amber-50">
+
{{ i "pencil" "w-4 h-4" }}
+
<span class="font-medium">resubmit your patch</span>
+
</div>
+
+
<div class="mt-2 text-sm text-gray-700 dark:text-gray-200">
+
You can update this patch to address any reviews.
+
This will begin a new round of reviews,
+
but you'll still be able to view your previous submissions and feedback.
+
</div>
+
+
<div class="mt-4 flex flex-col">
+
<form
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
+
hx-swap="none"
+
class="w-full flex flex-wrap gap-2">
+
<textarea
+
name="patch"
+
class="w-full p-2 mb-2"
+
placeholder="Paste your updated patch here."
+
rows="15"
+
>{{.Pull.LatestPatch}}</textarea>
+
<button
+
type="submit"
+
class="btn flex items-center gap-2"
+
{{ if or .Pull.State.IsClosed }}
+
disabled
+
{{ end }}>
+
{{ i "rotate-ccw" "w-4 h-4" }}
+
<span>resubmit</span>
+
</button>
+
<button
+
type="button"
+
class="btn flex items-center gap-2"
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .Pull.LastRoundNumber }}/actions"
+
hx-swap="outerHTML"
+
hx-target="#resubmit-pull-card">
+
{{ i "x" "w-4 h-4" }}
+
<span>cancel</span>
+
</button>
+
</form>
+
+
<div id="resubmit-error" class="error"></div>
+
<div id="resubmit-success" class="success"></div>
+
</div>
+
</div>
+
{{ end }}
+1 -1
appview/pages/templates/repo/pulls/new.html
···
</nav>
<section id="patch-strategy">
-
{{ template "fragments/pullPatchUpload" . }}
+
{{ template "repo/pulls/fragments/pullPatchUpload" . }}
</section>
<div class="flex justify-start items-center gap-2 mt-4">
+1 -14
appview/pages/templates/repo/pulls/patch.html
···
{{ end }}
</section>
-
<div id="diff-stat">
-
<br>
-
<strong class="text-sm uppercase mb-4">Changed files</strong>
-
{{ range .Diff.Diff }}
-
<ul>
-
{{ if .IsDelete }}
-
<li><a href="#file-{{ .Name.Old }}">{{ .Name.Old }}</a></li>
-
{{ else }}
-
<li><a href="#file-{{ .Name.New }}">{{ .Name.New }}</a></li>
-
{{ end }}
-
</ul>
-
{{ end }}
-
</div>
</div>
<section>
-
{{ template "fragments/diff" (list .RepoInfo.FullName .Diff) }}
+
{{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }}
</section>
{{ end }}
+8 -8
appview/pages/templates/repo/pulls/pull.html
···
{{ $icon = "git-merge" }}
{{ end }}
-
<section>
+
<section class="mt-2">
<div class="flex items-center gap-2">
<div
id="state"
···
<a href="/{{ .RepoInfo.FullName }}/tree/{{ .Pull.TargetBranch }}" class="no-underline hover:underline">{{ .Pull.TargetBranch }}</a>
</span>
</span>
-
{{ if not .Pull.IsPatch }}
+
{{ if not .Pull.IsPatchBased }}
<span>from
-
{{ if not .Pull.IsSameRepoBranch }}
+
{{ if not .Pull.IsBranchBased }}
<a href="/{{ $owner }}/{{ .PullSourceRepo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSourceRepo.Name }}</a>
{{ end }}
{{ $fullRepo := .RepoInfo.FullName }}
-
{{ if not .Pull.IsSameRepoBranch }}
+
{{ if not .Pull.IsBranchBased }}
{{ $fullRepo = printf "%s/%s" $owner .PullSourceRepo.Name }}
{{ end }}
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
···
</div>
{{ if .Pull.Body }}
-
<article id="body" class="mt-2 prose dark:prose-invert">
+
<article id="body" class="mt-8 prose dark:prose-invert">
{{ .Pull.Body | markdown }}
</article>
{{ end }}
···
</summary>
<div class="md:pl-12 flex flex-col gap-2 mt-2 relative">
{{ range .Comments }}
-
<div id="comment-{{.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-fit">
+
<div id="comment-{{.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full md:max-w-3/5 md:w-fit">
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ $owner := index $.DidHandleMap .OwnerDid }}
···
{{ end }}
{{ if $.LoggedInUser }}
-
{{ template "fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }}
+
{{ template "repo/pulls/fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }}
{{ else }}
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm px-6 py-4 w-fit dark:text-white">
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
···
{{ end }}
</div>
</details>
-
<hr class="md:hidden"/>
+
<hr class="md:hidden border-t border-gray-300 dark:border-gray-600"/>
{{ end }}
{{ end }}
{{ end }}
+10 -6
appview/pages/templates/repo/pulls/pulls.html
···
<p class="dark:text-white">
filtering
<select
-
class="border px-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white"
+
class="border p-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white"
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/pulls?state=' + this.value"
>
<option value="open" {{ if .FilteringBy.IsOpen }}selected{{ end }}>
···
</p>
<a
href="/{{ .RepoInfo.FullName }}/pulls/new"
-
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:bg-gray-700 dark:hover:bg-gray-600"
+
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"
>
-
{{ i "git-pull-request" "w-4 h-4" }}
-
<span>new pull request</span>
+
{{ i "git-pull-request-create" "w-4 h-4" }}
+
<span>new</span>
</a>
</div>
<div class="error" id="pulls"></div>
···
{{ .TargetBranch }}
</span>
</span>
-
{{ if not .IsPatch }}
+
{{ if not .IsPatchBased }}
<span>from
-
{{ if not .IsSameRepoBranch }}
+
{{ if .IsForkBased }}
+
{{ if .PullSource.Repo }}
<a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a>
+
{{ else }}
+
<span class="italic">[deleted fork]</span>
+
{{ end }}
{{ end }}
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
+4 -3
appview/pages/templates/repo/tree.html
···
{{ $stats := .TreeStats }}
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span>
-
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
{{ if eq $stats.NumFolders 1 }}
-
<span>{{ $stats.NumFolders }} folder</span>
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
+
<span>{{ $stats.NumFolders }} folder</span>
{{ else if gt $stats.NumFolders 1 }}
+
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
<span>{{ $stats.NumFolders }} folders</span>
-
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
{{ end }}
{{ if eq $stats.NumFiles 1 }}
+
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
<span>{{ $stats.NumFiles }} file</span>
{{ else if gt $stats.NumFiles 1 }}
+
<span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span>
<span>{{ $stats.NumFiles }} files</span>
{{ end }}
+17
appview/pages/templates/user/fragments/follow.html
···
+
{{ define "user/fragments/follow" }}
+
<button id="followBtn"
+
class="btn mt-2 w-full"
+
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}
+
hx-post="/follow?subject={{.UserDid}}"
+
{{ else }}
+
hx-delete="/follow?subject={{.UserDid}}"
+
{{ end }}
+
+
hx-trigger="click"
+
hx-target="#followBtn"
+
hx-swap="outerHTML"
+
>
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
+
</button>
+
{{ end }}
+1 -1
appview/pages/templates/user/profile.html
···
</div>
{{ if ne .FollowStatus.String "IsSelf" }}
-
{{ template "fragments/follow" . }}
+
{{ template "user/fragments/follow" . }}
{{ end }}
</div>
{{ end }}
+347 -194
appview/state/pull.go
···
pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String())
if err != nil {
log.Printf("failed to get repo by at uri: %v", err)
-
return
+
continue
+
} else {
+
p.PullSource.Repo = pullSourceRepo
}
}
-
p.PullSource.Repo = pullSourceRepo
}
}
···
return
}
-
resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
-
switch resp.StatusCode {
-
case 404:
-
case 400:
-
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
-
return
-
}
-
-
respBody, err := io.ReadAll(resp.Body)
+
diffTreeResponse, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
if err != nil {
-
log.Println("failed to compare across branches")
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
defer resp.Body.Close()
-
-
var diffTreeResponse types.RepoDiffTreeResponse
-
err = json.Unmarshal(respBody, &diffTreeResponse)
-
if err != nil {
-
log.Println("failed to unmarshal diff tree response", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+
log.Println("failed to compare", err)
+
s.pages.Notice(w, "pull", err.Error())
return
}
···
// hiddenRef: hidden/feature-1/main (on repo-fork)
// targetBranch: main (on repo-1)
// sourceBranch: feature-1 (on repo-fork)
-
diffResp, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
+
diffTreeResponse, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
if err != nil {
log.Println("failed to compare across branches", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
-
respBody, err := io.ReadAll(diffResp.Body)
-
if err != nil {
-
log.Println("failed to read response body", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
-
defer resp.Body.Close()
-
-
var diffTreeResponse types.RepoDiffTreeResponse
-
err = json.Unmarshal(respBody, &diffTreeResponse)
-
if err != nil {
-
log.Println("failed to unmarshal diff tree response", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+
s.pages.Notice(w, "pull", err.Error())
return
}
···
})
return
case http.MethodPost:
-
patch := r.FormValue("patch")
-
var sourceRev string
-
var recordPullSource *tangled.RepoPull_Source
+
if pull.IsPatchBased() {
+
s.resubmitPatch(w, r)
+
return
+
} else if pull.IsBranchBased() {
+
s.resubmitBranch(w, r)
+
return
+
} else if pull.IsForkBased() {
+
s.resubmitFork(w, r)
+
return
+
}
+
}
+
}
+
+
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
-
var ownerDid, repoName, knotName string
-
var isSameRepo bool = pull.IsSameRepoBranch()
-
sourceBranch := pull.PullSource.Branch
-
targetBranch := pull.TargetBranch
-
recordPullSource = &tangled.RepoPull_Source{
-
Branch: sourceBranch,
-
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
+
return
+
}
+
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
+
+
if user.Did != pull.OwnerDid {
+
log.Println("unauthorized user")
+
w.WriteHeader(http.StatusUnauthorized)
+
return
+
}
+
+
patch := r.FormValue("patch")
+
+
if err = validateResubmittedPatch(pull, patch); err != nil {
+
s.pages.Notice(w, "resubmit-error", err.Error())
+
}
+
+
tx, err := s.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
log.Println("failed to start tx")
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
defer tx.Rollback()
+
+
err = db.ResubmitPull(tx, pull, patch, "")
+
if err != nil {
+
log.Println("failed to resubmit pull request", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
+
return
+
}
+
client, _ := s.auth.AuthorizedClient(r)
+
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
+
if err != nil {
+
// failed to get record
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
+
return
+
}
+
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoPullNSID,
+
Repo: user.Did,
+
Rkey: pull.Rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoPull{
+
Title: pull.Title,
+
PullId: int64(pull.PullId),
+
TargetRepo: string(f.RepoAt),
+
TargetBranch: pull.TargetBranch,
+
Patch: patch, // new patch
+
},
+
},
+
})
+
if err != nil {
+
log.Println("failed to update record", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
+
return
+
}
-
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
-
if isSameRepo && isPushAllowed {
-
ownerDid = f.OwnerDid()
-
repoName = f.RepoName
-
knotName = f.Knot
-
} else if !isSameRepo {
-
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
-
if err != nil {
-
log.Println("failed to get source repo", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
ownerDid = sourceRepo.Did
-
repoName = sourceRepo.Name
-
knotName = sourceRepo.Knot
-
}
+
if err = tx.Commit(); err != nil {
+
log.Println("failed to commit transaction", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
+
return
+
}
-
if sourceBranch != "" && knotName != "" {
-
// extract patch by performing compare
-
ksClient, err := NewUnsignedClient(knotName, s.config.Dev)
-
if err != nil {
-
log.Printf("failed to create client for %s: %s", knotName, err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
+
return
+
}
-
if !isSameRepo {
-
secret, err := db.GetRegistrationKey(s.db, knotName)
-
if err != nil {
-
log.Printf("failed to get registration key for %s: %s", knotName, err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
// update the hidden tracking branch to latest
-
signedClient, err := NewSignedClient(knotName, secret, s.config.Dev)
-
if err != nil {
-
log.Printf("failed to create signed client for %s: %s", knotName, err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
resp, err := signedClient.NewHiddenRef(ownerDid, repoName, sourceBranch, targetBranch)
-
if err != nil || resp.StatusCode != http.StatusNoContent {
-
log.Printf("failed to update tracking branch: %s", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
}
+
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
-
var compareResp *http.Response
-
if !isSameRepo {
-
hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch))
-
compareResp, err = ksClient.Compare(ownerDid, repoName, hiddenRef, sourceBranch)
-
} else {
-
compareResp, err = ksClient.Compare(ownerDid, repoName, targetBranch, sourceBranch)
-
}
-
if err != nil {
-
log.Printf("failed to compare branches: %s", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
defer compareResp.Body.Close()
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
+
return
+
}
-
switch compareResp.StatusCode {
-
case 404:
-
case 400:
-
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
-
return
-
}
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
return
+
}
-
respBody, err := io.ReadAll(compareResp.Body)
-
if err != nil {
-
log.Println("failed to compare across branches")
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
-
defer compareResp.Body.Close()
+
if user.Did != pull.OwnerDid {
+
log.Println("unauthorized user")
+
w.WriteHeader(http.StatusUnauthorized)
+
return
+
}
-
var diffTreeResponse types.RepoDiffTreeResponse
-
err = json.Unmarshal(respBody, &diffTreeResponse)
-
if err != nil {
-
log.Println("failed to unmarshal diff tree response", err)
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
-
return
-
}
+
if !f.RepoInfo(s, user).Roles.IsPushAllowed() {
+
log.Println("unauthorized user")
+
w.WriteHeader(http.StatusUnauthorized)
+
return
+
}
-
sourceRev = diffTreeResponse.DiffTree.Rev2
-
patch = diffTreeResponse.DiffTree.Patch
-
}
+
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
+
if err != nil {
+
log.Printf("failed to create client for %s: %s", f.Knot, err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
-
if patch == "" {
-
s.pages.Notice(w, "resubmit-error", "Patch is empty.")
-
return
-
}
+
diffTreeResponse, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
+
if err != nil {
+
log.Printf("compare request failed: %s", err)
+
s.pages.Notice(w, "resubmit-error", err.Error())
+
return
+
}
-
if patch == pull.LatestPatch() {
-
s.pages.Notice(w, "resubmit-error", "Patch is identical to previous submission.")
-
return
-
}
+
sourceRev := diffTreeResponse.DiffTree.Rev2
+
patch := diffTreeResponse.DiffTree.Patch
-
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
-
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
-
return
-
}
+
if err = validateResubmittedPatch(pull, patch); err != nil {
+
s.pages.Notice(w, "resubmit-error", err.Error())
+
}
-
if !isPatchValid(patch) {
-
s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.")
-
return
-
}
+
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
+
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
+
return
+
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
-
if err != nil {
-
log.Println("failed to start tx")
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
-
return
-
}
-
defer tx.Rollback()
+
tx, err := s.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
log.Println("failed to start tx")
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
defer tx.Rollback()
-
err = db.ResubmitPull(tx, pull, patch, sourceRev)
-
if err != nil {
-
log.Println("failed to create pull request", err)
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
-
return
-
}
-
client, _ := s.auth.AuthorizedClient(r)
+
err = db.ResubmitPull(tx, pull, patch, sourceRev)
+
if err != nil {
+
log.Println("failed to create pull request", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
client, _ := s.auth.AuthorizedClient(r)
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
-
if err != nil {
-
// failed to get record
-
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
-
return
-
}
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
+
if err != nil {
+
// failed to get record
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
+
return
+
}
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoPullNSID,
-
Repo: user.Did,
-
Rkey: pull.Rkey,
-
SwapRecord: ex.Cid,
-
Record: &lexutil.LexiconTypeDecoder{
-
Val: &tangled.RepoPull{
-
Title: pull.Title,
-
PullId: int64(pull.PullId),
-
TargetRepo: string(f.RepoAt),
-
TargetBranch: pull.TargetBranch,
-
Patch: patch, // new patch
-
Source: recordPullSource,
-
},
+
recordPullSource := &tangled.RepoPull_Source{
+
Branch: pull.PullSource.Branch,
+
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoPullNSID,
+
Repo: user.Did,
+
Rkey: pull.Rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoPull{
+
Title: pull.Title,
+
PullId: int64(pull.PullId),
+
TargetRepo: string(f.RepoAt),
+
TargetBranch: pull.TargetBranch,
+
Patch: patch, // new patch
+
Source: recordPullSource,
},
-
})
-
if err != nil {
-
log.Println("failed to update record", err)
-
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
-
return
-
}
+
},
+
})
+
if err != nil {
+
log.Println("failed to update record", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
+
return
+
}
+
+
if err = tx.Commit(); err != nil {
+
log.Println("failed to commit transaction", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
+
return
+
}
+
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
+
return
+
}
+
+
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
-
if err = tx.Commit(); err != nil {
-
log.Println("failed to commit transaction", err)
-
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
-
return
-
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
+
return
+
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
return
+
+
if user.Did != pull.OwnerDid {
+
log.Println("unauthorized user")
+
w.WriteHeader(http.StatusUnauthorized)
+
return
+
}
+
+
forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
+
if err != nil {
+
log.Println("failed to get source repo", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
+
// extract patch by performing compare
+
ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev)
+
if err != nil {
+
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
+
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
+
if err != nil {
+
log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
+
// update the hidden tracking branch to latest
+
signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev)
+
if err != nil {
+
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
+
resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch)
+
if err != nil || resp.StatusCode != http.StatusNoContent {
+
log.Printf("failed to update tracking branch: %s", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
+
hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch))
+
diffTreeResponse, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch)
+
if err != nil {
+
log.Printf("failed to compare branches: %s", err)
+
s.pages.Notice(w, "resubmit-error", err.Error())
+
return
+
}
+
+
sourceRev := diffTreeResponse.DiffTree.Rev2
+
patch := diffTreeResponse.DiffTree.Patch
+
+
if err = validateResubmittedPatch(pull, patch); err != nil {
+
s.pages.Notice(w, "resubmit-error", err.Error())
+
}
+
+
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
+
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
+
return
+
}
+
+
tx, err := s.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
log.Println("failed to start tx")
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
defer tx.Rollback()
+
+
err = db.ResubmitPull(tx, pull, patch, sourceRev)
+
if err != nil {
+
log.Println("failed to create pull request", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
+
return
+
}
+
client, _ := s.auth.AuthorizedClient(r)
+
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
+
if err != nil {
+
// failed to get record
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
+
return
+
}
+
+
repoAt := pull.PullSource.RepoAt.String()
+
recordPullSource := &tangled.RepoPull_Source{
+
Branch: pull.PullSource.Branch,
+
Repo: &repoAt,
+
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoPullNSID,
+
Repo: user.Did,
+
Rkey: pull.Rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.RepoPull{
+
Title: pull.Title,
+
PullId: int64(pull.PullId),
+
TargetRepo: string(f.RepoAt),
+
TargetBranch: pull.TargetBranch,
+
Patch: patch, // new patch
+
Source: recordPullSource,
+
},
+
},
+
})
+
if err != nil {
+
log.Println("failed to update record", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
+
return
+
}
+
+
if err = tx.Commit(); err != nil {
+
log.Println("failed to commit transaction", err)
+
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
+
return
+
}
+
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
+
return
+
}
+
+
// validate a resubmission against a pull request
+
func validateResubmittedPatch(pull *db.Pull, patch string) error {
+
if patch == "" {
+
return fmt.Errorf("Patch is empty.")
+
}
+
+
if patch == pull.LatestPatch() {
+
return fmt.Errorf("Patch is identical to previous submission.")
+
}
+
+
if !isPatchValid(patch) {
+
return fmt.Errorf("Invalid patch format. Please provide a valid diff.")
+
}
+
+
return nil
func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
+23 -6
appview/state/repo.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-chi/chi/v5"
+
"github.com/go-git/go-git/v5/plumbing"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
···
if !s.config.Dev {
protocol = "https"
}
+
+
if !plumbing.IsHash(ref) {
+
s.pages.Error404(w)
+
return
+
}
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
if err != nil {
log.Println("failed to reach knotserver", err)
···
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
}
}
-
+
showRendered := false
renderToggle := false
···
renderToggle = true
showRendered = r.URL.Query().Get("code") != "true"
}
-
+
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
···
forkName := fmt.Sprintf("%s", f.RepoName)
+
// this check is *only* to see if the forked repo name already exists
+
// in the user's account.
existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName)
-
if err == nil && existingRepo != nil {
+
if err != nil {
+
if errors.Is(err, sql.ErrNoRows) {
+
// no existing repo with this name found, we can use the name as is
+
} else {
+
log.Println("error fetching existing repo from db", err)
+
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
+
return
+
}
+
} else if existingRepo != nil {
+
// repo with this name already exists, append random string
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
-
secret, err := db.GetRegistrationKey(s.db, knot)
if err != nil {
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
···
client, err := NewSignedClient(knot, secret, s.config.Dev)
if err != nil {
-
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
+
s.pages.Notice(w, "repo", "Failed to reach knot server.")
return
···
} else {
uri = "https"
-
sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, knot, f.OwnerDid(), f.RepoName)
+
sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
sourceAt := f.RepoAt.String()
rkey := s.TID()
+31 -3
appview/state/signer.go
···
"encoding/hex"
"encoding/json"
"fmt"
+
"io"
+
"log"
"net/http"
"net/url"
"time"
···
return &capabilities, nil
}
-
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*http.Response, error) {
+
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoDiffTreeResponse, error) {
const (
Method = "GET"
)
···
req, err := us.newRequest(Method, endpoint, nil)
if err != nil {
-
return nil, err
+
return nil, fmt.Errorf("Failed to create request.")
}
-
return us.client.Do(req)
+
compareResp, err := us.client.Do(req)
+
if err != nil {
+
return nil, fmt.Errorf("Failed to create request.")
+
}
+
defer compareResp.Body.Close()
+
+
switch compareResp.StatusCode {
+
case 404:
+
case 400:
+
return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
+
}
+
+
respBody, err := io.ReadAll(compareResp.Body)
+
if err != nil {
+
log.Println("failed to compare across branches")
+
return nil, fmt.Errorf("Failed to compare branches.")
+
}
+
defer compareResp.Body.Close()
+
+
var diffTreeResponse types.RepoDiffTreeResponse
+
err = json.Unmarshal(respBody, &diffTreeResponse)
+
if err != nil {
+
log.Println("failed to unmarshal diff tree response", err)
+
return nil, fmt.Errorf("Failed to compare branches.")
+
}
+
+
return &diffTreeResponse, nil
}
+74
docs/contributing.md
···
+
# tangled contributing guide
+
+
## commit guidelines
+
+
We follow a commit style similar to the Go project. Please keep commits:
+
+
* **atomic**: each commit should represent one logical change
+
* **descriptive**: the commit message should clearly describe what the
+
change does and why it's needed
+
+
### message format
+
+
```
+
<service/top-level directory>: <package/path>: <short summary of change>
+
+
+
Optional longer description, if needed. Explain what the change does and
+
why, especially if not obvious. Reference relevant issues or PRs when
+
applicable. These can be links for now since we don't auto-link
+
issues/PRs yet.
+
```
+
+
Here are some examples:
+
+
```
+
appview: state: fix token expiry check in middleware
+
+
The previous check did not account for clock drift, leading to premature
+
token invalidation.
+
```
+
+
```
+
knotserver: git/service: improve error checking in upload-pack
+
```
+
+
### general notes
+
+
- PRs get merged as a single commit, so keep PRs small and focused. Use
+
the above guidelines for the PR title and description.
+
- Use the imperative mood in the summary line (e.g., "fix bug" not
+
"fixed bug" or "fixes bug").
+
- Try to keep the summary line under 72 characters, but we aren't too
+
fussed about this.
+
- Don't include unrelated changes in the same commit.
+
- Avoid noisy commit messages like "wip" or "final fix"โ€”rewrite history
+
before submitting if necessary.
+
+
## proposals for bigger changes
+
+
Small fixes like typos, minor bugs, or trivial refactors can be
+
submitted directly as PRs.
+
+
For larger changesโ€”especially those introducing new features,
+
significant refactoring, or altering system behaviorโ€”please open a
+
proposal first. This helps us evaluate the scope, design, and potential
+
impact before implementation.
+
+
### proposal format
+
+
Create a new issue titled:
+
+
```
+
proposal: <affected scope>: <summary of change>
+
```
+
+
In the description, explain:
+
+
- What the change is
+
- Why it's needed
+
- How you plan to implement it (roughly)
+
- Any open questions or tradeoffs
+
+
We'll use the issue thread to discuss and refine the idea before moving
+
forward.
+108
docs/knot-hosting.md
···
+
# knot self-hosting guide
+
+
So you want to run your own knot server? Great! Here are a few prerequisites:
+
+
1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux of some kind.
+
2. A (sub)domain name. People generally use `knot.example.com`.
+
3. A valid SSL certificate for your domain.
+
+
There's a couple of ways to get started:
+
* NixOS: refer to [flake.nix](https://tangled.sh/@tangled.sh/core/blob/master/flake.nix)
+
* Docker: Documented below.
+
* Manual: Documented below.
+
+
## docker setup
+
+
Clone this repository:
+
+
```
+
git clone https://tangled.sh/@tangled.sh/core
+
```
+
+
Modify the `docker/docker-compose.yml`, specifically the
+
`KNOT_SERVER_SECRET` and `KNOT_SERVER_HOSTNAME` env vars. Then run:
+
+
```
+
docker compose -f docker/docker-compose.yml up
+
```
+
+
## manual setup
+
+
First, clone this repository:
+
+
```
+
git clone https://tangled.sh/@tangled.sh/core
+
```
+
+
Then, build our binaries (you need to have Go installed):
+
* `knotserver`: the main server program
+
* `keyfetch`: utility to fetch ssh pubkeys
+
* `repoguard`: enforces repository access control
+
+
```
+
cd core
+
export CGO_ENABLED=1
+
go build -o knot ./cmd/knotserver
+
go build -o keyfetch ./cmd/keyfetch
+
go build -o repoguard ./cmd/repoguard
+
```
+
+
Next, move the `keyfetch` binary to a location owned by `root` --
+
`/usr/local/libexec/tangled-keyfetch` is a good choice:
+
+
```
+
sudo mv keyfetch /usr/local/libexec/tangled-keyfetch
+
sudo chown root:root /usr/local/libexec/tangled-keyfetch
+
sudo chmod 755 /usr/local/libexec/tangled-keyfetch
+
```
+
+
This is necessary because SSH `AuthorizedKeysCommand` requires [really specific
+
permissions](https://stackoverflow.com/a/27638306). Let's set that up:
+
+
```
+
sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
+
Match User git
+
AuthorizedKeysCommand /usr/local/libexec/tangled-keyfetch
+
AuthorizedKeysCommandUser nobody
+
EOF
+
```
+
+
Next, create the `git` user:
+
+
```
+
sudo adduser git
+
```
+
+
Copy the `repoguard` binary to the `git` user's home directory:
+
+
```
+
sudo cp repoguard /home/git
+
sudo chown git:git /home/git/repoguard
+
```
+
+
Now, let's set up the server. Copy the `knot` binary to
+
`/usr/local/bin/knotserver`. Then, create `/home/git/.knot.env` with the
+
following, updating the values as necessary. The `KNOT_SERVER_SECRET` can be
+
obtaind from the [/knots](/knots) page on Tangled.
+
+
```
+
KNOT_REPO_SCAN_PATH=/home/git
+
KNOT_SERVER_HOSTNAME=knot.example.com
+
APPVIEW_ENDPOINT=https://tangled.sh
+
KNOT_SERVER_SECRET=secret
+
KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
+
KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
+
```
+
+
If you run a Linux distribution that uses systemd, you can use the provided
+
service file to run the server. Copy
+
[`knotserver.service`](https://tangled.sh/did:plc:wshs7t2adsemcrrd4snkeqli/core/blob/master/systemd/knotserver.service)
+
to `/etc/systemd/system/`. Then, run:
+
+
```
+
systemctl enable knotserver
+
systemctl start knotserver
+
```
+
+
You should now have a running knot server! You can finalize your registration by hitting the
+
`initialize` button on the [/knots](/knots) page.
+8 -4
flake.nix
···
flake = false;
};
ibm-plex-mono-src = {
-
url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip";
-
flake = false;
+
url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip";
+
flake = false;
};
gitignore = {
url = "github:hercules-ci/gitignore.nix";
···
virtualisation.cores = 2;
services.getty.autologinUser = "root";
environment.systemPackages = with pkgs; [curl vim git];
-
systemd.tmpfiles.rules = [
-
"w /var/lib/knotserver/secret 0660 git git - KNOT_SERVER_SECRET=6995e040e80e2d593b5e5e9ca611a70140b9ef8044add0a28b48b1ee34aa3e85"
+
systemd.tmpfiles.rules = let
+
u = config.services.tangled-knotserver.gitUser;
+
g = config.services.tangled-knotserver.gitUser;
+
in [
+
"d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first
+
"f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=5b42390da4c6659f34c9a545adebd8af82c4a19960d735f651e3d582623ba9f2"
];
services.tangled-knotserver = {
enable = true;
+8
knotserver/routes.go
···
return
}
+
// add perms for this user to access the repo
+
err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
+
if err != nil {
+
l.Error("adding repo permissions", "error", err.Error())
+
writeError(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
w.WriteHeader(http.StatusNoContent)
}
+1 -17
rbac/rbac.go
···
import (
"database/sql"
"fmt"
-
"path"
"strings"
adapter "github.com/Blank-Xu/sql-adapter"
···
e = some(where (p.eft == allow))
[matchers]
-
m = r.act == p.act && r.dom == p.dom && keyMatch2(r.obj, p.obj) && g(r.sub, p.sub, r.dom)
+
m = r.act == p.act && r.dom == p.dom && r.obj == p.obj && g(r.sub, p.sub, r.dom)
`
)
···
E *casbin.Enforcer
}
-
func keyMatch2(key1 string, key2 string) bool {
-
matched, _ := path.Match(key2, key1)
-
return matched
-
}
-
func NewEnforcer(path string) (*Enforcer, error) {
m, err := model.NewModelFromString(Model)
if err != nil {
···
}
e.EnableAutoSave(false)
-
-
e.AddFunction("keyMatch2", keyMatch2Func)
return &Enforcer{e}, nil
}
···
}
return permissions
-
}
-
-
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
-
func keyMatch2Func(args ...interface{}) (interface{}, error) {
-
name1 := args[0].(string)
-
name2 := args[1].(string)
-
-
return keyMatch2(name1, name2), nil
}
func checkRepoFormat(repo string) error {
+8 -105
readme.md
···
Read the introduction to Tangled [here](https://blog.tangled.sh/intro).
-
## knot self-hosting guide
+
## docs
-
So you want to run your own knot server? Great! Here are a few prerequisites:
+
* [knot hosting
+
guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md)
+
* [contributing
+
guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/contributing.md)&mdash;**read this before opening a PR!**
-
1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux of some kind.
-
2. A (sub)domain name. People generally use `knot.example.com`.
-
3. A valid SSL certificate for your domain.
+
## security
-
There's a couple of ways to get started:
-
* NixOS: refer to [flake.nix](https://tangled.sh/@tangled.sh/core/blob/master/flake.nix)
-
* Docker: Documented below.
-
* Manual: Documented below.
-
-
## docker setup
-
-
Clone this repository:
-
-
```
-
git clone https://tangled.sh/@tangled.sh/core
-
```
-
-
Modify the `docker/docker-compose.yml`, specifically the
-
`KNOT_SERVER_SECRET` and `KNOT_SERVER_HOSTNAME` env vars. Then run:
-
-
```
-
docker compose -f docker/docker-compose.yml up
-
```
-
-
### manual setup
-
-
First, clone this repository:
-
-
```
-
git clone https://tangled.sh/@tangled.sh/core
-
```
-
-
Then, build our binaries (you need to have Go installed):
-
* `knotserver`: the main server program
-
* `keyfetch`: utility to fetch ssh pubkeys
-
* `repoguard`: enforces repository access control
-
-
```
-
cd core
-
export CGO_ENABLED=1
-
go build -o knot ./cmd/knotserver
-
go build -o keyfetch ./cmd/keyfetch
-
go build -o repoguard ./cmd/repoguard
-
```
-
-
Next, move the `keyfetch` binary to a location owned by `root` --
-
`/usr/local/libexec/tangled-keyfetch` is a good choice:
-
-
```
-
sudo mv keyfetch /usr/local/libexec/tangled-keyfetch
-
sudo chown root:root /usr/local/libexec/tangled-keyfetch
-
sudo chmod 755 /usr/local/libexec/tangled-keyfetch
-
```
-
-
This is necessary because SSH `AuthorizedKeysCommand` requires [really specific
-
permissions](https://stackoverflow.com/a/27638306). Let's set that up:
-
-
```
-
sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
-
Match User git
-
AuthorizedKeysCommand /usr/local/libexec/tangled-keyfetch
-
AuthorizedKeysCommandUser nobody
-
EOF
-
```
-
-
Next, create the `git` user:
-
-
```
-
sudo adduser git
-
```
-
-
Copy the `repoguard` binary to the `git` user's home directory:
-
-
```
-
sudo cp repoguard /home/git
-
sudo chown git:git /home/git/repoguard
-
```
-
-
Now, let's set up the server. Copy the `knot` binary to
-
`/usr/local/bin/knotserver`. Then, create `/home/git/.knot.env` with the
-
following, updating the values as necessary. The `KNOT_SERVER_SECRET` can be
-
obtaind from the [/knots](/knots) page on Tangled.
-
-
```
-
KNOT_REPO_SCAN_PATH=/home/git
-
KNOT_SERVER_HOSTNAME=knot.example.com
-
APPVIEW_ENDPOINT=https://tangled.sh
-
KNOT_SERVER_SECRET=secret
-
KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
-
KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
-
```
-
-
If you run a Linux distribution that uses systemd, you can use the provided
-
service file to run the server. Copy
-
[`knotserver.service`](https://tangled.sh/did:plc:wshs7t2adsemcrrd4snkeqli/core/blob/master/systemd/knotserver.service)
-
to `/etc/systemd/system/`. Then, run:
-
-
```
-
systemctl enable knotserver
-
systemctl start knotserver
-
```
-
-
You should now have a running knot server! You can finalize your registration by hitting the
-
`initialize` button on the [/knots](/knots) page.
+
If you've identified a security issue in Tangled, please email
+
[security@tangled.sh](mailto:security@tangled.sh) with details!
+68 -45
tailwind.config.js
···
const colors = require("tailwindcss/colors");
module.exports = {
-
content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go"],
-
darkMode: "media",
-
theme: {
-
container: {
-
padding: "2rem",
-
center: true,
-
screens: {
-
sm: "500px",
-
md: "600px",
-
lg: "800px",
-
xl: "1000px",
-
"2xl": "1200px",
-
},
-
},
-
extend: {
-
fontFamily: {
-
sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"],
-
mono: [
-
"IBMPlexMono",
-
"ui-monospace",
-
"SFMono-Regular",
-
"Menlo",
-
"Monaco",
-
"Consolas",
-
"Liberation Mono",
-
"Courier New",
-
"monospace",
-
],
-
},
-
typography: {
-
DEFAULT: {
-
css: {
-
maxWidth: "none",
-
pre: {
-
backgroundColor: colors.gray[100],
-
color: colors.black,
-
"@apply dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border":
-
{},
-
},
-
},
-
},
-
},
-
},
-
},
-
plugins: [require("@tailwindcss/typography")],
+
content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go"],
+
darkMode: "media",
+
theme: {
+
container: {
+
padding: "2rem",
+
center: true,
+
screens: {
+
sm: "500px",
+
md: "600px",
+
lg: "800px",
+
xl: "1000px",
+
"2xl": "1200px",
+
},
+
},
+
extend: {
+
fontFamily: {
+
sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"],
+
mono: [
+
"IBMPlexMono",
+
"ui-monospace",
+
"SFMono-Regular",
+
"Menlo",
+
"Monaco",
+
"Consolas",
+
"Liberation Mono",
+
"Courier New",
+
"monospace",
+
],
+
},
+
typography: {
+
DEFAULT: {
+
css: {
+
maxWidth: "none",
+
pre: {
+
backgroundColor: colors.gray[100],
+
color: colors.black,
+
"@apply font-normal text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border": {},
+
},
+
code: {
+
"@apply font-normal font-mono p-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700": {},
+
},
+
"code::before": {
+
content: '""',
+
},
+
"code::after": {
+
content: '""',
+
},
+
blockquote: {
+
quotes: "none",
+
},
+
'h1, h2, h3, h4': {
+
"@apply mt-4 mb-2": {}
+
},
+
h1: {
+
"@apply mt-3 pb-3 border-b border-gray-300 dark:border-gray-600": {}
+
},
+
h2: {
+
"@apply mt-3 pb-3 border-b border-gray-200 dark:border-gray-700": {}
+
},
+
h3: {
+
"@apply mt-2": {}
+
},
+
},
+
},
+
},
+
},
+
},
+
plugins: [require("@tailwindcss/typography")],
};
+14
types/diff.go
···
IsRename bool `json:"is_rename"`
}
+
type DiffStat struct {
+
Insertions int64
+
Deletions int64
+
}
+
+
func (d *Diff) Stats() DiffStat {
+
var stats DiffStat
+
for _, f := range d.TextFragments {
+
stats.Insertions += f.LinesAdded
+
stats.Deletions += f.LinesDeleted
+
}
+
return stats
+
}
+
// A nicer git diff representation.
type NiceDiff struct {
Commit struct {