fix(web): point the finder of an abbreviated-id page at the full commit
setFinder uses the repository of the layout. The tree page passes the full id it got from GetTree through the target, and the blob page resolves an abbreviated id once, so data-paths names the full commit for both and pinRef is the only place that sets the finder.
- commit
- b06e3856ed4ccbc91580aca097b7758e5304a196
- author
- brice · · 2026-10-12 05:10 UTC
- parent
- c75c5f3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +43 -11
M internal/adapters/web/blob.go +3 -0
| @@ -185,6 +185,9 @@ func (s *Server) blob(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 185 | 185 | } | |
| 186 | 186 | ||
| 187 | 187 | l := s.repoLayout(ctx, d, "tree") | |
| 188 | + | // A blob stream does not name its commit: an abbreviated id is resolved | |
| 189 | + | // for the finder. | |
| 190 | + | s.resolveCommit(ctx, d.name(), &t) | |
| 188 | 191 | pinRef(&l, t) | |
| 189 | 192 | l.Crumbs = pathCrumbs(d.name(), "blob", t) | |
| 190 | 193 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | |
| @@ -185,6 +185,9 @@ func (s *Server) blob(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 185 | } | 185 | } | ||
| 186 | 186 | ||||
| 187 | l := s.repoLayout(ctx, d, "tree") | 187 | l := s.repoLayout(ctx, d, "tree") | ||
| 188 | + | // A blob stream does not name its commit: an abbreviated id is resolved | |||
| 189 | + | // for the finder. | |||
| 190 | + | s.resolveCommit(ctx, d.name(), &t) | |||
| 188 | pinRef(&l, t) | 191 | pinRef(&l, t) | ||
| 189 | l.Crumbs = pathCrumbs(d.name(), "blob", t) | 192 | l.Crumbs = pathCrumbs(d.name(), "blob", t) | ||
| 190 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | 193 | l.Title = visibleText(t.path) + " · " + d.name() + " · " + s.instance | ||
M internal/adapters/web/refs.go +36 -7
| @@ -3,9 +3,12 @@ | |||
| 3 | 3 | package web | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "context" | |
| 6 | 7 | "net/url" | |
| 7 | 8 | "strings" | |
| 8 | 9 | ||
| 10 | + | "connectrpc.com/connect" | |
| 11 | + | ||
| 9 | 12 | pb "bnema.dev/ermite-api/gen/ermite" | |
| 10 | 13 | ) | |
| 11 | 14 | ||
| @@ -77,6 +80,32 @@ type target struct { | |||
| 77 | 80 | path string | |
| 78 | 81 | // detached is true when the URL names a commit and not a ref. | |
| 79 | 82 | detached bool | |
| 83 | + | // commit is the full id of the commit when the page has learnt it from | |
| 84 | + | // the API and rev is an abbreviation; "" otherwise. | |
| 85 | + | commit string | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // commitID is the full id of the commit of the target when it is known, and | |
| 89 | + | // rev otherwise. | |
| 90 | + | func (t target) commitID() string { | |
| 91 | + | if t.commit != "" { | |
| 92 | + | return t.commit | |
| 93 | + | } | |
| 94 | + | return t.rev | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // resolveCommit sets the full id of a target named by an abbreviated commit id, | |
| 98 | + | // with one ResolveRevision. The page renders without it (it only lacks the file | |
| 99 | + | // finder), so a failure is not one. | |
| 100 | + | func (s *Server) resolveCommit(ctx context.Context, repo string, t *target) { | |
| 101 | + | if !t.detached || isFullID(t.rev) { | |
| 102 | + | return | |
| 103 | + | } | |
| 104 | + | res, err := s.cfg.Git.ResolveRevision(ctx, connect.NewRequest(&pb.ResolveRevisionRequest{Repo: repo, Revision: t.rev})) | |
| 105 | + | if err != nil { | |
| 106 | + | return | |
| 107 | + | } | |
| 108 | + | t.commit = res.Msg.GetCommitId() | |
| 80 | 109 | } | |
| 81 | 110 | ||
| 82 | 111 | // locate reads "<ref>[/<path>]" or "<commit>[/<path>]". The ref is the | |
| @@ -132,14 +161,14 @@ func (d *repoData) isBranchHead(t target) bool { | |||
| 132 | 161 | ||
| 133 | 162 | // pinRef points the ref switcher of the layout at what a page shows: a | |
| 134 | 163 | // branch or a tag, which is then the current one of the lists, or a commit, | |
| 135 | - | // which makes none current. | |
| 164 | + | // which makes none current. It points the file finder at the commit too. | |
| 136 | 165 | func pinRef(l *Layout, t target) { | |
| 137 | 166 | if t.detached { | |
| 138 | 167 | l.Ref, l.RefPath, l.Short, l.SHA = "detached", "", short(t.rev), "" | |
| 139 | 168 | if len(t.rev) == 40 { | |
| 140 | 169 | l.SHA = t.rev | |
| 141 | 170 | } | |
| 142 | - | l.setFinder(l.Repo, l.SHA, t.rev) | |
| 171 | + | l.setFinder(t.commitID(), t.rev) | |
| 143 | 172 | for i := range l.Heads { | |
| 144 | 173 | l.Heads[i].Current = false | |
| 145 | 174 | } | |
| @@ -149,7 +178,7 @@ func pinRef(l *Layout, t target) { | |||
| 149 | 178 | return | |
| 150 | 179 | } | |
| 151 | 180 | l.Ref, l.RefPath, l.SHA, l.Short = t.name, t.name, t.rev, short(t.rev) | |
| 152 | - | l.setFinder(l.Repo, t.rev, t.name) | |
| 181 | + | l.setFinder(t.rev, t.name) | |
| 153 | 182 | for i := range l.Heads { | |
| 154 | 183 | l.Heads[i].Current = l.Heads[i].Name == t.name | |
| 155 | 184 | } | |
| @@ -162,13 +191,13 @@ func pinRef(l *Layout, t target) { | |||
| 162 | 191 | // is a full commit id, and the blob pages of ref (a ref name or a commit id) | |
| 163 | 192 | // its results open. Without a full id there is nothing to search and the | |
| 164 | 193 | // page has no finder data. | |
| 165 | - | func (l *Layout) setFinder(repo, sha, ref string) { | |
| 194 | + | func (l *Layout) setFinder(sha, ref string) { | |
| 166 | 195 | l.PathsURL, l.BlobBase = "", "" | |
| 167 | - | if repo == "" || ref == "" || !isFullID(sha) { | |
| 196 | + | if l.Repo == "" || ref == "" || !isFullID(sha) { | |
| 168 | 197 | return | |
| 169 | 198 | } | |
| 170 | - | l.PathsURL = repoHref(repo, "paths", sha) | |
| 171 | - | l.BlobBase = repoHref(repo, "blob", ref) | |
| 199 | + | l.PathsURL = repoHref(l.Repo, "paths", sha) | |
| 200 | + | l.BlobBase = repoHref(l.Repo, "blob", ref) | |
| 172 | 201 | } | |
| 173 | 202 | ||
| 174 | 203 | func hasName(items []RefItem, name string) bool { | |
| @@ -3,9 +3,12 @@ | |||||
| 3 | package web | 3 | package web | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "context" | |||
| 6 | "net/url" | 7 | "net/url" | ||
| 7 | "strings" | 8 | "strings" | ||
| 8 | 9 | ||||
| 10 | + | "connectrpc.com/connect" | |||
| 11 | + | ||||
| 9 | pb "bnema.dev/ermite-api/gen/ermite" | 12 | pb "bnema.dev/ermite-api/gen/ermite" | ||
| 10 | ) | 13 | ) | ||
| 11 | 14 | ||||
| @@ -77,6 +80,32 @@ type target struct { | |||||
| 77 | path string | 80 | path string | ||
| 78 | // detached is true when the URL names a commit and not a ref. | 81 | // detached is true when the URL names a commit and not a ref. | ||
| 79 | detached bool | 82 | detached bool | ||
| 83 | + | // commit is the full id of the commit when the page has learnt it from | |||
| 84 | + | // the API and rev is an abbreviation; "" otherwise. | |||
| 85 | + | commit string | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // commitID is the full id of the commit of the target when it is known, and | |||
| 89 | + | // rev otherwise. | |||
| 90 | + | func (t target) commitID() string { | |||
| 91 | + | if t.commit != "" { | |||
| 92 | + | return t.commit | |||
| 93 | + | } | |||
| 94 | + | return t.rev | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // resolveCommit sets the full id of a target named by an abbreviated commit id, | |||
| 98 | + | // with one ResolveRevision. The page renders without it (it only lacks the file | |||
| 99 | + | // finder), so a failure is not one. | |||
| 100 | + | func (s *Server) resolveCommit(ctx context.Context, repo string, t *target) { | |||
| 101 | + | if !t.detached || isFullID(t.rev) { | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | res, err := s.cfg.Git.ResolveRevision(ctx, connect.NewRequest(&pb.ResolveRevisionRequest{Repo: repo, Revision: t.rev})) | |||
| 105 | + | if err != nil { | |||
| 106 | + | return | |||
| 107 | + | } | |||
| 108 | + | t.commit = res.Msg.GetCommitId() | |||
| 80 | } | 109 | } | ||
| 81 | 110 | ||||
| 82 | // locate reads "<ref>[/<path>]" or "<commit>[/<path>]". The ref is the | 111 | // locate reads "<ref>[/<path>]" or "<commit>[/<path>]". The ref is the | ||
| @@ -132,14 +161,14 @@ func (d *repoData) isBranchHead(t target) bool { | |||||
| 132 | 161 | ||||
| 133 | // pinRef points the ref switcher of the layout at what a page shows: a | 162 | // pinRef points the ref switcher of the layout at what a page shows: a | ||
| 134 | // branch or a tag, which is then the current one of the lists, or a commit, | 163 | // branch or a tag, which is then the current one of the lists, or a commit, | ||
| 135 | - | // which makes none current. | 164 | + | // which makes none current. It points the file finder at the commit too. |
| 136 | func pinRef(l *Layout, t target) { | 165 | func pinRef(l *Layout, t target) { | ||
| 137 | if t.detached { | 166 | if t.detached { | ||
| 138 | l.Ref, l.RefPath, l.Short, l.SHA = "detached", "", short(t.rev), "" | 167 | l.Ref, l.RefPath, l.Short, l.SHA = "detached", "", short(t.rev), "" | ||
| 139 | if len(t.rev) == 40 { | 168 | if len(t.rev) == 40 { | ||
| 140 | l.SHA = t.rev | 169 | l.SHA = t.rev | ||
| 141 | } | 170 | } | ||
| 142 | - | l.setFinder(l.Repo, l.SHA, t.rev) | 171 | + | l.setFinder(t.commitID(), t.rev) |
| 143 | for i := range l.Heads { | 172 | for i := range l.Heads { | ||
| 144 | l.Heads[i].Current = false | 173 | l.Heads[i].Current = false | ||
| 145 | } | 174 | } | ||
| @@ -149,7 +178,7 @@ func pinRef(l *Layout, t target) { | |||||
| 149 | return | 178 | return | ||
| 150 | } | 179 | } | ||
| 151 | l.Ref, l.RefPath, l.SHA, l.Short = t.name, t.name, t.rev, short(t.rev) | 180 | l.Ref, l.RefPath, l.SHA, l.Short = t.name, t.name, t.rev, short(t.rev) | ||
| 152 | - | l.setFinder(l.Repo, t.rev, t.name) | 181 | + | l.setFinder(t.rev, t.name) |
| 153 | for i := range l.Heads { | 182 | for i := range l.Heads { | ||
| 154 | l.Heads[i].Current = l.Heads[i].Name == t.name | 183 | l.Heads[i].Current = l.Heads[i].Name == t.name | ||
| 155 | } | 184 | } | ||
| @@ -162,13 +191,13 @@ func pinRef(l *Layout, t target) { | |||||
| 162 | // is a full commit id, and the blob pages of ref (a ref name or a commit id) | 191 | // is a full commit id, and the blob pages of ref (a ref name or a commit id) | ||
| 163 | // its results open. Without a full id there is nothing to search and the | 192 | // its results open. Without a full id there is nothing to search and the | ||
| 164 | // page has no finder data. | 193 | // page has no finder data. | ||
| 165 | - | func (l *Layout) setFinder(repo, sha, ref string) { | 194 | + | func (l *Layout) setFinder(sha, ref string) { |
| 166 | l.PathsURL, l.BlobBase = "", "" | 195 | l.PathsURL, l.BlobBase = "", "" | ||
| 167 | - | if repo == "" || ref == "" || !isFullID(sha) { | 196 | + | if l.Repo == "" || ref == "" || !isFullID(sha) { |
| 168 | return | 197 | return | ||
| 169 | } | 198 | } | ||
| 170 | - | l.PathsURL = repoHref(repo, "paths", sha) | 199 | + | l.PathsURL = repoHref(l.Repo, "paths", sha) |
| 171 | - | l.BlobBase = repoHref(repo, "blob", ref) | 200 | + | l.BlobBase = repoHref(l.Repo, "blob", ref) |
| 172 | } | 201 | } | ||
| 173 | 202 | ||||
| 174 | func hasName(items []RefItem, name string) bool { | 203 | func hasName(items []RefItem, name string) bool { | ||
M internal/adapters/web/repo.go +1 -1
| @@ -98,7 +98,7 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||
| 98 | 98 | def = d.head.GetShortName() | |
| 99 | 99 | l.Ref, l.RefPath = def, def | |
| 100 | 100 | l.SHA, l.Short = d.head.GetCommitId(), short(d.head.GetCommitId()) | |
| 101 | - | l.setFinder(d.name(), l.SHA, def) | |
| 101 | + | l.setFinder(l.SHA, def) | |
| 102 | 102 | } | |
| 103 | 103 | mk := func(key, label, href, id string) Section { | |
| 104 | 104 | return Section{Key: key, Label: label, Href: href, Current: section == id} | |
| @@ -98,7 +98,7 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||||
| 98 | def = d.head.GetShortName() | 98 | def = d.head.GetShortName() | ||
| 99 | l.Ref, l.RefPath = def, def | 99 | l.Ref, l.RefPath = def, def | ||
| 100 | l.SHA, l.Short = d.head.GetCommitId(), short(d.head.GetCommitId()) | 100 | l.SHA, l.Short = d.head.GetCommitId(), short(d.head.GetCommitId()) | ||
| 101 | - | l.setFinder(d.name(), l.SHA, def) | 101 | + | l.setFinder(l.SHA, def) |
| 102 | } | 102 | } | ||
| 103 | mk := func(key, label, href, id string) Section { | 103 | mk := func(key, label, href, id string) Section { | ||
| 104 | return Section{Key: key, Label: label, Href: href, Current: section == id} | 104 | return Section{Key: key, Label: label, Href: href, Current: section == id} | ||
M internal/adapters/web/tree.go +3 -3
| @@ -105,10 +105,10 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 105 | 105 | } | |
| 106 | 106 | ||
| 107 | 107 | l := s.repoLayout(ctx, d, "tree") | |
| 108 | - | pinRef(&l, t) | |
| 109 | 108 | // The finder searches the commit this page lists, whichever way the URL | |
| 110 | - | // spelled it (an abbreviated id included). | |
| 111 | - | l.setFinder(d.name(), res.Msg.GetCommitId(), t.name) | |
| 109 | + | // spelled it (an abbreviated id included): the listing knows it. | |
| 110 | + | t.commit = res.Msg.GetCommitId() | |
| 111 | + | pinRef(&l, t) | |
| 112 | 112 | l.Crumbs = pathCrumbs(d.name(), "tree", t) | |
| 113 | 113 | if t.path == "" { | |
| 114 | 114 | l.Title = "tree · " + d.name() + " · " + s.instance | |
| @@ -105,10 +105,10 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 105 | } | 105 | } | ||
| 106 | 106 | ||||
| 107 | l := s.repoLayout(ctx, d, "tree") | 107 | l := s.repoLayout(ctx, d, "tree") | ||
| 108 | - | pinRef(&l, t) | |||
| 109 | // The finder searches the commit this page lists, whichever way the URL | 108 | // The finder searches the commit this page lists, whichever way the URL | ||
| 110 | - | // spelled it (an abbreviated id included). | 109 | + | // spelled it (an abbreviated id included): the listing knows it. |
| 111 | - | l.setFinder(d.name(), res.Msg.GetCommitId(), t.name) | 110 | + | t.commit = res.Msg.GetCommitId() |
| 111 | + | pinRef(&l, t) | |||
| 112 | l.Crumbs = pathCrumbs(d.name(), "tree", t) | 112 | l.Crumbs = pathCrumbs(d.name(), "tree", t) | ||
| 113 | if t.path == "" { | 113 | if t.path == "" { | ||
| 114 | l.Title = "tree · " + d.name() + " · " + s.instance | 114 | l.Title = "tree · " + d.name() + " · " + s.instance | ||