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

Compare changes

Choose any two refs to compare.

Changed files
+676 -392
api
appview
knotserver
lexicons
nix
modules
spindle
config
types
+13
.editorconfig
··· 1 + root = true 2 + 3 + [*.html] 4 + indent_size = 2 5 + 6 + [*.json] 7 + indent_size = 2 8 + 9 + [*.nix] 10 + indent_size = 2 11 + 12 + [*.yml] 13 + indent_size = 2
+13 -1
api/tangled/repoblob.go
··· 30 30 // RepoBlob_Output is the output of a sh.tangled.repo.blob call. 31 31 type RepoBlob_Output struct { 32 32 // content: File content (base64 encoded for binary files) 33 - Content string `json:"content" cborgen:"content"` 33 + Content *string `json:"content,omitempty" cborgen:"content,omitempty"` 34 34 // encoding: Content encoding 35 35 Encoding *string `json:"encoding,omitempty" cborgen:"encoding,omitempty"` 36 36 // isBinary: Whether the file is binary ··· 44 44 Ref string `json:"ref" cborgen:"ref"` 45 45 // size: File size in bytes 46 46 Size *int64 `json:"size,omitempty" cborgen:"size,omitempty"` 47 + // submodule: Submodule information if path is a submodule 48 + Submodule *RepoBlob_Submodule `json:"submodule,omitempty" cborgen:"submodule,omitempty"` 47 49 } 48 50 49 51 // RepoBlob_Signature is a "signature" in the sh.tangled.repo.blob schema. ··· 54 56 Name string `json:"name" cborgen:"name"` 55 57 // when: Author timestamp 56 58 When string `json:"when" cborgen:"when"` 59 + } 60 + 61 + // RepoBlob_Submodule is a "submodule" in the sh.tangled.repo.blob schema. 62 + type RepoBlob_Submodule struct { 63 + // branch: Branch to track in the submodule 64 + Branch *string `json:"branch,omitempty" cborgen:"branch,omitempty"` 65 + // name: Submodule name 66 + Name string `json:"name" cborgen:"name"` 67 + // url: Submodule repository URL 68 + Url string `json:"url" cborgen:"url"` 57 69 } 58 70 59 71 // RepoBlob calls the XRPC method "sh.tangled.repo.blob".
-4
api/tangled/repotree.go
··· 47 47 48 48 // RepoTree_TreeEntry is a "treeEntry" in the sh.tangled.repo.tree schema. 49 49 type RepoTree_TreeEntry struct { 50 - // is_file: Whether this entry is a file 51 - Is_file bool `json:"is_file" cborgen:"is_file"` 52 - // is_subtree: Whether this entry is a directory/subtree 53 - Is_subtree bool `json:"is_subtree" cborgen:"is_subtree"` 54 50 Last_commit *RepoTree_LastCommit `json:"last_commit,omitempty" cborgen:"last_commit,omitempty"` 55 51 // mode: File mode 56 52 Mode string `json:"mode" cborgen:"mode"`
+2
appview/middleware/middleware.go
··· 206 206 return func(next http.Handler) http.Handler { 207 207 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 208 208 repoName := chi.URLParam(req, "repo") 209 + repoName = strings.TrimSuffix(repoName, ".git") 210 + 209 211 id, ok := req.Context().Value("resolvedId").(identity.Identity) 210 212 if !ok { 211 213 log.Println("malformed middleware")
+47
appview/models/repo.go
··· 104 104 Repo *Repo 105 105 Issues []Issue 106 106 } 107 + 108 + type BlobContentType int 109 + 110 + const ( 111 + BlobContentTypeCode BlobContentType = iota 112 + BlobContentTypeMarkup 113 + BlobContentTypeImage 114 + BlobContentTypeSvg 115 + BlobContentTypeVideo 116 + BlobContentTypeSubmodule 117 + ) 118 + 119 + func (ty BlobContentType) IsCode() bool { return ty == BlobContentTypeCode } 120 + func (ty BlobContentType) IsMarkup() bool { return ty == BlobContentTypeMarkup } 121 + func (ty BlobContentType) IsImage() bool { return ty == BlobContentTypeImage } 122 + func (ty BlobContentType) IsSvg() bool { return ty == BlobContentTypeSvg } 123 + func (ty BlobContentType) IsVideo() bool { return ty == BlobContentTypeVideo } 124 + func (ty BlobContentType) IsSubmodule() bool { return ty == BlobContentTypeSubmodule } 125 + 126 + type BlobView struct { 127 + HasTextView bool // can show as code/text 128 + HasRenderedView bool // can show rendered (markup/image/video/submodule) 129 + HasRawView bool // can download raw (everything except submodule) 130 + 131 + // current display mode 132 + ShowingRendered bool // currently in rendered mode 133 + ShowingText bool // currently in text/code mode 134 + 135 + // content type flags 136 + ContentType BlobContentType 137 + 138 + // Content data 139 + Contents string 140 + ContentSrc string // URL for media files 141 + Lines int 142 + SizeHint uint64 143 + } 144 + 145 + // if both views are available, then show a toggle between them 146 + func (b BlobView) ShowToggle() bool { 147 + return b.HasTextView && b.HasRenderedView 148 + } 149 + 150 + func (b BlobView) IsUnsupported() bool { 151 + // no view available, only raw 152 + return !(b.HasRenderedView || b.HasTextView) 153 + }
+41
appview/pages/funcmap.go
··· 1 1 package pages 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "crypto/hmac" 6 7 "crypto/sha256" ··· 17 18 "strings" 18 19 "time" 19 20 21 + "github.com/alecthomas/chroma/v2" 22 + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 23 + "github.com/alecthomas/chroma/v2/lexers" 24 + "github.com/alecthomas/chroma/v2/styles" 20 25 "github.com/bluesky-social/indigo/atproto/syntax" 21 26 "github.com/dustin/go-humanize" 22 27 "github.com/go-enry/go-enry/v2" ··· 245 250 htmlString := p.rctx.RenderMarkdown(text) 246 251 sanitized := p.rctx.SanitizeDescription(htmlString) 247 252 return template.HTML(sanitized) 253 + }, 254 + "readme": func(text string) template.HTML { 255 + p.rctx.RendererType = markup.RendererTypeRepoMarkdown 256 + htmlString := p.rctx.RenderMarkdown(text) 257 + sanitized := p.rctx.SanitizeDefault(htmlString) 258 + return template.HTML(sanitized) 259 + }, 260 + "code": func(content, path string) string { 261 + var style *chroma.Style = styles.Get("catpuccin-latte") 262 + formatter := chromahtml.New( 263 + chromahtml.InlineCode(false), 264 + chromahtml.WithLineNumbers(true), 265 + chromahtml.WithLinkableLineNumbers(true, "L"), 266 + chromahtml.Standalone(false), 267 + chromahtml.WithClasses(true), 268 + ) 269 + 270 + lexer := lexers.Get(filepath.Base(path)) 271 + if lexer == nil { 272 + lexer = lexers.Fallback 273 + } 274 + 275 + iterator, err := lexer.Tokenise(nil, content) 276 + if err != nil { 277 + p.logger.Error("chroma tokenize", "err", "err") 278 + return "" 279 + } 280 + 281 + var code bytes.Buffer 282 + err = formatter.Format(&code, style, iterator) 283 + if err != nil { 284 + p.logger.Error("chroma format", "err", "err") 285 + return "" 286 + } 287 + 288 + return code.String() 248 289 }, 249 290 "trimUriScheme": func(text string) string { 250 291 text = strings.TrimPrefix(text, "https://")
+10 -98
appview/pages/pages.go
··· 1 1 package pages 2 2 3 3 import ( 4 - "bytes" 5 4 "crypto/sha256" 6 5 "embed" 7 6 "encoding/hex" ··· 29 28 "tangled.org/core/patchutil" 30 29 "tangled.org/core/types" 31 30 32 - "github.com/alecthomas/chroma/v2" 33 - chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 34 - "github.com/alecthomas/chroma/v2/lexers" 35 - "github.com/alecthomas/chroma/v2/styles" 36 31 "github.com/bluesky-social/indigo/atproto/identity" 37 32 "github.com/bluesky-social/indigo/atproto/syntax" 38 33 "github.com/go-git/go-git/v5/plumbing" ··· 744 739 func (r RepoTreeParams) TreeStats() RepoTreeStats { 745 740 numFolders, numFiles := 0, 0 746 741 for _, f := range r.Files { 747 - if !f.IsFile { 742 + if !f.IsFile() { 748 743 numFolders += 1 749 - } else if f.IsFile { 744 + } else if f.IsFile() { 750 745 numFiles += 1 751 746 } 752 747 } ··· 817 812 } 818 813 819 814 type RepoBlobParams struct { 820 - LoggedInUser *oauth.User 821 - RepoInfo repoinfo.RepoInfo 822 - Active string 823 - Unsupported bool 824 - IsImage bool 825 - IsVideo bool 826 - ContentSrc string 827 - BreadCrumbs [][]string 828 - ShowRendered bool 829 - RenderToggle bool 830 - RenderedContents template.HTML 815 + LoggedInUser *oauth.User 816 + RepoInfo repoinfo.RepoInfo 817 + Active string 818 + BreadCrumbs [][]string 819 + BlobView models.BlobView 831 820 *tangled.RepoBlob_Output 832 - // Computed fields for template compatibility 833 - Contents string 834 - Lines int 835 - SizeHint uint64 836 - IsBinary bool 837 821 } 838 822 839 823 func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 840 - var style *chroma.Style = styles.Get("catpuccin-latte") 841 - 842 - if params.ShowRendered { 843 - switch markup.GetFormat(params.Path) { 844 - case markup.FormatMarkdown: 845 - p.rctx.RepoInfo = params.RepoInfo 846 - p.rctx.RendererType = markup.RendererTypeRepoMarkdown 847 - htmlString := p.rctx.RenderMarkdown(params.Contents) 848 - sanitized := p.rctx.SanitizeDefault(htmlString) 849 - params.RenderedContents = template.HTML(sanitized) 850 - } 851 - } 852 - 853 - c := params.Contents 854 - formatter := chromahtml.New( 855 - chromahtml.InlineCode(false), 856 - chromahtml.WithLineNumbers(true), 857 - chromahtml.WithLinkableLineNumbers(true, "L"), 858 - chromahtml.Standalone(false), 859 - chromahtml.WithClasses(true), 860 - ) 861 - 862 - lexer := lexers.Get(filepath.Base(params.Path)) 863 - if lexer == nil { 864 - lexer = lexers.Fallback 865 - } 866 - 867 - iterator, err := lexer.Tokenise(nil, c) 868 - if err != nil { 869 - return fmt.Errorf("chroma tokenize: %w", err) 870 - } 871 - 872 - var code bytes.Buffer 873 - err = formatter.Format(&code, style, iterator) 874 - if err != nil { 875 - return fmt.Errorf("chroma format: %w", err) 824 + switch params.BlobView.ContentType { 825 + case models.BlobContentTypeMarkup: 826 + p.rctx.RepoInfo = params.RepoInfo 876 827 } 877 828 878 - params.Contents = code.String() 879 829 params.Active = "overview" 880 830 return p.executeRepo("repo/blob", w, params) 881 831 } ··· 1432 1382 } 1433 1383 1434 1384 func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1435 - var style *chroma.Style = styles.Get("catpuccin-latte") 1436 - 1437 - if params.ShowRendered { 1438 - switch markup.GetFormat(params.String.Filename) { 1439 - case markup.FormatMarkdown: 1440 - p.rctx.RendererType = markup.RendererTypeRepoMarkdown 1441 - htmlString := p.rctx.RenderMarkdown(params.String.Contents) 1442 - sanitized := p.rctx.SanitizeDefault(htmlString) 1443 - params.RenderedContents = template.HTML(sanitized) 1444 - } 1445 - } 1446 - 1447 - c := params.String.Contents 1448 - formatter := chromahtml.New( 1449 - chromahtml.InlineCode(false), 1450 - chromahtml.WithLineNumbers(true), 1451 - chromahtml.WithLinkableLineNumbers(true, "L"), 1452 - chromahtml.Standalone(false), 1453 - chromahtml.WithClasses(true), 1454 - ) 1455 - 1456 - lexer := lexers.Get(filepath.Base(params.String.Filename)) 1457 - if lexer == nil { 1458 - lexer = lexers.Fallback 1459 - } 1460 - 1461 - iterator, err := lexer.Tokenise(nil, c) 1462 - if err != nil { 1463 - return fmt.Errorf("chroma tokenize: %w", err) 1464 - } 1465 - 1466 - var code bytes.Buffer 1467 - err = formatter.Format(&code, style, iterator) 1468 - if err != nil { 1469 - return fmt.Errorf("chroma format: %w", err) 1470 - } 1471 - 1472 - params.String.Contents = code.String() 1473 1385 return p.execute("strings/string", w, params) 1474 1386 } 1475 1387
+62 -39
appview/pages/templates/repo/blob.html
··· 11 11 {{ end }} 12 12 13 13 {{ define "repoContent" }} 14 - {{ $lines := split .Contents }} 15 - {{ $tot_lines := len $lines }} 16 - {{ $tot_chars := len (printf "%d" $tot_lines) }} 17 - {{ $code_number_style := "text-gray-400 dark:text-gray-500 left-0 bg-white dark:bg-gray-800 text-right mr-6 select-none inline-block w-12" }} 18 14 {{ $linkstyle := "no-underline hover:underline" }} 19 15 <div class="pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700"> 20 16 <div class="flex flex-col md:flex-row md:justify-between gap-2"> ··· 36 32 </div> 37 33 <div id="file-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0"> 38 34 <span>at <a href="/{{ .RepoInfo.FullName }}/tree/{{ .Ref }}">{{ .Ref }}</a></span> 39 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 40 - <span>{{ .Lines }} lines</span> 41 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 42 - <span>{{ byteFmt .SizeHint }}</span> 43 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 44 - <a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a> 45 - {{ if .RenderToggle }} 46 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 47 - <a 48 - href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}" 49 - hx-boost="true" 50 - >view {{ if .ShowRendered }}code{{ else }}rendered{{ end }}</a> 35 + 36 + {{ if .BlobView.ShowingText }} 37 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 38 + <span>{{ .Lines }} lines</span> 39 + {{ end }} 40 + 41 + {{ if .BlobView.SizeHint }} 42 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 43 + <span>{{ byteFmt .BlobView.SizeHint }}</span> 44 + {{ end }} 45 + 46 + {{ if .BlobView.HasRawView }} 47 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 48 + <a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a> 49 + {{ end }} 50 + 51 + {{ if .BlobView.ShowToggle }} 52 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 53 + <a href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .BlobView.ShowingRendered }}" hx-boost="true"> 54 + view {{ if .BlobView.ShowingRendered }}code{{ else }}rendered{{ end }} 55 + </a> 51 56 {{ end }} 52 57 </div> 53 58 </div> 54 59 </div> 55 - {{ if and .IsBinary .Unsupported }} 56 - <p class="text-center text-gray-400 dark:text-gray-500"> 57 - Previews are not supported for this file type. 58 - </p> 59 - {{ else if .IsBinary }} 60 - <div class="text-center"> 61 - {{ if .IsImage }} 62 - <img src="{{ .ContentSrc }}" 63 - alt="{{ .Path }}" 64 - class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded" /> 65 - {{ else if .IsVideo }} 66 - <video controls class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded"> 67 - <source src="{{ .ContentSrc }}"> 68 - Your browser does not support the video tag. 69 - </video> 70 - {{ end }} 71 - </div> 72 - {{ else }} 73 - <div class="overflow-auto relative"> 74 - {{ if .ShowRendered }} 75 - <div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div> 60 + {{ if .BlobView.IsUnsupported }} 61 + <p class="text-center text-gray-400 dark:text-gray-500"> 62 + Previews are not supported for this file type. 63 + </p> 64 + {{ else if .BlobView.ContentType.IsSubmodule }} 65 + <p class="text-center text-gray-400 dark:text-gray-500"> 66 + This directory is a git submodule of <a href="{{ .BlobView.ContentSrc }}">{{ .BlobView.ContentSrc }}</a>. 67 + </p> 68 + {{ else if .BlobView.ContentType.IsImage }} 69 + <div class="text-center"> 70 + <img src="{{ .BlobView.ContentSrc }}" 71 + alt="{{ .Path }}" 72 + class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded" /> 73 + </div> 74 + {{ else if .BlobView.ContentType.IsVideo }} 75 + <div class="text-center"> 76 + <video controls class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded"> 77 + <source src="{{ .BlobView.ContentSrc }}"> 78 + Your browser does not support the video tag. 79 + </video> 80 + </div> 81 + {{ else if .BlobView.ContentType.IsSvg }} 82 + <div class="overflow-auto relative"> 83 + {{ if .BlobView.ShowingRendered }} 84 + <div class="text-center"> 85 + <img src="{{ .BlobView.ContentSrc }}" 86 + alt="{{ .Path }}" 87 + class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded" /> 88 + </div> 76 89 {{ else }} 77 - <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ $.Contents | escapeHtml }}</div> 90 + <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ code .BlobView.Contents .Path | escapeHtml }}</div> 91 + {{ end }} 92 + </div> 93 + {{ else if .BlobView.ContentType.IsMarkup }} 94 + <div class="overflow-auto relative"> 95 + {{ if .BlobView.ShowingRendered }} 96 + <div id="blob-contents" class="prose dark:prose-invert">{{ .BlobView.Contents | readme }}</div> 97 + {{ else }} 98 + <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ code .BlobView.Contents .Path | escapeHtml }}</div> 78 99 {{ end }} 79 - </div> 100 + </div> 101 + {{ else if .BlobView.ContentType.IsCode }} 102 + <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ code .BlobView.Contents .Path | escapeHtml }}</div> 80 103 {{ end }} 81 104 {{ template "fragments/multiline-select" }} 82 105 {{ end }}
+8 -1
appview/pages/templates/repo/index.html
··· 35 35 {{ end }} 36 36 37 37 {{ define "repoLanguages" }} 38 - <details class="group -m-6 mb-4"> 38 + <details class="group -my-4 -m-6 mb-4"> 39 39 <summary class="flex gap-[1px] h-4 scale-y-50 hover:scale-y-100 origin-top group-open:scale-y-100 transition-all hover:cursor-pointer overflow-hidden rounded-t"> 40 40 {{ range $value := .Languages }} 41 41 <div ··· 129 129 {{ $icon := "folder" }} 130 130 {{ $iconStyle := "size-4 fill-current" }} 131 131 132 + {{ if .IsSubmodule }} 133 + {{ $link = printf "/%s/%s/%s/%s" $.RepoInfo.FullName "blob" (urlquery $.Ref) .Name }} 134 + {{ $icon = "folder-input" }} 135 + {{ $iconStyle = "size-4" }} 136 + {{ end }} 137 + 132 138 {{ if .IsFile }} 133 139 {{ $link = printf "/%s/%s/%s/%s" $.RepoInfo.FullName "blob" (urlquery $.Ref) .Name }} 134 140 {{ $icon = "file" }} 135 141 {{ $iconStyle = "size-4" }} 136 142 {{ end }} 143 + 137 144 <a href="{{ $link }}" class="{{ $linkstyle }}"> 138 145 <div class="flex items-center gap-2"> 139 146 {{ i $icon $iconStyle "flex-shrink-0" }}
+81 -83
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 22 22 {{ $isLastRound := eq $roundNumber $lastIdx }} 23 23 {{ $isSameRepoBranch := .Pull.IsBranchBased }} 24 24 {{ $isUpToDate := .ResubmitCheck.No }} 25 - <div class="relative w-fit"> 26 - <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2"> 27 - <button 28 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 29 - hx-target="#actions-{{$roundNumber}}" 30 - hx-swap="outerHtml" 31 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group"> 32 - {{ i "message-square-plus" "w-4 h-4" }} 33 - <span>comment</span> 34 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 35 - </button> 36 - {{ if .BranchDeleteStatus }} 37 - <button 38 - hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches" 39 - hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }' 40 - hx-swap="none" 41 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 42 - {{ i "git-branch" "w-4 h-4" }} 43 - <span>delete branch</span> 44 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 45 - </button> 46 - {{ end }} 47 - {{ if and $isPushAllowed $isOpen $isLastRound }} 48 - {{ $disabled := "" }} 49 - {{ if $isConflicted }} 50 - {{ $disabled = "disabled" }} 51 - {{ end }} 52 - <button 53 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 54 - hx-swap="none" 55 - hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 56 - class="btn p-2 flex items-center gap-2 group" {{ $disabled }}> 57 - {{ i "git-merge" "w-4 h-4" }} 58 - <span>merge{{if $stackCount}} {{$stackCount}}{{end}}</span> 59 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 60 - </button> 61 - {{ end }} 25 + <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative"> 26 + <button 27 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 28 + hx-target="#actions-{{$roundNumber}}" 29 + hx-swap="outerHtml" 30 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group"> 31 + {{ i "message-square-plus" "w-4 h-4" }} 32 + <span>comment</span> 33 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 34 + </button> 35 + {{ if .BranchDeleteStatus }} 36 + <button 37 + hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches" 38 + hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }' 39 + hx-swap="none" 40 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 41 + {{ i "git-branch" "w-4 h-4" }} 42 + <span>delete branch</span> 43 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 44 + </button> 45 + {{ end }} 46 + {{ if and $isPushAllowed $isOpen $isLastRound }} 47 + {{ $disabled := "" }} 48 + {{ if $isConflicted }} 49 + {{ $disabled = "disabled" }} 50 + {{ end }} 51 + <button 52 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 53 + hx-swap="none" 54 + hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 55 + class="btn p-2 flex items-center gap-2 group" {{ $disabled }}> 56 + {{ i "git-merge" "w-4 h-4" }} 57 + <span>merge{{if $stackCount}} {{$stackCount}}{{end}}</span> 58 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 59 + </button> 60 + {{ end }} 62 61 63 - {{ if and $isPullAuthor $isOpen $isLastRound }} 64 - {{ $disabled := "" }} 65 - {{ if $isUpToDate }} 66 - {{ $disabled = "disabled" }} 62 + {{ if and $isPullAuthor $isOpen $isLastRound }} 63 + {{ $disabled := "" }} 64 + {{ if $isUpToDate }} 65 + {{ $disabled = "disabled" }} 66 + {{ end }} 67 + <button id="resubmitBtn" 68 + {{ if not .Pull.IsPatchBased }} 69 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 70 + {{ else }} 71 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 72 + hx-target="#actions-{{$roundNumber}}" 73 + hx-swap="outerHtml" 67 74 {{ end }} 68 - <button id="resubmitBtn" 69 - {{ if not .Pull.IsPatchBased }} 70 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 71 - {{ else }} 72 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 73 - hx-target="#actions-{{$roundNumber}}" 74 - hx-swap="outerHtml" 75 - {{ end }} 76 75 77 - hx-disabled-elt="#resubmitBtn" 78 - class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 76 + hx-disabled-elt="#resubmitBtn" 77 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 79 78 80 - {{ if $disabled }} 81 - title="Update this branch to resubmit this pull request" 82 - {{ else }} 83 - title="Resubmit this pull request" 84 - {{ end }} 85 - > 86 - {{ i "rotate-ccw" "w-4 h-4" }} 87 - <span>resubmit</span> 88 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 89 - </button> 90 - {{ end }} 79 + {{ if $disabled }} 80 + title="Update this branch to resubmit this pull request" 81 + {{ else }} 82 + title="Resubmit this pull request" 83 + {{ end }} 84 + > 85 + {{ i "rotate-ccw" "w-4 h-4" }} 86 + <span>resubmit</span> 87 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 + </button> 89 + {{ end }} 91 90 92 - {{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }} 93 - <button 94 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 95 - hx-swap="none" 96 - class="btn p-2 flex items-center gap-2 group"> 97 - {{ i "ban" "w-4 h-4" }} 98 - <span>close</span> 99 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 100 - </button> 101 - {{ end }} 91 + {{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }} 92 + <button 93 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 94 + hx-swap="none" 95 + class="btn p-2 flex items-center gap-2 group"> 96 + {{ i "ban" "w-4 h-4" }} 97 + <span>close</span> 98 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 99 + </button> 100 + {{ end }} 102 101 103 - {{ if and (or $isPullAuthor $isPushAllowed) $isClosed $isLastRound }} 104 - <button 105 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 106 - hx-swap="none" 107 - class="btn p-2 flex items-center gap-2 group"> 108 - {{ i "refresh-ccw-dot" "w-4 h-4" }} 109 - <span>reopen</span> 110 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 111 - </button> 112 - {{ end }} 113 - </div> 102 + {{ if and (or $isPullAuthor $isPushAllowed) $isClosed $isLastRound }} 103 + <button 104 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 105 + hx-swap="none" 106 + class="btn p-2 flex items-center gap-2 group"> 107 + {{ i "refresh-ccw-dot" "w-4 h-4" }} 108 + <span>reopen</span> 109 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 110 + </button> 111 + {{ end }} 114 112 </div> 115 113 {{ end }} 116 114
+8
appview/pages/templates/repo/tree.html
··· 59 59 {{ $icon := "folder" }} 60 60 {{ $iconStyle := "size-4 fill-current" }} 61 61 62 + {{ if .IsSubmodule }} 63 + {{ $link = printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "blob" (urlquery $.Ref) $.TreePath .Name }} 64 + {{ $icon = "folder-input" }} 65 + {{ $iconStyle = "size-4" }} 66 + {{ end }} 67 + 62 68 {{ if .IsFile }} 69 + {{ $link = printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "blob" (urlquery $.Ref) $.TreePath .Name }} 63 70 {{ $icon = "file" }} 64 71 {{ $iconStyle = "size-4" }} 65 72 {{ end }} 73 + 66 74 <a href="{{ $link }}" class="{{ $linkstyle }}"> 67 75 <div class="flex items-center gap-2"> 68 76 {{ i $icon $iconStyle "flex-shrink-0" }}
+2 -2
appview/pages/templates/strings/string.html
··· 75 75 </div> 76 76 <div class="overflow-x-auto overflow-y-hidden relative"> 77 77 {{ if .ShowRendered }} 78 - <div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div> 78 + <div id="blob-contents" class="prose dark:prose-invert">{{ .String.Contents | readme }}</div> 79 79 {{ else }} 80 - <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div> 80 + <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ code .String.Contents .String.Filename | escapeHtml }}</div> 81 81 {{ end }} 82 82 </div> 83 83 {{ template "fragments/multiline-select" }}
+136 -64
appview/repo/blob.go
··· 1 1 package repo 2 2 3 3 import ( 4 + "encoding/base64" 4 5 "fmt" 5 6 "io" 6 7 "net/http" ··· 10 11 "strings" 11 12 12 13 "tangled.org/core/api/tangled" 14 + "tangled.org/core/appview/config" 15 + "tangled.org/core/appview/models" 13 16 "tangled.org/core/appview/pages" 14 17 "tangled.org/core/appview/pages/markup" 18 + "tangled.org/core/appview/reporesolver" 15 19 xrpcclient "tangled.org/core/appview/xrpcclient" 16 20 17 21 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 22 "github.com/go-chi/chi/v5" 19 23 ) 20 24 25 + // the content can be one of the following: 26 + // 27 + // - code : text | | raw 28 + // - markup : text | rendered | raw 29 + // - svg : text | rendered | raw 30 + // - png : | rendered | raw 31 + // - video : | rendered | raw 32 + // - submodule : | rendered | 33 + // - rest : | | 21 34 func (rp *Repo) Blob(w http.ResponseWriter, r *http.Request) { 22 35 l := rp.logger.With("handler", "RepoBlob") 36 + 23 37 f, err := rp.repoResolver.Resolve(r) 24 38 if err != nil { 25 39 l.Error("failed to get repo and knot", "err", err) 26 40 return 27 41 } 42 + 28 43 ref := chi.URLParam(r, "ref") 29 44 ref, _ = url.PathUnescape(ref) 45 + 30 46 filePath := chi.URLParam(r, "*") 31 47 filePath, _ = url.PathUnescape(filePath) 48 + 32 49 scheme := "http" 33 50 if !rp.config.Core.Dev { 34 51 scheme = "https" ··· 44 61 rp.pages.Error503(w) 45 62 return 46 63 } 64 + 47 65 // Use XRPC response directly instead of converting to internal types 48 66 var breadcrumbs [][]string 49 67 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) ··· 52 70 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 53 71 } 54 72 } 55 - showRendered := false 56 - renderToggle := false 57 - if markup.GetFormat(resp.Path) == markup.FormatMarkdown { 58 - renderToggle = true 59 - showRendered = r.URL.Query().Get("code") != "true" 60 - } 61 - var unsupported bool 62 - var isImage bool 63 - var isVideo bool 64 - var contentSrc string 65 - if resp.IsBinary != nil && *resp.IsBinary { 66 - ext := strings.ToLower(filepath.Ext(resp.Path)) 67 - switch ext { 68 - case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp": 69 - isImage = true 70 - case ".mp4", ".webm", ".ogg", ".mov", ".avi": 71 - isVideo = true 72 - default: 73 - unsupported = true 74 - } 75 - // fetch the raw binary content using sh.tangled.repo.blob xrpc 76 - repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 77 - baseURL := &url.URL{ 78 - Scheme: scheme, 79 - Host: f.Knot, 80 - Path: "/xrpc/sh.tangled.repo.blob", 81 - } 82 - query := baseURL.Query() 83 - query.Set("repo", repoName) 84 - query.Set("ref", ref) 85 - query.Set("path", filePath) 86 - query.Set("raw", "true") 87 - baseURL.RawQuery = query.Encode() 88 - blobURL := baseURL.String() 89 - contentSrc = blobURL 90 - if !rp.config.Core.Dev { 91 - contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) 92 - } 93 - } 94 - lines := 0 95 - if resp.IsBinary == nil || !*resp.IsBinary { 96 - lines = strings.Count(resp.Content, "\n") + 1 97 - } 98 - var sizeHint uint64 99 - if resp.Size != nil { 100 - sizeHint = uint64(*resp.Size) 101 - } else { 102 - sizeHint = uint64(len(resp.Content)) 103 - } 73 + 74 + // Create the blob view 75 + blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query()) 76 + 104 77 user := rp.oauth.GetUser(r) 105 - // Determine if content is binary (dereference pointer) 106 - isBinary := false 107 - if resp.IsBinary != nil { 108 - isBinary = *resp.IsBinary 109 - } 78 + 110 79 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 111 80 LoggedInUser: user, 112 81 RepoInfo: f.RepoInfo(user), 113 82 BreadCrumbs: breadcrumbs, 114 - ShowRendered: showRendered, 115 - RenderToggle: renderToggle, 116 - Unsupported: unsupported, 117 - IsImage: isImage, 118 - IsVideo: isVideo, 119 - ContentSrc: contentSrc, 83 + BlobView: blobView, 120 84 RepoBlob_Output: resp, 121 - Contents: resp.Content, 122 - Lines: lines, 123 - SizeHint: sizeHint, 124 - IsBinary: isBinary, 125 85 }) 126 86 } 127 87 128 88 func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 129 89 l := rp.logger.With("handler", "RepoBlobRaw") 90 + 130 91 f, err := rp.repoResolver.Resolve(r) 131 92 if err != nil { 132 93 l.Error("failed to get repo and knot", "err", err) 133 94 w.WriteHeader(http.StatusBadRequest) 134 95 return 135 96 } 97 + 136 98 ref := chi.URLParam(r, "ref") 137 99 ref, _ = url.PathUnescape(ref) 100 + 138 101 filePath := chi.URLParam(r, "*") 139 102 filePath, _ = url.PathUnescape(filePath) 103 + 140 104 scheme := "http" 141 105 if !rp.config.Core.Dev { 142 106 scheme = "https" ··· 159 123 l.Error("failed to create request", "err", err) 160 124 return 161 125 } 126 + 162 127 // forward the If-None-Match header 163 128 if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { 164 129 req.Header.Set("If-None-Match", clientETag) 165 130 } 166 131 client := &http.Client{} 132 + 167 133 resp, err := client.Do(req) 168 134 if err != nil { 169 135 l.Error("failed to reach knotserver", "err", err) 170 136 rp.pages.Error503(w) 171 137 return 172 138 } 139 + 173 140 defer resp.Body.Close() 141 + 174 142 // forward 304 not modified 175 143 if resp.StatusCode == http.StatusNotModified { 176 144 w.WriteHeader(http.StatusNotModified) 177 145 return 178 146 } 147 + 179 148 if resp.StatusCode != http.StatusOK { 180 149 l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode) 181 150 w.WriteHeader(resp.StatusCode) 182 151 _, _ = io.Copy(w, resp.Body) 183 152 return 184 153 } 154 + 185 155 contentType := resp.Header.Get("Content-Type") 186 156 body, err := io.ReadAll(resp.Body) 187 157 if err != nil { ··· 189 159 w.WriteHeader(http.StatusInternalServerError) 190 160 return 191 161 } 162 + 192 163 if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) { 193 164 // serve all textual content as text/plain 194 165 w.Header().Set("Content-Type", "text/plain; charset=utf-8") ··· 202 173 w.Write([]byte("unsupported content type")) 203 174 return 204 175 } 176 + } 177 + 178 + // NewBlobView creates a BlobView from the XRPC response 179 + func NewBlobView(resp *tangled.RepoBlob_Output, config *config.Config, f *reporesolver.ResolvedRepo, ref, filePath string, queryParams url.Values) models.BlobView { 180 + view := models.BlobView{ 181 + Contents: "", 182 + Lines: 0, 183 + } 184 + 185 + // Set size 186 + if resp.Size != nil { 187 + view.SizeHint = uint64(*resp.Size) 188 + } else if resp.Content != nil { 189 + view.SizeHint = uint64(len(*resp.Content)) 190 + } 191 + 192 + if resp.Submodule != nil { 193 + view.ContentType = models.BlobContentTypeSubmodule 194 + view.HasRenderedView = true 195 + view.ContentSrc = resp.Submodule.Url 196 + return view 197 + } 198 + 199 + // Determine if binary 200 + if resp.IsBinary != nil && *resp.IsBinary { 201 + view.ContentSrc = generateBlobURL(config, f, ref, filePath) 202 + ext := strings.ToLower(filepath.Ext(resp.Path)) 203 + 204 + switch ext { 205 + case ".jpg", ".jpeg", ".png", ".gif", ".webp": 206 + view.ContentType = models.BlobContentTypeImage 207 + view.HasRawView = true 208 + view.HasRenderedView = true 209 + view.ShowingRendered = true 210 + 211 + case ".svg": 212 + view.ContentType = models.BlobContentTypeSvg 213 + view.HasRawView = true 214 + view.HasTextView = true 215 + view.HasRenderedView = true 216 + view.ShowingRendered = queryParams.Get("code") != "true" 217 + if resp.Content != nil { 218 + bytes, _ := base64.StdEncoding.DecodeString(*resp.Content) 219 + view.Contents = string(bytes) 220 + view.Lines = strings.Count(view.Contents, "\n") + 1 221 + } 222 + 223 + case ".mp4", ".webm", ".ogg", ".mov", ".avi": 224 + view.ContentType = models.BlobContentTypeVideo 225 + view.HasRawView = true 226 + view.HasRenderedView = true 227 + view.ShowingRendered = true 228 + } 229 + 230 + return view 231 + } 232 + 233 + // otherwise, we are dealing with text content 234 + view.HasRawView = true 235 + view.HasTextView = true 236 + 237 + if resp.Content != nil { 238 + view.Contents = *resp.Content 239 + view.Lines = strings.Count(view.Contents, "\n") + 1 240 + } 241 + 242 + // with text, we may be dealing with markdown 243 + format := markup.GetFormat(resp.Path) 244 + if format == markup.FormatMarkdown { 245 + view.ContentType = models.BlobContentTypeMarkup 246 + view.HasRenderedView = true 247 + view.ShowingRendered = queryParams.Get("code") != "true" 248 + } 249 + 250 + return view 251 + } 252 + 253 + func generateBlobURL(config *config.Config, f *reporesolver.ResolvedRepo, ref, filePath string) string { 254 + scheme := "http" 255 + if !config.Core.Dev { 256 + scheme = "https" 257 + } 258 + 259 + repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 260 + baseURL := &url.URL{ 261 + Scheme: scheme, 262 + Host: f.Knot, 263 + Path: "/xrpc/sh.tangled.repo.blob", 264 + } 265 + query := baseURL.Query() 266 + query.Set("repo", repoName) 267 + query.Set("ref", ref) 268 + query.Set("path", filePath) 269 + query.Set("raw", "true") 270 + baseURL.RawQuery = query.Encode() 271 + blobURL := baseURL.String() 272 + 273 + if !config.Core.Dev { 274 + return markup.GenerateCamoURL(config.Camo.Host, config.Camo.SharedSecret, blobURL) 275 + } 276 + return blobURL 205 277 } 206 278 207 279 func isTextualMimeType(mimeType string) bool {
+4 -5
appview/repo/index.go
··· 351 351 if treeResp != nil && treeResp.Files != nil { 352 352 for _, file := range treeResp.Files { 353 353 niceFile := types.NiceTree{ 354 - IsFile: file.Is_file, 355 - IsSubtree: file.Is_subtree, 356 - Name: file.Name, 357 - Mode: file.Mode, 358 - Size: file.Size, 354 + Name: file.Name, 355 + Mode: file.Mode, 356 + Size: file.Size, 359 357 } 358 + 360 359 if file.Last_commit != nil { 361 360 when, _ := time.Parse(time.RFC3339, file.Last_commit.When) 362 361 niceFile.LastCommit = &types.LastCommitInfo{
-2
appview/repo/repo.go
··· 78 78 } 79 79 } 80 80 81 - // isTextualMimeType returns true if the MIME type represents textual content 82 - 83 81 // modify the spindle configured for this repo 84 82 func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 85 83 user := rp.oauth.GetUser(r)
+2 -2
appview/repo/repo_util.go
··· 17 17 18 18 func sortFiles(files []types.NiceTree) { 19 19 sort.Slice(files, func(i, j int) bool { 20 - iIsFile := files[i].IsFile 21 - jIsFile := files[j].IsFile 20 + iIsFile := files[i].IsFile() 21 + jIsFile := files[j].IsFile() 22 22 if iIsFile != jIsFile { 23 23 return !iIsFile 24 24 }
+1 -1
appview/repo/settings.go
··· 374 374 ) 375 375 376 376 err = rp.validator.ValidateURI(website) 377 - if err != nil { 377 + if website != "" && err != nil { 378 378 l.Error("invalid uri", "err", err) 379 379 rp.pages.Notice(w, noticeId, err.Error()) 380 380 return
+4 -5
appview/repo/tree.go
··· 50 50 files := make([]types.NiceTree, len(xrpcResp.Files)) 51 51 for i, xrpcFile := range xrpcResp.Files { 52 52 file := types.NiceTree{ 53 - Name: xrpcFile.Name, 54 - Mode: xrpcFile.Mode, 55 - Size: int64(xrpcFile.Size), 56 - IsFile: xrpcFile.Is_file, 57 - IsSubtree: xrpcFile.Is_subtree, 53 + Name: xrpcFile.Name, 54 + Mode: xrpcFile.Mode, 55 + Size: int64(xrpcFile.Size), 58 56 } 59 57 // Convert last commit info if present 60 58 if xrpcFile.Last_commit != nil { ··· 97 95 } 98 96 } 99 97 sortFiles(result.Files) 98 + 100 99 rp.pages.RepoTree(w, pages.RepoTreeParams{ 101 100 LoggedInUser: user, 102 101 BreadCrumbs: breadcrumbs,
+11 -8
appview/state/router.go
··· 57 57 if userutil.IsFlattenedDid(firstPart) { 58 58 unflattenedDid := userutil.UnflattenDid(firstPart) 59 59 redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/") 60 - http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 60 + 61 + redirectURL := *r.URL 62 + redirectURL.Path = "/" + redirectPath 63 + 64 + http.Redirect(w, r, redirectURL.String(), http.StatusFound) 61 65 return 62 66 } 63 67 64 68 // if using a handle with @, rewrite to work without @ 65 69 if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) { 66 70 redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/") 67 - http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 71 + 72 + redirectURL := *r.URL 73 + redirectURL.Path = "/" + redirectPath 74 + 75 + http.Redirect(w, r, redirectURL.String(), http.StatusFound) 68 76 return 69 77 } 78 + 70 79 } 71 80 72 81 standardRouter.ServeHTTP(w, r) ··· 81 90 r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) { 82 91 r.Get("/", s.Profile) 83 92 r.Get("/feed.atom", s.AtomFeedPage) 84 - 85 - // redirect /@handle/repo.git -> /@handle/repo 86 - r.Get("/{repo}.git", func(w http.ResponseWriter, r *http.Request) { 87 - nonDotGitPath := strings.TrimSuffix(r.URL.Path, ".git") 88 - http.Redirect(w, r, nonDotGitPath, http.StatusMovedPermanently) 89 - }) 90 93 91 94 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 92 95 r.Use(mw.GoImport())
+4 -12
go.mod
··· 7 7 github.com/alecthomas/assert/v2 v2.11.0 8 8 github.com/alecthomas/chroma/v2 v2.15.0 9 9 github.com/avast/retry-go/v4 v4.6.1 10 + github.com/blevesearch/bleve/v2 v2.5.3 10 11 github.com/bluekeyes/go-gitdiff v0.8.1 11 12 github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 12 13 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 14 + github.com/bmatcuk/doublestar/v4 v4.9.1 13 15 github.com/carlmjohnson/versioninfo v0.22.5 14 16 github.com/casbin/casbin/v2 v2.103.0 17 + github.com/charmbracelet/log v0.4.2 15 18 github.com/cloudflare/cloudflare-go v0.115.0 16 19 github.com/cyphar/filepath-securejoin v0.4.1 17 20 github.com/dgraph-io/ristretto v0.2.0 ··· 29 32 github.com/hiddeco/sshsig v0.2.0 30 33 github.com/hpcloud/tail v1.0.0 31 34 github.com/ipfs/go-cid v0.5.0 32 - github.com/lestrrat-go/jwx/v2 v2.1.6 33 35 github.com/mattn/go-sqlite3 v1.14.24 34 36 github.com/microcosm-cc/bluemonday v1.0.27 35 37 github.com/openbao/openbao/api/v2 v2.3.0 ··· 45 47 github.com/wyatt915/goldmark-treeblood v0.0.1 46 48 github.com/yuin/goldmark v1.7.13 47 49 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 48 51 golang.org/x/crypto v0.40.0 49 52 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b 50 53 golang.org/x/image v0.31.0 ··· 65 68 github.com/aymerick/douceur v0.2.0 // indirect 66 69 github.com/beorn7/perks v1.0.1 // indirect 67 70 github.com/bits-and-blooms/bitset v1.22.0 // indirect 68 - github.com/blevesearch/bleve/v2 v2.5.3 // indirect 69 71 github.com/blevesearch/bleve_index_api v1.2.8 // indirect 70 72 github.com/blevesearch/geo v0.2.4 // indirect 71 73 github.com/blevesearch/go-faiss v1.0.25 // indirect ··· 83 85 github.com/blevesearch/zapx/v14 v14.4.2 // indirect 84 86 github.com/blevesearch/zapx/v15 v15.4.2 // indirect 85 87 github.com/blevesearch/zapx/v16 v16.2.4 // indirect 86 - github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect 87 88 github.com/casbin/govaluate v1.3.0 // indirect 88 89 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 89 90 github.com/cespare/xxhash/v2 v2.3.0 // indirect 90 91 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 91 92 github.com/charmbracelet/lipgloss v1.1.0 // indirect 92 - github.com/charmbracelet/log v0.4.2 // indirect 93 93 github.com/charmbracelet/x/ansi v0.8.0 // indirect 94 94 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 95 95 github.com/charmbracelet/x/term v0.2.1 // indirect ··· 98 98 github.com/containerd/errdefs/pkg v0.3.0 // indirect 99 99 github.com/containerd/log v0.1.0 // indirect 100 100 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 101 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 102 101 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 103 102 github.com/distribution/reference v0.6.0 // indirect 104 103 github.com/dlclark/regexp2 v1.11.5 // indirect ··· 152 151 github.com/kevinburke/ssh_config v1.2.0 // indirect 153 152 github.com/klauspost/compress v1.18.0 // indirect 154 153 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 155 - github.com/lestrrat-go/blackmagic v1.0.4 // indirect 156 - github.com/lestrrat-go/httpcc v1.0.1 // indirect 157 - github.com/lestrrat-go/httprc v1.0.6 // indirect 158 - github.com/lestrrat-go/iter v1.0.2 // indirect 159 - github.com/lestrrat-go/option v1.0.1 // indirect 160 154 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 161 155 github.com/mattn/go-isatty v0.0.20 // indirect 162 156 github.com/mattn/go-runewidth v0.0.16 // indirect ··· 191 185 github.com/prometheus/procfs v0.16.1 // indirect 192 186 github.com/rivo/uniseg v0.4.7 // indirect 193 187 github.com/ryanuber/go-glob v1.0.0 // indirect 194 - github.com/segmentio/asm v1.2.0 // indirect 195 188 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 196 189 github.com/spaolacci/murmur3 v1.1.0 // indirect 197 190 github.com/vmihailenco/go-tinylfu v0.2.2 // indirect ··· 199 192 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 200 193 github.com/wyatt915/treeblood v0.1.16 // indirect 201 194 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 202 - gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect 203 195 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 204 196 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 205 197 go.etcd.io/bbolt v1.4.0 // indirect
-17
go.sum
··· 71 71 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= 72 72 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 73 73 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 74 - github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= 75 74 github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 76 75 github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= 77 76 github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= ··· 126 125 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 127 126 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 128 127 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 129 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 130 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 131 128 github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= 132 129 github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 133 130 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= ··· 330 327 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 331 328 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 332 329 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 333 - github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= 334 - github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= 335 - github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 336 - github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 337 - github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= 338 - github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 339 - github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 340 - github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 341 - github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= 342 - github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= 343 - github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 344 - github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 345 330 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 346 331 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 347 332 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= ··· 466 451 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 467 452 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 468 453 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 469 - github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 470 - github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 471 454 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 472 455 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 473 456 github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog=
+60 -2
knotserver/git/git.go
··· 3 3 import ( 4 4 "archive/tar" 5 5 "bytes" 6 + "errors" 6 7 "fmt" 7 8 "io" 8 9 "io/fs" ··· 12 13 "time" 13 14 14 15 "github.com/go-git/go-git/v5" 16 + "github.com/go-git/go-git/v5/config" 15 17 "github.com/go-git/go-git/v5/plumbing" 16 18 "github.com/go-git/go-git/v5/plumbing/object" 17 19 ) 18 20 19 21 var ( 20 - ErrBinaryFile = fmt.Errorf("binary file") 21 - ErrNotBinaryFile = fmt.Errorf("not binary file") 22 + ErrBinaryFile = errors.New("binary file") 23 + ErrNotBinaryFile = errors.New("not binary file") 24 + ErrMissingGitModules = errors.New("no .gitmodules file found") 25 + ErrInvalidGitModules = errors.New("invalid .gitmodules file") 26 + ErrNotSubmodule = errors.New("path is not a submodule") 22 27 ) 23 28 24 29 type GitRepo struct { ··· 188 193 defer reader.Close() 189 194 190 195 return io.ReadAll(reader) 196 + } 197 + 198 + // read and parse .gitmodules 199 + func (g *GitRepo) Submodules() (*config.Modules, error) { 200 + c, err := g.r.CommitObject(g.h) 201 + if err != nil { 202 + return nil, fmt.Errorf("commit object: %w", err) 203 + } 204 + 205 + tree, err := c.Tree() 206 + if err != nil { 207 + return nil, fmt.Errorf("tree: %w", err) 208 + } 209 + 210 + // read .gitmodules file 211 + modulesEntry, err := tree.FindEntry(".gitmodules") 212 + if err != nil { 213 + return nil, fmt.Errorf("%w: %w", ErrMissingGitModules, err) 214 + } 215 + 216 + modulesFile, err := tree.TreeEntryFile(modulesEntry) 217 + if err != nil { 218 + return nil, fmt.Errorf("%w: failed to read file: %w", ErrInvalidGitModules, err) 219 + } 220 + 221 + content, err := modulesFile.Contents() 222 + if err != nil { 223 + return nil, fmt.Errorf("%w: failed to read contents: %w", ErrInvalidGitModules, err) 224 + } 225 + 226 + // parse .gitmodules 227 + modules := config.NewModules() 228 + if err = modules.Unmarshal([]byte(content)); err != nil { 229 + return nil, fmt.Errorf("%w: failed to parse: %w", ErrInvalidGitModules, err) 230 + } 231 + 232 + return modules, nil 233 + } 234 + 235 + func (g *GitRepo) Submodule(path string) (*config.Submodule, error) { 236 + modules, err := g.Submodules() 237 + if err != nil { 238 + return nil, err 239 + } 240 + 241 + for _, submodule := range modules.Submodules { 242 + if submodule.Path == path { 243 + return submodule, nil 244 + } 245 + } 246 + 247 + // path is not a submodule 248 + return nil, ErrNotSubmodule 191 249 } 192 250 193 251 func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
+4 -13
knotserver/git/tree.go
··· 7 7 "path" 8 8 "time" 9 9 10 + "github.com/go-git/go-git/v5/plumbing/filemode" 10 11 "github.com/go-git/go-git/v5/plumbing/object" 11 12 "tangled.org/core/types" 12 13 ) ··· 53 54 } 54 55 55 56 for _, e := range subtree.Entries { 56 - mode, _ := e.Mode.ToOSFileMode() 57 57 sz, _ := subtree.Size(e.Name) 58 - 59 58 fpath := path.Join(parent, e.Name) 60 59 61 60 var lastCommit *types.LastCommitInfo ··· 69 68 70 69 nts = append(nts, types.NiceTree{ 71 70 Name: e.Name, 72 - Mode: mode.String(), 73 - IsFile: e.Mode.IsFile(), 71 + Mode: e.Mode.String(), 74 72 Size: sz, 75 73 LastCommit: lastCommit, 76 74 }) ··· 126 124 default: 127 125 } 128 126 129 - mode, err := e.Mode.ToOSFileMode() 130 - if err != nil { 131 - // TODO: log this 132 - continue 133 - } 134 - 135 127 if e.Mode.IsFile() { 136 - err = cb(e, currentTree, root) 137 - if errors.Is(err, TerminateWalk) { 128 + if err := cb(e, currentTree, root); errors.Is(err, TerminateWalk) { 138 129 return err 139 130 } 140 131 } 141 132 142 133 // e is a directory 143 - if mode.IsDir() { 134 + if e.Mode == filemode.Dir { 144 135 subtree, err := currentTree.Tree(e.Name) 145 136 if err != nil { 146 137 return fmt.Errorf("sub tree %s: %w", e.Name, err)
+1 -1
knotserver/ingester.go
··· 161 161 162 162 var pipeline workflow.RawPipeline 163 163 for _, e := range workflowDir { 164 - if !e.IsFile { 164 + if !e.IsFile() { 165 165 continue 166 166 } 167 167
+1 -1
knotserver/internal.go
··· 277 277 278 278 var pipeline workflow.RawPipeline 279 279 for _, e := range workflowDir { 280 - if !e.IsFile { 280 + if !e.IsFile() { 281 281 continue 282 282 } 283 283
+21 -2
knotserver/xrpc/repo_blob.go
··· 42 42 return 43 43 } 44 44 45 + // first check if this path is a submodule 46 + submodule, err := gr.Submodule(treePath) 47 + if err != nil { 48 + // this is okay, continue and try to treat it as a regular file 49 + } else { 50 + response := tangled.RepoBlob_Output{ 51 + Ref: ref, 52 + Path: treePath, 53 + Submodule: &tangled.RepoBlob_Submodule{ 54 + Name: submodule.Name, 55 + Url: submodule.URL, 56 + Branch: &submodule.Branch, 57 + }, 58 + } 59 + writeJson(w, response) 60 + return 61 + } 62 + 45 63 contents, err := gr.RawContent(treePath) 46 64 if err != nil { 47 65 x.Logger.Error("file content", "error", err.Error(), "treePath", treePath) ··· 101 119 var encoding string 102 120 103 121 isBinary := !isTextual(mimeType) 122 + size := int64(len(contents)) 104 123 105 124 if isBinary { 106 125 content = base64.StdEncoding.EncodeToString(contents) ··· 113 132 response := tangled.RepoBlob_Output{ 114 133 Ref: ref, 115 134 Path: treePath, 116 - Content: content, 135 + Content: &content, 117 136 Encoding: &encoding, 118 - Size: &[]int64{int64(len(contents))}[0], 137 + Size: &size, 119 138 IsBinary: &isBinary, 120 139 } 121 140
+3 -5
knotserver/xrpc/repo_tree.go
··· 67 67 treeEntries := make([]*tangled.RepoTree_TreeEntry, len(files)) 68 68 for i, file := range files { 69 69 entry := &tangled.RepoTree_TreeEntry{ 70 - Name: file.Name, 71 - Mode: file.Mode, 72 - Size: file.Size, 73 - Is_file: file.IsFile, 74 - Is_subtree: file.IsSubtree, 70 + Name: file.Name, 71 + Mode: file.Mode, 72 + Size: file.Size, 75 73 } 76 74 77 75 if file.LastCommit != nil {
+49 -5
lexicons/repo/blob.json
··· 6 6 "type": "query", 7 7 "parameters": { 8 8 "type": "params", 9 - "required": ["repo", "ref", "path"], 9 + "required": [ 10 + "repo", 11 + "ref", 12 + "path" 13 + ], 10 14 "properties": { 11 15 "repo": { 12 16 "type": "string", ··· 31 35 "encoding": "application/json", 32 36 "schema": { 33 37 "type": "object", 34 - "required": ["ref", "path", "content"], 38 + "required": [ 39 + "ref", 40 + "path" 41 + ], 35 42 "properties": { 36 43 "ref": { 37 44 "type": "string", ··· 48 55 "encoding": { 49 56 "type": "string", 50 57 "description": "Content encoding", 51 - "enum": ["utf-8", "base64"] 58 + "enum": [ 59 + "utf-8", 60 + "base64" 61 + ] 52 62 }, 53 63 "size": { 54 64 "type": "integer", ··· 61 71 "mimeType": { 62 72 "type": "string", 63 73 "description": "MIME type of the file" 74 + }, 75 + "submodule": { 76 + "type": "ref", 77 + "ref": "#submodule", 78 + "description": "Submodule information if path is a submodule" 64 79 }, 65 80 "lastCommit": { 66 81 "type": "ref", ··· 90 105 }, 91 106 "lastCommit": { 92 107 "type": "object", 93 - "required": ["hash", "message", "when"], 108 + "required": [ 109 + "hash", 110 + "message", 111 + "when" 112 + ], 94 113 "properties": { 95 114 "hash": { 96 115 "type": "string", ··· 117 136 }, 118 137 "signature": { 119 138 "type": "object", 120 - "required": ["name", "email", "when"], 139 + "required": [ 140 + "name", 141 + "email", 142 + "when" 143 + ], 121 144 "properties": { 122 145 "name": { 123 146 "type": "string", ··· 131 154 "type": "string", 132 155 "format": "datetime", 133 156 "description": "Author timestamp" 157 + } 158 + } 159 + }, 160 + "submodule": { 161 + "type": "object", 162 + "required": [ 163 + "name", 164 + "url" 165 + ], 166 + "properties": { 167 + "name": { 168 + "type": "string", 169 + "description": "Submodule name" 170 + }, 171 + "url": { 172 + "type": "string", 173 + "description": "Submodule repository URL" 174 + }, 175 + "branch": { 176 + "type": "string", 177 + "description": "Branch to track in the submodule" 134 178 } 135 179 } 136 180 }
+1 -9
lexicons/repo/tree.json
··· 91 91 }, 92 92 "treeEntry": { 93 93 "type": "object", 94 - "required": ["name", "mode", "size", "is_file", "is_subtree"], 94 + "required": ["name", "mode", "size"], 95 95 "properties": { 96 96 "name": { 97 97 "type": "string", ··· 104 104 "size": { 105 105 "type": "integer", 106 106 "description": "File size in bytes" 107 - }, 108 - "is_file": { 109 - "type": "boolean", 110 - "description": "Whether this entry is a file" 111 - }, 112 - "is_subtree": { 113 - "type": "boolean", 114 - "description": "Whether this entry is a directory/subtree" 115 107 }, 116 108 "last_commit": { 117 109 "type": "ref",
+56 -2
nix/modules/knot.nix
··· 51 51 description = "Path where repositories are scanned from"; 52 52 }; 53 53 54 + readme = mkOption { 55 + type = types.listOf types.str; 56 + default = [ 57 + "README.md" 58 + "readme.md" 59 + "README" 60 + "readme" 61 + "README.markdown" 62 + "readme.markdown" 63 + "README.txt" 64 + "readme.txt" 65 + "README.rst" 66 + "readme.rst" 67 + "README.org" 68 + "readme.org" 69 + "README.asciidoc" 70 + "readme.asciidoc" 71 + ]; 72 + description = "List of README filenames to look for (in priority order)"; 73 + }; 74 + 54 75 mainBranch = mkOption { 55 76 type = types.str; 56 77 default = "main"; ··· 58 79 }; 59 80 }; 60 81 82 + git = { 83 + userName = mkOption { 84 + type = types.str; 85 + default = "Tangled"; 86 + description = "Git user name used as committer"; 87 + }; 88 + 89 + userEmail = mkOption { 90 + type = types.str; 91 + default = "noreply@tangled.org"; 92 + description = "Git user email used as committer"; 93 + }; 94 + }; 95 + 61 96 motd = mkOption { 62 97 type = types.nullOr types.str; 63 98 default = null; ··· 123 158 description = "Jetstream endpoint to subscribe to"; 124 159 }; 125 160 161 + logDids = mkOption { 162 + type = types.bool; 163 + default = true; 164 + description = "Enable logging of DIDs"; 165 + }; 166 + 126 167 dev = mkOption { 127 168 type = types.bool; 128 169 default = false; ··· 190 231 mkdir -p "${cfg.stateDir}/.config/git" 191 232 cat > "${cfg.stateDir}/.config/git/config" << EOF 192 233 [user] 193 - name = Git User 194 - email = git@example.com 234 + name = ${cfg.git.userName} 235 + email = ${cfg.git.userEmail} 195 236 [receive] 196 237 advertisePushOptions = true 197 238 [uploadpack] ··· 207 248 WorkingDirectory = cfg.stateDir; 208 249 Environment = [ 209 250 "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 251 + "KNOT_REPO_README=${concatStringsSep "," cfg.repo.readme}" 210 252 "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 253 + "KNOT_GIT_USER_NAME=${cfg.git.userName}" 254 + "KNOT_GIT_USER_EMAIL=${cfg.git.userEmail}" 211 255 "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 212 256 "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 213 257 "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" ··· 216 260 "KNOT_SERVER_PLC_URL=${cfg.server.plcUrl}" 217 261 "KNOT_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}" 218 262 "KNOT_SERVER_OWNER=${cfg.server.owner}" 263 + "KNOT_SERVER_LOG_DIDS=${ 264 + if cfg.server.logDids 265 + then "true" 266 + else "false" 267 + }" 268 + "KNOT_SERVER_DEV=${ 269 + if cfg.server.dev 270 + then "true" 271 + else "false" 272 + }" 219 273 ]; 220 274 ExecStart = "${cfg.package}/bin/knot server"; 221 275 Restart = "always";
+1 -1
nix/vm.nix
··· 97 97 enable = true; 98 98 server = { 99 99 owner = envVar "TANGLED_VM_SPINDLE_OWNER"; 100 - hostname = envVarOr "TANGLED_VM_SPINDLE_OWNER" "localhost:6555"; 100 + hostname = envVarOr "TANGLED_VM_SPINDLE_HOST" "localhost:6555"; 101 101 plcUrl = plcUrl; 102 102 jetstreamEndpoint = jetstream; 103 103 listenAddr = "0.0.0.0:6555";
+1 -1
spindle/config/config.go
··· 13 13 DBPath string `env:"DB_PATH, default=spindle.db"` 14 14 Hostname string `env:"HOSTNAME, required"` 15 15 JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 16 - PlcUrl string `env:"PLC_URL, default=plc.directory"` 16 + PlcUrl string `env:"PLC_URL, default=https://plc.directory"` 17 17 Dev bool `env:"DEV, default=false"` 18 18 Owner string `env:"OWNER, required"` 19 19 Secrets Secrets `env:",prefix=SECRETS_"`
+1 -1
types/repo.go
··· 66 66 type Branch struct { 67 67 Reference `json:"reference"` 68 68 Commit *object.Commit `json:"commit,omitempty"` 69 - IsDefault bool `json:"is_deafult,omitempty"` 69 + IsDefault bool `json:"is_default,omitempty"` 70 70 } 71 71 72 72 type RepoTagsResponse struct {
+28 -5
types/tree.go
··· 4 4 "time" 5 5 6 6 "github.com/go-git/go-git/v5/plumbing" 7 + "github.com/go-git/go-git/v5/plumbing/filemode" 7 8 ) 8 9 9 10 // A nicer git tree representation. 10 11 type NiceTree struct { 11 12 // Relative path 12 - Name string `json:"name"` 13 - Mode string `json:"mode"` 14 - Size int64 `json:"size"` 15 - IsFile bool `json:"is_file"` 16 - IsSubtree bool `json:"is_subtree"` 13 + Name string `json:"name"` 14 + Mode string `json:"mode"` 15 + Size int64 `json:"size"` 17 16 18 17 LastCommit *LastCommitInfo `json:"last_commit,omitempty"` 18 + } 19 + 20 + func (t *NiceTree) FileMode() (filemode.FileMode, error) { 21 + return filemode.New(t.Mode) 22 + } 23 + 24 + func (t *NiceTree) IsFile() bool { 25 + m, err := t.FileMode() 26 + 27 + if err != nil { 28 + return false 29 + } 30 + 31 + return m.IsFile() 32 + } 33 + 34 + func (t *NiceTree) IsSubmodule() bool { 35 + m, err := t.FileMode() 36 + 37 + if err != nil { 38 + return false 39 + } 40 + 41 + return m == filemode.Submodule 19 42 } 20 43 21 44 type LastCommitInfo struct {