Experimental browser for the Atmosphere
{ "uri": "at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3lo6nw66s2s22", "cid": "bafyreiaebe7tp4q4du7znuvlqy7gbcko3crezl5jzntstigvnvi2vrxt5q", "value": { "$type": "sh.tangled.repo.pull", "patch": "From 11d58231f7b47295fe026e86c266472a33e5cf97 Mon Sep 17 00:00:00 2001\nFrom: Anirudh Oppiliappan <x@icyphox.sh>\nDate: Thu, 1 May 2025 19:48:23 +0300\nSubject: [PATCH 1/5] appview: pages/markup: render markdown with\n transformations\n\n---\n appview/pages/markup/{readme.go => format.go} | 0\n appview/pages/markup/markdown.go | 75 ++++++++++++++++++-\n 2 files changed, 74 insertions(+), 1 deletion(-)\n rename appview/pages/markup/{readme.go => format.go} (100%)\n\ndiff --git a/appview/pages/markup/readme.go b/appview/pages/markup/format.go\nsimilarity index 100%\nrename from appview/pages/markup/readme.go\nrename to appview/pages/markup/format.go\ndiff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go\nindex a219f3d..191074d 100644\n--- a/appview/pages/markup/markdown.go\n+++ b/appview/pages/markup/markdown.go\n@@ -3,22 +3,95 @@ package markup\n \n import (\n \t\"bytes\"\n+\t\"path\"\n \n \t\"github.com/yuin/goldmark\"\n+\t\"github.com/yuin/goldmark/ast\"\n \t\"github.com/yuin/goldmark/extension\"\n \t\"github.com/yuin/goldmark/parser\"\n+\t\"github.com/yuin/goldmark/text\"\n+\t\"github.com/yuin/goldmark/util\"\n )\n \n-func RenderMarkdown(source string) string {\n+// RendererType defines the type of renderer to use based on context\n+type RendererType int\n+\n+const (\n+\t// RendererTypeRepoMarkdown is for repository documentation markdown files\n+\tRendererTypeRepoMarkdown RendererType = iota\n+\t// RendererTypeIssueComment is for issue comments\n+\tRendererTypeIssueComment\n+\t// RendererTypePullComment is for pull request comments\n+\tRendererTypePullComment\n+\t// RendererTypeDefault is the default renderer with minimal transformations\n+\tRendererTypeDefault\n+)\n+\n+// RenderContext holds the contextual data for rendering markdown.\n+// It can be initialized empty, and that'll skip any transformations\n+// and use the default renderer (RendererTypeDefault).\n+type RenderContext struct {\n+\tRef string\n+\tFullRepoName string\n+\tRendererType RendererType\n+}\n+\n+func (rctx *RenderContext) RenderMarkdown(source string) string {\n \tmd := goldmark.New(\n \t\tgoldmark.WithExtensions(extension.GFM),\n \t\tgoldmark.WithParserOptions(\n \t\t\tparser.WithAutoHeadingID(),\n \t\t),\n \t)\n+\n+\tif rctx != nil {\n+\t\tvar transformers []util.PrioritizedValue\n+\n+\t\ttransformers = append(transformers, util.Prioritized(&MarkdownTransformer{rctx: rctx}, 10000))\n+\n+\t\tmd.Parser().AddOptions(\n+\t\t\tparser.WithASTTransformers(transformers...),\n+\t\t)\n+\t}\n+\n \tvar buf bytes.Buffer\n \tif err := md.Convert([]byte(source), &buf); err != nil {\n \t\treturn source\n \t}\n \treturn buf.String()\n }\n+\n+type MarkdownTransformer struct {\n+\trctx *RenderContext\n+}\n+\n+func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {\n+\t_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {\n+\t\tif !entering {\n+\t\t\treturn ast.WalkContinue, nil\n+\t\t}\n+\n+\t\tswitch a.rctx.RendererType {\n+\t\tcase RendererTypeRepoMarkdown:\n+\t\t\ta.rctx.relativeLinkTransformer(n.(*ast.Link))\n+\t\tcase RendererTypeDefault:\n+\t\t\ta.rctx.relativeLinkTransformer(n.(*ast.Link))\n+\t\t\t// more types here like RendererTypeIssue/Pull etc.\n+\t\t}\n+\n+\t\treturn ast.WalkContinue, nil\n+\t})\n+}\n+\n+func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {\n+\tdst := string(link.Destination)\n+\n+\tif len(dst) == 0 || dst[0] == '#' ||\n+\t\tbytes.Contains(link.Destination, []byte(\"://\")) ||\n+\t\tbytes.HasPrefix(link.Destination, []byte(\"mailto:\")) {\n+\t\treturn\n+\t}\n+\n+\tnewPath := path.Join(\"/\", rctx.FullRepoName, \"tree\", rctx.Ref, dst)\n+\tlink.Destination = []byte(newPath)\n+}\n-- \n2.43.0\n\n\nFrom 9fbb3c31d527d380412e2ad204c9db36c68bba8d Mon Sep 17 00:00:00 2001\nFrom: Anirudh Oppiliappan <x@icyphox.sh>\nDate: Thu, 1 May 2025 19:48:23 +0300\nSubject: [PATCH 2/5] appview: pages/markup: resolve relative links in markdown\n\n---\n appview/pages/funcmap.go | 3 +-\n appview/pages/markup/markdown.go | 15 ++-----\n appview/pages/pages.go | 15 ++++++-\n appview/state/middleware.go | 4 +-\n appview/state/pull.go | 38 ++++++++--------\n appview/state/repo.go | 75 +++++++++++++++-----------------\n appview/state/repo_util.go | 19 +++++++-\n appview/state/signer.go | 15 ++++++-\n 8 files changed, 106 insertions(+), 78 deletions(-)\n\ndiff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go\nindex 490b98a..9024cc7 100644\n--- a/appview/pages/funcmap.go\n+++ b/appview/pages/funcmap.go\n@@ -143,7 +143,8 @@ func funcMap() template.FuncMap {\n \t\t\treturn v.Slice(start, end).Interface()\n \t\t},\n \t\t\"markdown\": func(text string) template.HTML {\n-\t\t\treturn template.HTML(markup.RenderMarkdown(text))\n+\t\t\trctx := &markup.RenderContext{}\n+\t\t\treturn template.HTML(rctx.RenderMarkdown(text))\n \t\t},\n \t\t\"isNil\": func(t any) bool {\n \t\t\t// returns false for other \"zero\" values\ndiff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go\nindex 191074d..6b19bad 100644\n--- a/appview/pages/markup/markdown.go\n+++ b/appview/pages/markup/markdown.go\n@@ -19,17 +19,10 @@ type RendererType int\n const (\n \t// RendererTypeRepoMarkdown is for repository documentation markdown files\n \tRendererTypeRepoMarkdown RendererType = iota\n-\t// RendererTypeIssueComment is for issue comments\n-\tRendererTypeIssueComment\n-\t// RendererTypePullComment is for pull request comments\n-\tRendererTypePullComment\n-\t// RendererTypeDefault is the default renderer with minimal transformations\n-\tRendererTypeDefault\n )\n \n // RenderContext holds the contextual data for rendering markdown.\n-// It can be initialized empty, and that'll skip any transformations\n-// and use the default renderer (RendererTypeDefault).\n+// It can be initialized empty, and that'll skip any transformations.\n type RenderContext struct {\n \tRef string\n \tFullRepoName string\n@@ -73,9 +66,9 @@ func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader,\n \n \t\tswitch a.rctx.RendererType {\n \t\tcase RendererTypeRepoMarkdown:\n-\t\t\ta.rctx.relativeLinkTransformer(n.(*ast.Link))\n-\t\tcase RendererTypeDefault:\n-\t\t\ta.rctx.relativeLinkTransformer(n.(*ast.Link))\n+\t\t\tif v, ok := n.(*ast.Link); ok {\n+\t\t\t\ta.rctx.relativeLinkTransformer(v)\n+\t\t\t}\n \t\t\t// more types here like RendererTypeIssue/Pull etc.\n \t\t}\n \ndiff --git a/appview/pages/pages.go b/appview/pages/pages.go\nindex 2bf3cee..1d0375d 100644\n--- a/appview/pages/pages.go\n+++ b/appview/pages/pages.go\n@@ -366,6 +366,7 @@ type RepoInfo struct {\n \tRoles RolesInRepo\n \tSource *db.Repo\n \tSourceHandle string\n+\tRef string\n \tDisableFork bool\n }\n \n@@ -478,12 +479,17 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {\n \t\treturn p.executeRepo(\"repo/empty\", w, params)\n \t}\n \n+\trctx := markup.RenderContext{\n+\t\tRef: params.RepoInfo.Ref,\n+\t\tFullRepoName: params.RepoInfo.FullName(),\n+\t}\n+\n \tif params.ReadmeFileName != \"\" {\n \t\tvar htmlString string\n \t\text := filepath.Ext(params.ReadmeFileName)\n \t\tswitch ext {\n \t\tcase \".md\", \".markdown\", \".mdown\", \".mkdn\", \".mkd\":\n-\t\t\thtmlString = markup.RenderMarkdown(params.Readme)\n+\t\t\thtmlString = rctx.RenderMarkdown(params.Readme)\n \t\t\tparams.Raw = false\n \t\t\tparams.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString))\n \t\tdefault:\n@@ -601,7 +607,12 @@ func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {\n \tif params.ShowRendered {\n \t\tswitch markup.GetFormat(params.Path) {\n \t\tcase markup.FormatMarkdown:\n-\t\t\tparams.RenderedContents = template.HTML(markup.RenderMarkdown(params.Contents))\n+\t\t\trctx := markup.RenderContext{\n+\t\t\t\tRef: params.RepoInfo.Ref,\n+\t\t\t\tFullRepoName: params.RepoInfo.FullName(),\n+\t\t\t\tRendererType: markup.RendererTypeRepoMarkdown,\n+\t\t\t}\n+\t\t\tparams.RenderedContents = template.HTML(rctx.RenderMarkdown(params.Contents))\n \t\t}\n \t}\n \ndiff --git a/appview/state/middleware.go b/appview/state/middleware.go\nindex 6bafbe1..c3c5cdb 100644\n--- a/appview/state/middleware.go\n+++ b/appview/state/middleware.go\n@@ -61,7 +61,7 @@ func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middlewa\n \t\t\t\thttp.Error(w, \"Forbiden\", http.StatusUnauthorized)\n \t\t\t\treturn\n \t\t\t}\n-\t\t\tf, err := fullyResolvedRepo(r)\n+\t\t\tf, err := s.fullyResolvedRepo(r)\n \t\t\tif err != nil {\n \t\t\t\thttp.Error(w, \"malformed url\", http.StatusBadRequest)\n \t\t\t\treturn\n@@ -148,7 +148,7 @@ func ResolveRepo(s *State) middleware.Middleware {\n func ResolvePull(s *State) middleware.Middleware {\n \treturn func(next http.Handler) http.Handler {\n \t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n-\t\t\tf, err := fullyResolvedRepo(r)\n+\t\t\tf, err := s.fullyResolvedRepo(r)\n \t\t\tif err != nil {\n \t\t\t\tlog.Println(\"failed to fully resolve repo\", err)\n \t\t\t\thttp.Error(w, \"invalid repo url\", http.StatusNotFound)\ndiff --git a/appview/state/pull.go b/appview/state/pull.go\nindex 128b558..db6667d 100644\n--- a/appview/state/pull.go\n+++ b/appview/state/pull.go\n@@ -30,7 +30,7 @@ func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {\n \tswitch r.Method {\n \tcase http.MethodGet:\n \t\tuser := s.auth.GetUser(r)\n-\t\tf, err := fullyResolvedRepo(r)\n+\t\tf, err := s.fullyResolvedRepo(r)\n \t\tif err != nil {\n \t\t\tlog.Println(\"failed to get repo and knot\", err)\n \t\t\treturn\n@@ -74,7 +74,7 @@ func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -251,7 +251,7 @@ func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.Resubmi\n \n func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -300,7 +300,7 @@ func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {\n func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -408,7 +408,7 @@ func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {\n \t\tstate = db.PullMerged\n \t}\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -462,7 +462,7 @@ func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -569,7 +569,7 @@ func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -904,7 +904,7 @@ func (s *State) createPullRequest(\n }\n \n func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {\n-\t_, err := fullyResolvedRepo(r)\n+\t_, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -930,7 +930,7 @@ func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -943,7 +943,7 @@ func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -983,7 +983,7 @@ func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request)\n \n func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1004,7 +1004,7 @@ func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {\n func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1082,7 +1082,7 @@ func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Requ\n \n func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1126,7 +1126,7 @@ func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {\n \t\treturn\n \t}\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1209,7 +1209,7 @@ func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {\n \t\treturn\n \t}\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1322,7 +1322,7 @@ func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {\n \t\treturn\n \t}\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1470,7 +1470,7 @@ func validateResubmittedPatch(pull *db.Pull, patch string) error {\n }\n \n func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to resolve repo:\", err)\n \t\ts.pages.Notice(w, \"pull-merge-error\", \"Failed to merge pull request. Try again later.\")\n@@ -1535,7 +1535,7 @@ func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {\n func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"malformed middleware\")\n \t\treturn\n@@ -1589,7 +1589,7 @@ func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {\n func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to resolve repo\", err)\n \t\ts.pages.Notice(w, \"pull-reopen\", \"Failed to reopen pull.\")\ndiff --git a/appview/state/repo.go b/appview/state/repo.go\nindex 88edeed..dd0d14d 100644\n--- a/appview/state/repo.go\n+++ b/appview/state/repo.go\n@@ -37,7 +37,7 @@ import (\n \n func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {\n \tref := chi.URLParam(r, \"ref\")\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to fully resolve repo\", err)\n \t\treturn\n@@ -129,7 +129,7 @@ func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to fully resolve repo\", err)\n \t\treturn\n@@ -210,7 +210,7 @@ func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\tw.WriteHeader(http.StatusBadRequest)\n@@ -225,7 +225,7 @@ func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\tw.WriteHeader(http.StatusBadRequest)\n@@ -304,7 +304,7 @@ func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to fully resolve repo\", err)\n \t\treturn\n@@ -350,7 +350,7 @@ func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to fully resolve repo\", err)\n \t\treturn\n@@ -381,6 +381,13 @@ func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {\n \t\treturn\n \t}\n \n+\t// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,\n+\t// so we can safely redirect to the \"parent\" (which is the same file).\n+\tif len(result.Files) == 0 && result.Parent == treePath {\n+\t\thttp.Redirect(w, r, fmt.Sprintf(\"/%s/blob/%s/%s\", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)\n+\t\treturn\n+\t}\n+\n \tuser := s.auth.GetUser(r)\n \n \tvar breadcrumbs [][]string\n@@ -406,7 +413,7 @@ func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -447,7 +454,7 @@ func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -502,7 +509,7 @@ func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -562,7 +569,7 @@ func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -606,7 +613,7 @@ func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -697,7 +704,7 @@ func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {\n func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -801,7 +808,7 @@ func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -840,7 +847,7 @@ func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {\n }\n \n func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -891,27 +898,13 @@ func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {\n \t\t\t\t}\n \t\t\t}\n \n-\t\t\tresp, err = us.DefaultBranch(f.OwnerDid(), f.RepoName)\n+\t\t\tdefaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName)\n \t\t\tif err != nil {\n \t\t\t\tlog.Println(\"failed to reach knotserver\", err)\n \t\t\t} else {\n-\t\t\t\tdefer resp.Body.Close()\n-\n-\t\t\t\tbody, err := io.ReadAll(resp.Body)\n-\t\t\t\tif err != nil {\n-\t\t\t\t\tlog.Printf(\"Error reading response body: %v\", err)\n-\t\t\t\t} else {\n-\t\t\t\t\tvar result types.RepoDefaultBranchResponse\n-\t\t\t\t\terr = json.Unmarshal(body, &result)\n-\t\t\t\t\tif err != nil {\n-\t\t\t\t\t\tlog.Println(\"failed to parse response:\", err)\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\tdefaultBranch = result.Branch\n-\t\t\t\t\t}\n-\t\t\t\t}\n+\t\t\t\tdefaultBranch = defaultBranchResp.Branch\n \t\t\t}\n \t\t}\n-\n \t\ts.pages.RepoSettings(w, pages.RepoSettingsParams{\n \t\t\tLoggedInUser: user,\n \t\t\tRepoInfo: f.RepoInfo(s, user),\n@@ -930,6 +923,7 @@ type FullyResolvedRepo struct {\n \tRepoAt syntax.ATURI\n \tDescription string\n \tCreatedAt string\n+\tRef string\n }\n \n func (f *FullyResolvedRepo) OwnerDid() string {\n@@ -1082,6 +1076,7 @@ func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {\n \t\tName: f.RepoName,\n \t\tRepoAt: f.RepoAt,\n \t\tDescription: f.Description,\n+\t\tRef: f.Ref,\n \t\tIsStarred: isStarred,\n \t\tKnot: knot,\n \t\tRoles: RolesInRepo(s, u, f),\n@@ -1103,7 +1098,7 @@ func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {\n \n func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1157,7 +1152,7 @@ func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1229,7 +1224,7 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1277,7 +1272,7 @@ func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1356,7 +1351,7 @@ func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1415,7 +1410,7 @@ func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1540,7 +1535,7 @@ func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1645,7 +1640,7 @@ func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {\n \t}\n \n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1686,7 +1681,7 @@ func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {\n func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n \n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Println(\"failed to get repo and knot\", err)\n \t\treturn\n@@ -1768,7 +1763,7 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {\n \n func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {\n \tuser := s.auth.GetUser(r)\n-\tf, err := fullyResolvedRepo(r)\n+\tf, err := s.fullyResolvedRepo(r)\n \tif err != nil {\n \t\tlog.Printf(\"failed to resolve source repo: %v\", err)\n \t\treturn\ndiff --git a/appview/state/repo_util.go b/appview/state/repo_util.go\nindex 3ba3403..20f6478 100644\n--- a/appview/state/repo_util.go\n+++ b/appview/state/repo_util.go\n@@ -17,7 +17,7 @@ import (\n \t\"tangled.sh/tangled.sh/core/appview/pages\"\n )\n \n-func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {\n+func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {\n \trepoName := chi.URLParam(r, \"repo\")\n \tknot, ok := r.Context().Value(\"knot\").(string)\n \tif !ok {\n@@ -42,6 +42,22 @@ func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {\n \t\treturn nil, fmt.Errorf(\"malformed middleware\")\n \t}\n \n+\tref := chi.URLParam(r, \"ref\")\n+\n+\tif ref == \"\" {\n+\t\tus, err := NewUnsignedClient(knot, s.config.Dev)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\tdefaultBranch, err := us.DefaultBranch(id.DID.String(), repoName)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\tref = defaultBranch.Branch\n+\t}\n+\n \t// pass through values from the middleware\n \tdescription, ok := r.Context().Value(\"repoDescription\").(string)\n \taddedAt, ok := r.Context().Value(\"repoAddedAt\").(string)\n@@ -53,6 +69,7 @@ func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {\n \t\tRepoAt: parsedRepoAt,\n \t\tDescription: description,\n \t\tCreatedAt: addedAt,\n+\t\tRef: ref,\n \t}, nil\n }\n \ndiff --git a/appview/state/signer.go b/appview/state/signer.go\nindex 53469b3..68abb85 100644\n--- a/appview/state/signer.go\n+++ b/appview/state/signer.go\n@@ -380,7 +380,7 @@ func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Respo\n \treturn us.client.Do(req)\n }\n \n-func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Response, error) {\n+func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {\n \tconst (\n \t\tMethod = \"GET\"\n \t)\n@@ -392,7 +392,18 @@ func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Respon\n \t\treturn nil, err\n \t}\n \n-\treturn us.client.Do(req)\n+\tresp, err := us.client.Do(req)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tdefer resp.Body.Close()\n+\n+\tvar defaultBranch types.RepoDefaultBranchResponse\n+\tif err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\treturn &defaultBranch, nil\n }\n \n func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {\n-- \n2.43.0\n\n\nFrom 5e7fddf7e12e2dbc935748df0e6991dea970ce93 Mon Sep 17 00:00:00 2001\nFrom: Anirudh Oppiliappan <x@icyphox.sh>\nDate: Fri, 2 May 2025 12:47:18 +0300\nSubject: [PATCH 3/5] knotserver: git: serve raw binary blobs\n\n---\n knotserver/git/git.go | 32 +++++++++++++++++++++++++++++++-\n knotserver/handler.go | 1 +\n knotserver/routes.go | 37 ++++++++++++++++++++++++++++++++++++-\n 3 files changed, 68 insertions(+), 2 deletions(-)\n\ndiff --git a/knotserver/git/git.go b/knotserver/git/git.go\nindex 959f3d4..c749000 100644\n--- a/knotserver/git/git.go\n+++ b/knotserver/git/git.go\n@@ -37,7 +37,8 @@ func init() {\n }\n \n var (\n-\tErrBinaryFile = fmt.Errorf(\"binary file\")\n+\tErrBinaryFile = fmt.Errorf(\"binary file\")\n+\tErrNotBinaryFile = fmt.Errorf(\"not binary file\")\n )\n \n type GitRepo struct {\n@@ -193,6 +194,35 @@ func (g *GitRepo) FileContent(path string) (string, error) {\n \t}\n }\n \n+func (g *GitRepo) BinContent(path string) ([]byte, error) {\n+\tc, err := g.r.CommitObject(g.h)\n+\tif err != nil {\n+\t\treturn nil, fmt.Errorf(\"commit object: %w\", err)\n+\t}\n+\n+\ttree, err := c.Tree()\n+\tif err != nil {\n+\t\treturn nil, fmt.Errorf(\"file tree: %w\", err)\n+\t}\n+\n+\tfile, err := tree.File(path)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tisbin, _ := file.IsBinary()\n+\tif isbin {\n+\t\treader, err := file.Reader()\n+\t\tif err != nil {\n+\t\t\treturn nil, fmt.Errorf(\"opening file reader: %w\", err)\n+\t\t}\n+\t\tdefer reader.Close()\n+\n+\t\treturn io.ReadAll(reader)\n+\t}\n+\treturn nil, ErrNotBinaryFile\n+}\n+\n func (g *GitRepo) Tags() ([]*TagReference, error) {\n \titer, err := g.r.Tags()\n \tif err != nil {\ndiff --git a/knotserver/handler.go b/knotserver/handler.go\nindex 1097874..8ced461 100644\n--- a/knotserver/handler.go\n+++ b/knotserver/handler.go\n@@ -100,6 +100,7 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j\n \n \t\t\tr.Route(\"/blob/{ref}\", func(r chi.Router) {\n \t\t\t\tr.Get(\"/*\", h.Blob)\n+\t\t\t\tr.Get(\"/raw/*\", h.BlobRaw)\n \t\t\t})\n \n \t\t\tr.Get(\"/log/{ref}\", h.Log)\ndiff --git a/knotserver/routes.go b/knotserver/routes.go\nindex 10c0535..bf97eee 100644\n--- a/knotserver/routes.go\n+++ b/knotserver/routes.go\n@@ -194,12 +194,47 @@ func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {\n \treturn\n }\n \n+func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {\n+\ttreePath := chi.URLParam(r, \"*\")\n+\tref := chi.URLParam(r, \"ref\")\n+\tref, _ = url.PathUnescape(ref)\n+\n+\tl := h.l.With(\"handler\", \"BlobRaw\", \"ref\", ref, \"treePath\", treePath)\n+\n+\tpath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))\n+\tgr, err := git.Open(path, ref)\n+\tif err != nil {\n+\t\tnotFound(w)\n+\t\treturn\n+\t}\n+\n+\tcontents, err := gr.BinContent(treePath)\n+\tif err != nil {\n+\t\twriteError(w, err.Error(), http.StatusBadRequest)\n+\t\tl.Error(\"file content\", \"error\", err.Error())\n+\t\treturn\n+\t}\n+\n+\tmimeType := http.DetectContentType(contents)\n+\n+\tif !strings.HasPrefix(mimeType, \"image/\") && !strings.HasPrefix(mimeType, \"video/\") {\n+\t\tl.Error(\"attempted to serve non-image/video file\", \"mimetype\", mimeType)\n+\t\twriteError(w, \"only image and video files can be accessed directly\", http.StatusForbidden)\n+\t\treturn\n+\t}\n+\n+\tw.Header().Set(\"Cache-Control\", \"public, max-age=86400\") // cache for 24 hours\n+\tw.Header().Set(\"ETag\", fmt.Sprintf(\"%x\", sha256.Sum256(contents)))\n+\tw.Header().Set(\"Content-Type\", mimeType)\n+\tw.Write(contents)\n+}\n+\n func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {\n \ttreePath := chi.URLParam(r, \"*\")\n \tref := chi.URLParam(r, \"ref\")\n \tref, _ = url.PathUnescape(ref)\n \n-\tl := h.l.With(\"handler\", \"FileContent\", \"ref\", ref, \"treePath\", treePath)\n+\tl := h.l.With(\"handler\", \"Blob\", \"ref\", ref, \"treePath\", treePath)\n \n \tpath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))\n \tgr, err := git.Open(path, ref)\n-- \n2.43.0\n\n\nFrom 0fa363a959721d6af9aa809d82bbaf3b2c9b4d47 Mon Sep 17 00:00:00 2001\nFrom: Anirudh Oppiliappan <x@icyphox.sh>\nDate: Fri, 2 May 2025 13:17:22 +0300\nSubject: [PATCH 4/5] appview: pages/repoinfo: split into separate package for\n reuse\n\n---\n appview/pages/repoinfo/repoinfo.go | 117 +++++++++++++++++++++++++++++\n 1 file changed, 117 insertions(+)\n create mode 100644 appview/pages/repoinfo/repoinfo.go\n\ndiff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go\nnew file mode 100644\nindex 0000000..c9f54ec\n--- /dev/null\n+++ b/appview/pages/repoinfo/repoinfo.go\n@@ -0,0 +1,117 @@\n+package repoinfo\n+\n+import (\n+\t\"fmt\"\n+\t\"path\"\n+\t\"slices\"\n+\t\"strings\"\n+\n+\t\"github.com/bluesky-social/indigo/atproto/syntax\"\n+\t\"tangled.sh/tangled.sh/core/appview/db\"\n+\t\"tangled.sh/tangled.sh/core/appview/state/userutil\"\n+)\n+\n+func (r RepoInfo) OwnerWithAt() string {\n+\tif r.OwnerHandle != \"\" {\n+\t\treturn fmt.Sprintf(\"@%s\", r.OwnerHandle)\n+\t} else {\n+\t\treturn r.OwnerDid\n+\t}\n+}\n+\n+func (r RepoInfo) FullName() string {\n+\treturn path.Join(r.OwnerWithAt(), r.Name)\n+}\n+\n+func (r RepoInfo) OwnerWithoutAt() string {\n+\tif strings.HasPrefix(r.OwnerWithAt(), \"@\") {\n+\t\treturn strings.TrimPrefix(r.OwnerWithAt(), \"@\")\n+\t} else {\n+\t\treturn userutil.FlattenDid(r.OwnerDid)\n+\t}\n+}\n+\n+func (r RepoInfo) FullNameWithoutAt() string {\n+\treturn path.Join(r.OwnerWithoutAt(), r.Name)\n+}\n+\n+func (r RepoInfo) GetTabs() [][]string {\n+\ttabs := [][]string{\n+\t\t{\"overview\", \"/\", \"square-chart-gantt\"},\n+\t\t{\"issues\", \"/issues\", \"circle-dot\"},\n+\t\t{\"pulls\", \"/pulls\", \"git-pull-request\"},\n+\t}\n+\n+\tif r.Roles.SettingsAllowed() {\n+\t\ttabs = append(tabs, []string{\"settings\", \"/settings\", \"cog\"})\n+\t}\n+\n+\treturn tabs\n+}\n+\n+type RepoInfo struct {\n+\tName string\n+\tOwnerDid string\n+\tOwnerHandle string\n+\tDescription string\n+\tKnot string\n+\tRepoAt syntax.ATURI\n+\tIsStarred bool\n+\tStats db.RepoStats\n+\tRoles RolesInRepo\n+\tSource *db.Repo\n+\tSourceHandle string\n+\tRef string\n+\tDisableFork bool\n+}\n+\n+// each tab on a repo could have some metadata:\n+//\n+// issues -> number of open issues etc.\n+// settings -> a warning icon to setup branch protection? idk\n+//\n+// we gather these bits of info here, because go templates\n+// are difficult to program in\n+func (r RepoInfo) TabMetadata() map[string]any {\n+\tmeta := make(map[string]any)\n+\n+\tif r.Stats.PullCount.Open > 0 {\n+\t\tmeta[\"pulls\"] = r.Stats.PullCount.Open\n+\t}\n+\n+\tif r.Stats.IssueCount.Open > 0 {\n+\t\tmeta[\"issues\"] = r.Stats.IssueCount.Open\n+\t}\n+\n+\t// more stuff?\n+\n+\treturn meta\n+}\n+\n+type RolesInRepo struct {\n+\tRoles []string\n+}\n+\n+func (r RolesInRepo) SettingsAllowed() bool {\n+\treturn slices.Contains(r.Roles, \"repo:settings\")\n+}\n+\n+func (r RolesInRepo) CollaboratorInviteAllowed() bool {\n+\treturn slices.Contains(r.Roles, \"repo:invite\")\n+}\n+\n+func (r RolesInRepo) RepoDeleteAllowed() bool {\n+\treturn slices.Contains(r.Roles, \"repo:delete\")\n+}\n+\n+func (r RolesInRepo) IsOwner() bool {\n+\treturn slices.Contains(r.Roles, \"repo:owner\")\n+}\n+\n+func (r RolesInRepo) IsCollaborator() bool {\n+\treturn slices.Contains(r.Roles, \"repo:collaborator\")\n+}\n+\n+func (r RolesInRepo) IsPushAllowed() bool {\n+\treturn slices.Contains(r.Roles, \"repo:push\")\n+}\n-- \n2.43.0\n\n\nFrom 9f6dd7043252c1e1a22da7e522bd2105b1cb0056 Mon Sep 17 00:00:00 2001\nFrom: Anirudh Oppiliappan <x@icyphox.sh>\nDate: Fri, 2 May 2025 13:17:22 +0300\nSubject: [PATCH 5/5] appview: pages/markup: serve relative images directly\n from the knot\n\n---\n .gitignore | 2 +\n appview/pages/markup/markdown.go | 57 ++++++--\n appview/pages/pages.go | 187 ++++++-------------------\n appview/pages/templates/repo/blob.html | 2 +-\n appview/state/repo.go | 5 +-\n appview/state/repo_util.go | 8 +-\n appview/state/router.go | 2 +-\n knotserver/handler.go | 5 +-\n 8 files changed, 108 insertions(+), 160 deletions(-)\n\ndiff --git a/.gitignore b/.gitignore\nindex c9e2523..1df590c 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -7,3 +7,5 @@ appview/pages/static/*\n result\n !.gitkeep\n out/\n+./camo/node_modules/*\n+\ndiff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go\nindex 6b19bad..809d418 100644\n--- a/appview/pages/markup/markdown.go\n+++ b/appview/pages/markup/markdown.go\n@@ -3,6 +3,7 @@ package markup\n \n import (\n \t\"bytes\"\n+\t\"net/url\"\n \t\"path\"\n \n \t\"github.com/yuin/goldmark\"\n@@ -11,6 +12,7 @@ import (\n \t\"github.com/yuin/goldmark/parser\"\n \t\"github.com/yuin/goldmark/text\"\n \t\"github.com/yuin/goldmark/util\"\n+\t\"tangled.sh/tangled.sh/core/appview/pages/repoinfo\"\n )\n \n // RendererType defines the type of renderer to use based on context\n@@ -24,8 +26,8 @@ const (\n // RenderContext holds the contextual data for rendering markdown.\n // It can be initialized empty, and that'll skip any transformations.\n type RenderContext struct {\n-\tRef string\n-\tFullRepoName string\n+\trepoinfo.RepoInfo\n+\tIsDev bool\n \tRendererType RendererType\n }\n \n@@ -66,8 +68,11 @@ func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader,\n \n \t\tswitch a.rctx.RendererType {\n \t\tcase RendererTypeRepoMarkdown:\n-\t\t\tif v, ok := n.(*ast.Link); ok {\n-\t\t\t\ta.rctx.relativeLinkTransformer(v)\n+\t\t\tswitch n.(type) {\n+\t\t\tcase *ast.Link:\n+\t\t\t\ta.rctx.relativeLinkTransformer(n.(*ast.Link))\n+\t\t\tcase *ast.Image:\n+\t\t\t\ta.rctx.imageFromKnotTransformer(n.(*ast.Image))\n \t\t\t}\n \t\t\t// more types here like RendererTypeIssue/Pull etc.\n \t\t}\n@@ -79,12 +84,48 @@ func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader,\n func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) {\n \tdst := string(link.Destination)\n \n-\tif len(dst) == 0 || dst[0] == '#' ||\n-\t\tbytes.Contains(link.Destination, []byte(\"://\")) ||\n-\t\tbytes.HasPrefix(link.Destination, []byte(\"mailto:\")) {\n+\tif isAbsoluteUrl(dst) {\n \t\treturn\n \t}\n \n-\tnewPath := path.Join(\"/\", rctx.FullRepoName, \"tree\", rctx.Ref, dst)\n+\tnewPath := path.Join(\"/\", rctx.RepoInfo.FullName(), \"tree\", rctx.RepoInfo.Ref, dst)\n \tlink.Destination = []byte(newPath)\n }\n+\n+func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) {\n+\tdst := string(img.Destination)\n+\n+\tif isAbsoluteUrl(dst) {\n+\t\treturn\n+\t}\n+\n+\t// strip leading './'\n+\tif len(dst) >= 2 && dst[0:2] == \"./\" {\n+\t\tdst = dst[2:]\n+\t}\n+\n+\tscheme := \"https\"\n+\tif rctx.IsDev {\n+\t\tscheme = \"http\"\n+\t}\n+\tparsedURL := &url.URL{\n+\t\tScheme: scheme,\n+\t\tHost: rctx.Knot,\n+\t\tPath: path.Join(\"/\",\n+\t\t\trctx.RepoInfo.OwnerDid,\n+\t\t\trctx.RepoInfo.Name,\n+\t\t\t\"raw\",\n+\t\t\turl.PathEscape(rctx.RepoInfo.Ref),\n+\t\t\tdst),\n+\t}\n+\tnewPath := parsedURL.String()\n+\timg.Destination = []byte(newPath)\n+}\n+\n+func isAbsoluteUrl(link string) bool {\n+\tparsed, err := url.Parse(link)\n+\tif err != nil {\n+\t\treturn false\n+\t}\n+\treturn parsed.IsAbs()\n+}\ndiff --git a/appview/pages/pages.go b/appview/pages/pages.go\nindex 1d0375d..23fb520 100644\n--- a/appview/pages/pages.go\n+++ b/appview/pages/pages.go\n@@ -12,16 +12,14 @@ import (\n \t\"log\"\n \t\"net/http\"\n \t\"os\"\n-\t\"path\"\n \t\"path/filepath\"\n-\t\"slices\"\n \t\"strings\"\n \n \t\"tangled.sh/tangled.sh/core/appview/auth\"\n \t\"tangled.sh/tangled.sh/core/appview/db\"\n \t\"tangled.sh/tangled.sh/core/appview/pages/markup\"\n+\t\"tangled.sh/tangled.sh/core/appview/pages/repoinfo\"\n \t\"tangled.sh/tangled.sh/core/appview/pagination\"\n-\t\"tangled.sh/tangled.sh/core/appview/state/userutil\"\n \t\"tangled.sh/tangled.sh/core/patchutil\"\n \t\"tangled.sh/tangled.sh/core/types\"\n \n@@ -42,13 +40,20 @@ type Pages struct {\n \tdev bool\n \tembedFS embed.FS\n \ttemplateDir string // Path to templates on disk for dev mode\n+\trctx *markup.RenderContext\n }\n \n func NewPages(dev bool) *Pages {\n+\t// initialized with safe defaults, can be overriden per use\n+\trctx := &markup.RenderContext{\n+\t\tIsDev: dev,\n+\t}\n+\n \tp := &Pages{\n \t\tt: make(map[string]*template.Template),\n \t\tdev: dev,\n \t\tembedFS: Files,\n+\t\trctx: rctx,\n \t\ttemplateDir: \"appview/pages\",\n \t}\n \n@@ -293,7 +298,7 @@ func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {\n type ForkRepoParams struct {\n \tLoggedInUser *auth.User\n \tKnots []string\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n }\n \n func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {\n@@ -343,7 +348,7 @@ func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParam\n }\n \n type RepoDescriptionParams struct {\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n }\n \n func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error {\n@@ -354,114 +359,9 @@ func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParam\n \treturn p.executePlain(\"repo/fragments/repoDescription\", w, params)\n }\n \n-type RepoInfo struct {\n-\tName string\n-\tOwnerDid string\n-\tOwnerHandle string\n-\tDescription string\n-\tKnot string\n-\tRepoAt syntax.ATURI\n-\tIsStarred bool\n-\tStats db.RepoStats\n-\tRoles RolesInRepo\n-\tSource *db.Repo\n-\tSourceHandle string\n-\tRef string\n-\tDisableFork bool\n-}\n-\n-type RolesInRepo struct {\n-\tRoles []string\n-}\n-\n-func (r RolesInRepo) SettingsAllowed() bool {\n-\treturn slices.Contains(r.Roles, \"repo:settings\")\n-}\n-\n-func (r RolesInRepo) CollaboratorInviteAllowed() bool {\n-\treturn slices.Contains(r.Roles, \"repo:invite\")\n-}\n-\n-func (r RolesInRepo) RepoDeleteAllowed() bool {\n-\treturn slices.Contains(r.Roles, \"repo:delete\")\n-}\n-\n-func (r RolesInRepo) IsOwner() bool {\n-\treturn slices.Contains(r.Roles, \"repo:owner\")\n-}\n-\n-func (r RolesInRepo) IsCollaborator() bool {\n-\treturn slices.Contains(r.Roles, \"repo:collaborator\")\n-}\n-\n-func (r RolesInRepo) IsPushAllowed() bool {\n-\treturn slices.Contains(r.Roles, \"repo:push\")\n-}\n-\n-func (r RepoInfo) OwnerWithAt() string {\n-\tif r.OwnerHandle != \"\" {\n-\t\treturn fmt.Sprintf(\"@%s\", r.OwnerHandle)\n-\t} else {\n-\t\treturn r.OwnerDid\n-\t}\n-}\n-\n-func (r RepoInfo) FullName() string {\n-\treturn path.Join(r.OwnerWithAt(), r.Name)\n-}\n-\n-func (r RepoInfo) OwnerWithoutAt() string {\n-\tif strings.HasPrefix(r.OwnerWithAt(), \"@\") {\n-\t\treturn strings.TrimPrefix(r.OwnerWithAt(), \"@\")\n-\t} else {\n-\t\treturn userutil.FlattenDid(r.OwnerDid)\n-\t}\n-}\n-\n-func (r RepoInfo) FullNameWithoutAt() string {\n-\treturn path.Join(r.OwnerWithoutAt(), r.Name)\n-}\n-\n-func (r RepoInfo) GetTabs() [][]string {\n-\ttabs := [][]string{\n-\t\t{\"overview\", \"/\", \"square-chart-gantt\"},\n-\t\t{\"issues\", \"/issues\", \"circle-dot\"},\n-\t\t{\"pulls\", \"/pulls\", \"git-pull-request\"},\n-\t}\n-\n-\tif r.Roles.SettingsAllowed() {\n-\t\ttabs = append(tabs, []string{\"settings\", \"/settings\", \"cog\"})\n-\t}\n-\n-\treturn tabs\n-}\n-\n-// each tab on a repo could have some metadata:\n-//\n-// issues -> number of open issues etc.\n-// settings -> a warning icon to setup branch protection? idk\n-//\n-// we gather these bits of info here, because go templates\n-// are difficult to program in\n-func (r RepoInfo) TabMetadata() map[string]any {\n-\tmeta := make(map[string]any)\n-\n-\tif r.Stats.PullCount.Open > 0 {\n-\t\tmeta[\"pulls\"] = r.Stats.PullCount.Open\n-\t}\n-\n-\tif r.Stats.IssueCount.Open > 0 {\n-\t\tmeta[\"issues\"] = r.Stats.IssueCount.Open\n-\t}\n-\n-\t// more stuff?\n-\n-\treturn meta\n-}\n-\n type RepoIndexParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tTagMap map[string][]string\n \tCommitsTrunc []*object.Commit\n@@ -479,9 +379,10 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {\n \t\treturn p.executeRepo(\"repo/empty\", w, params)\n \t}\n \n-\trctx := markup.RenderContext{\n-\t\tRef: params.RepoInfo.Ref,\n-\t\tFullRepoName: params.RepoInfo.FullName(),\n+\tp.rctx = &markup.RenderContext{\n+\t\tRepoInfo: params.RepoInfo,\n+\t\tIsDev: p.dev,\n+\t\tRendererType: markup.RendererTypeRepoMarkdown,\n \t}\n \n \tif params.ReadmeFileName != \"\" {\n@@ -489,7 +390,7 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {\n \t\text := filepath.Ext(params.ReadmeFileName)\n \t\tswitch ext {\n \t\tcase \".md\", \".markdown\", \".mdown\", \".mkdn\", \".mkd\":\n-\t\t\thtmlString = rctx.RenderMarkdown(params.Readme)\n+\t\t\thtmlString = p.rctx.RenderMarkdown(params.Readme)\n \t\t\tparams.Raw = false\n \t\t\tparams.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString))\n \t\tdefault:\n@@ -504,7 +405,7 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {\n \n type RepoLogParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tTagMap map[string][]string\n \ttypes.RepoLogResponse\n \tActive string\n@@ -518,7 +419,7 @@ func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {\n \n type RepoCommitParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tEmailToDidOrHandle map[string]string\n \n@@ -532,7 +433,7 @@ func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {\n \n type RepoTreeParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tBreadCrumbs [][]string\n \tBaseTreeLink string\n@@ -568,7 +469,7 @@ func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {\n \n type RepoBranchesParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \ttypes.RepoBranchesResponse\n }\n@@ -580,7 +481,7 @@ func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {\n \n type RepoTagsParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \ttypes.RepoTagsResponse\n }\n@@ -592,7 +493,7 @@ func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {\n \n type RepoBlobParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tBreadCrumbs [][]string\n \tShowRendered bool\n@@ -607,12 +508,12 @@ func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {\n \tif params.ShowRendered {\n \t\tswitch markup.GetFormat(params.Path) {\n \t\tcase markup.FormatMarkdown:\n-\t\t\trctx := markup.RenderContext{\n-\t\t\t\tRef: params.RepoInfo.Ref,\n-\t\t\t\tFullRepoName: params.RepoInfo.FullName(),\n+\t\t\tp.rctx = &markup.RenderContext{\n+\t\t\t\tRepoInfo: params.RepoInfo,\n+\t\t\t\tIsDev: p.dev,\n \t\t\t\tRendererType: markup.RendererTypeRepoMarkdown,\n \t\t\t}\n-\t\t\tparams.RenderedContents = template.HTML(rctx.RenderMarkdown(params.Contents))\n+\t\t\tparams.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents))\n \t\t}\n \t}\n \n@@ -657,7 +558,7 @@ type Collaborator struct {\n \n type RepoSettingsParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tCollaborators []Collaborator\n \tActive string\n \tBranches []string\n@@ -673,7 +574,7 @@ func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error {\n \n type RepoIssuesParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tIssues []db.Issue\n \tDidHandleMap map[string]string\n@@ -688,7 +589,7 @@ func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {\n \n type RepoSingleIssueParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tIssue db.Issue\n \tComments []db.Comment\n@@ -710,7 +611,7 @@ func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error\n \n type RepoNewIssueParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n }\n \n@@ -721,7 +622,7 @@ func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {\n \n type EditIssueCommentParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tIssue *db.Issue\n \tComment *db.Comment\n }\n@@ -733,7 +634,7 @@ func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentPar\n type SingleIssueCommentParams struct {\n \tLoggedInUser *auth.User\n \tDidHandleMap map[string]string\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tIssue *db.Issue\n \tComment *db.Comment\n }\n@@ -744,7 +645,7 @@ func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommen\n \n type RepoNewPullParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tBranches []types.Branch\n \tActive string\n }\n@@ -756,7 +657,7 @@ func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {\n \n type RepoPullsParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tPulls []*db.Pull\n \tActive string\n \tDidHandleMap map[string]string\n@@ -788,7 +689,7 @@ func (r ResubmitResult) Unknown() bool {\n \n type RepoSinglePullParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tActive string\n \tDidHandleMap map[string]string\n \tPull *db.Pull\n@@ -804,7 +705,7 @@ func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {\n type RepoPullPatchParams struct {\n \tLoggedInUser *auth.User\n \tDidHandleMap map[string]string\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tPull *db.Pull\n \tDiff *types.NiceDiff\n \tRound int\n@@ -819,7 +720,7 @@ func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error\n type RepoPullInterdiffParams struct {\n \tLoggedInUser *auth.User\n \tDidHandleMap map[string]string\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tPull *db.Pull\n \tRound int\n \tInterdiff *patchutil.InterdiffResult\n@@ -831,7 +732,7 @@ func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParam\n }\n \n type PullPatchUploadParams struct {\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n }\n \n func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error {\n@@ -839,7 +740,7 @@ func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParam\n }\n \n type PullCompareBranchesParams struct {\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tBranches []types.Branch\n }\n \n@@ -848,7 +749,7 @@ func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranc\n }\n \n type PullCompareForkParams struct {\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tForks []db.Repo\n }\n \n@@ -857,7 +758,7 @@ func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParam\n }\n \n type PullCompareForkBranchesParams struct {\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tSourceBranches []types.Branch\n \tTargetBranches []types.Branch\n }\n@@ -868,7 +769,7 @@ func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareF\n \n type PullResubmitParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tPull *db.Pull\n \tSubmissionId int\n }\n@@ -879,7 +780,7 @@ func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) err\n \n type PullActionsParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tPull *db.Pull\n \tRoundNumber int\n \tMergeCheck types.MergeCheckResponse\n@@ -892,7 +793,7 @@ func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error\n \n type PullNewCommentParams struct {\n \tLoggedInUser *auth.User\n-\tRepoInfo RepoInfo\n+\tRepoInfo repoinfo.RepoInfo\n \tPull *db.Pull\n \tRoundNumber int\n }\ndiff --git a/appview/pages/templates/repo/blob.html b/appview/pages/templates/repo/blob.html\nindex d4c7643..87e20f8 100644\n--- a/appview/pages/templates/repo/blob.html\n+++ b/appview/pages/templates/repo/blob.html\n@@ -42,7 +42,7 @@\n <span class=\"select-none px-1 md:px-2 [&:before]:content-['·']\"></span>\n <span>{{ byteFmt .SizeHint }}</span>\n <span class=\"select-none px-1 md:px-2 [&:before]:content-['·']\"></span>\n- <a href=\"/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/raw/{{ .Path }}\">view raw</a>\n+ <a href=\"/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}\">view raw</a>\n {{ if .RenderToggle }}\n <span class=\"select-none px-1 md:px-2 [&:before]:content-['·']\"></span>\n <a \ndiff --git a/appview/state/repo.go b/appview/state/repo.go\nindex dd0d14d..f1209ab 100644\n--- a/appview/state/repo.go\n+++ b/appview/state/repo.go\n@@ -28,6 +28,7 @@ import (\n \t\"tangled.sh/tangled.sh/core/appview/db\"\n \t\"tangled.sh/tangled.sh/core/appview/pages\"\n \t\"tangled.sh/tangled.sh/core/appview/pages/markup\"\n+\t\"tangled.sh/tangled.sh/core/appview/pages/repoinfo\"\n \t\"tangled.sh/tangled.sh/core/appview/pagination\"\n \t\"tangled.sh/tangled.sh/core/types\"\n \n@@ -996,7 +997,7 @@ func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]page\n \treturn collaborators, nil\n }\n \n-func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {\n+func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {\n \tisStarred := false\n \tif u != nil {\n \t\tisStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))\n@@ -1070,7 +1071,7 @@ func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {\n \t\tknot = \"tangled.sh\"\n \t}\n \n-\trepoInfo := pages.RepoInfo{\n+\trepoInfo := repoinfo.RepoInfo{\n \t\tOwnerDid: f.OwnerDid(),\n \t\tOwnerHandle: f.OwnerHandle(),\n \t\tName: f.RepoName,\ndiff --git a/appview/state/repo_util.go b/appview/state/repo_util.go\nindex 20f6478..9ca3b2d 100644\n--- a/appview/state/repo_util.go\n+++ b/appview/state/repo_util.go\n@@ -14,7 +14,7 @@ import (\n \t\"github.com/go-git/go-git/v5/plumbing/object\"\n \t\"tangled.sh/tangled.sh/core/appview/auth\"\n \t\"tangled.sh/tangled.sh/core/appview/db\"\n-\t\"tangled.sh/tangled.sh/core/appview/pages\"\n+\t\"tangled.sh/tangled.sh/core/appview/pages/repoinfo\"\n )\n \n func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {\n@@ -73,12 +73,12 @@ func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {\n \t}, nil\n }\n \n-func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {\n+func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {\n \tif u != nil {\n \t\tr := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())\n-\t\treturn pages.RolesInRepo{r}\n+\t\treturn repoinfo.RolesInRepo{r}\n \t} else {\n-\t\treturn pages.RolesInRepo{}\n+\t\treturn repoinfo.RolesInRepo{}\n \t}\n }\n \ndiff --git a/appview/state/router.go b/appview/state/router.go\nindex e4cb7d1..8bb0ab3 100644\n--- a/appview/state/router.go\n+++ b/appview/state/router.go\n@@ -65,7 +65,7 @@ func (s *State) UserRouter() http.Handler {\n \t\t\tr.Get(\"/branches\", s.RepoBranches)\n \t\t\tr.Get(\"/tags\", s.RepoTags)\n \t\t\tr.Get(\"/blob/{ref}/*\", s.RepoBlob)\n-\t\t\tr.Get(\"/blob/{ref}/raw/*\", s.RepoBlobRaw)\n+\t\t\tr.Get(\"/raw/{ref}/*\", s.RepoBlobRaw)\n \n \t\t\tr.Route(\"/issues\", func(r chi.Router) {\n \t\t\t\tr.With(middleware.Paginate).Get(\"/\", s.RepoIssues)\ndiff --git a/knotserver/handler.go b/knotserver/handler.go\nindex 8ced461..c33a4ff 100644\n--- a/knotserver/handler.go\n+++ b/knotserver/handler.go\n@@ -100,7 +100,10 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j\n \n \t\t\tr.Route(\"/blob/{ref}\", func(r chi.Router) {\n \t\t\t\tr.Get(\"/*\", h.Blob)\n-\t\t\t\tr.Get(\"/raw/*\", h.BlobRaw)\n+\t\t\t})\n+\n+\t\t\tr.Route(\"/raw/{ref}\", func(r chi.Router) {\n+\t\t\t\tr.Get(\"/*\", h.BlobRaw)\n \t\t\t})\n \n \t\t\tr.Get(\"/log/{ref}\", h.Log)\n-- \n2.43.0\n\n", "title": "appview: pages/markup: render markdown with transformations", "pullId": 73, "source": { "branch": "push-kstnoynmspqp" }, "createdAt": "", "targetRepo": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", "targetBranch": "master" } }