~gitpocket/gitpocket

8

feat(pages): publish the pages branch of private repositories

commit
e0cad84176f3a107a44ea141a76ab87d56afcacf browse tree
parent
bae926725bd43e86c83216adf292c2eed3c8c6c6
author
~james <354576047+[email protected]>
signature
Signed by: ~james SHA256:KlzJ0P0gvJcnOYpaxD5yCUw52ImHttTKRjVAEnnYnKg
date
2026-11-14 10:10 UTC

4 files changed, +26 −7

README.md modified

@@ -24,7 +24,7 @@ The web interface is then on `127.0.0.1:3200` and wants a reverse proxy with TLS
2424 - Imports from GitHub, GitLab, Gitea or Forgejo over https, with the option to credit commits made under your old identity to your account
2525 - Pull mirrors that fetch a source on a schedule and stay read only and push mirrors that copy every branch and tag to another host after each change
2626 - Pull requests opened by pushing to `refs/for/<branch>` with no fork needed
27-- Static sites served from `<user>.<host>` or a domain of your own, built from a `pages` repository or the `pages` branch of any public repository
27+- Static sites served from `<user>.<host>` or a domain of your own, built from a `pages` repository or the `pages` branch of any repository, private ones included
2828 - Reports to admins and per account blocking, with hourly limits on new issues, pull requests and repositories
2929 - SSH and PGP commit verification, TOTP, access tokens and Atom feeds for commits, issues and pull requests
3030 - Site search over repositories and people with language facets
@@ -58,7 +58,7 @@ Pushing again with the same topic updates that pull request and records a new re
5858
5959 ### Pages
6060
61-A public repository named `pages` is served at `https://you.git.example.org/` from its `pages` branch when one exists and from the default branch otherwise, and any other public repository with a `pages` branch appears beneath it at `/name/`. Paths without an extension also try `.html` and directories serve their `index.html`, while a missing file serves the site's own `404.html` with a 404 status when the site provides one. Symlinks and submodules are never followed and files over 32 MB are refused, accounts younger than a day have no site at all and admins can turn any site off from the admin page.
61+A repository named `pages` is served at `https://you.git.example.org/` from its `pages` branch when one exists and from the default branch otherwise, and any other repository with a `pages` branch appears beneath it at `/name/`. Private repositories publish only their `pages` branch so the site is public while the source on every other branch stays private, which matches how GitHub treats Pages built from a private repository. Paths without an extension also try `.html` and directories serve their `index.html`, while a missing file serves the site's own `404.html` with a 404 status when the site provides one. Symlinks and submodules are never followed and files over 32 MB are refused, accounts younger than a day have no site at all and admins can turn any site off from the admin page.
6262
6363 A site can also answer on a domain of your own once you claim it under settings, publish the TXT record `_gitpocket-pages.<domain>` with the token shown there and point the domain at the server through a CNAME to your subdomain or matching A and AAAA records. Verification checks both records and a daily recheck drops any domain whose TXT record has disappeared, while `deploy/domains.sh` on the host issues the certificate and writes the nginx server block for every verified domain.
6464

forge/feature_test.go modified

@@ -132,9 +132,16 @@ func TestPages(t *testing.T) {
132132 t.Errorf("project root redirect: %d", w.Code)
133133 }
134134 _, secret := newRepo(t, s, alice, "secret", Private)
135+ addEntry(t, secret, secret.Head(), "100644", "source.txt", "private source")
136+ if w := pagesGet(h, host, "/secret/source.txt"); strings.Contains(w.Body.String(), "private source") {
137+ t.Error("private repository without a pages branch served")
138+ }
135139 addEntry(t, secret, "pages", "100644", "index.html", "private site")
136- if w := pagesGet(h, host, "/secret/"); strings.Contains(w.Body.String(), "private site") {
137- t.Error("private repository served as pages")
140+ if w := pagesGet(h, host, "/secret/"); w.Body.String() != "private site" {
141+ t.Errorf("private pages branch: %d %q", w.Code, w.Body.String())
142+ }
143+ if w := pagesGet(h, host, "/secret/source.txt"); strings.Contains(w.Body.String(), "private source") {
144+ t.Error("private default branch leaked through pages")
138145 }
139146
140147 for _, host := range []string{"www.example.org", "a.b.example.org", "x_y.example.org"} {
@@ -149,6 +156,18 @@ func TestPages(t *testing.T) {
149156 t.Error("main site lost its headers")
150157 }
151158
159+ carol, _ := s.store.CreateUser("carol", "correct horse")
160+ age(t, s, carol)
161+ _, cg := newRepo(t, s, carol, "pages", Private)
162+ addEntry(t, cg, cg.Head(), "100644", "index.html", "draft")
163+ if w := pagesGet(h, "carol.example.org", "/"); w.Code != 404 {
164+ t.Errorf("private pages repository served its default branch: %d", w.Code)
165+ }
166+ addEntry(t, cg, "pages", "100644", "index.html", "published")
167+ if w := pagesGet(h, "carol.example.org", "/"); w.Body.String() != "published" {
168+ t.Errorf("private pages branch: %d %q", w.Code, w.Body.String())
169+ }
170+
152171 bob, _ := s.store.CreateUser("bob", "correct horse")
153172 _, bg := newRepo(t, s, bob, "pages", Public)
154173 addEntry(t, bg, bg.Head(), "100644", "index.html", "fresh")

forge/pages.go modified

@@ -93,12 +93,12 @@ func (s *Server) pagesEnabled(owner User) bool {
9393
9494 func (s *Server) pageSite(owner User, name string, branchOnly bool) (pageSite, bool) {
9595 repo, err := s.store.RepoByPath(owner.Name, name)
96- if err != nil || repo.Private {
96+ if err != nil {
9797 return pageSite{}, false
9898 }
9999 g := Git{Dir: s.repoDir(repo)}
100100 commit, err := g.Resolve("refs/heads/" + pagesBranch)
101- if err != nil && !branchOnly {
101+ if err != nil && !branchOnly && !repo.Private {
102102 commit, err = g.Resolve("HEAD")
103103 }
104104 if err != nil {

web/templates/settings.tmpl modified

@@ -48,7 +48,7 @@
4848 </form>
4949 <h3 class="rule" id="pages">Pages</h3>
5050 {{with .Data.Domain}}{{$d := .}}
51-<p class="muted small">A public repository called <code>pages</code> is published as your site, from its <code>pages</code> branch when it has one.{{with .Subdomain}} It lives at <a href="https://{{.}}/">{{.}}</a>.{{end}}{{if not .Enabled}} Sites go live once an account is a day old and while an admin has not turned them off.{{end}}</p>
51+<p class="muted small">A repository called <code>pages</code> is published as your site, from its <code>pages</code> branch when it has one. A private repository publishes only its <code>pages</code> branch, so that branch is public while the rest stays private.{{with .Subdomain}} It lives at <a href="https://{{.}}/">{{.}}</a>.{{end}}{{if not .Enabled}} Sites go live once an account is a day old and while an admin has not turned them off.{{end}}</p>
5252 {{with .Domain}}
5353 <p>Custom domain <code>{{.Name}}</code> {{if .Verified}}<span class="muted">(verified, serving your site)</span>{{else}}<span class="muted">(waiting for DNS)</span>{{end}}</p>
5454 {{if not .Verified}}<table class="keys">