feat(pages): publish the pages branch of private repositories
4 files changed, +26 −7
- README.md modified +2 −2
- forge/feature_test.go modified +21 −2
- forge/pages.go modified +2 −2
- web/templates/settings.tmpl modified +1 −1
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 | ||
| 24 | 24 | - Imports from GitHub, GitLab, Gitea or Forgejo over https, with the option to credit commits made under your old identity to your account |
| 25 | 25 | - 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 |
| 26 | 26 | - 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 | |
| 28 | 28 | - Reports to admins and per account blocking, with hourly limits on new issues, pull requests and repositories |
| 29 | 29 | - SSH and PGP commit verification, TOTP, access tokens and Atom feeds for commits, issues and pull requests |
| 30 | 30 | - 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 | ||
| 58 | 58 | |
| 59 | 59 | ### Pages |
| 60 | 60 | |
| 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. | |
| 62 | 62 | |
| 63 | 63 | 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. |
| 64 | 64 | |
forge/feature_test.go modified
| @@ -132,9 +132,16 @@ func TestPages(t *testing.T) { | ||
| 132 | 132 | t.Errorf("project root redirect: %d", w.Code) |
| 133 | 133 | } |
| 134 | 134 | _, 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 | + } | |
| 135 | 139 | 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") | |
| 138 | 145 | } |
| 139 | 146 | |
| 140 | 147 | for _, host := range []string{"www.example.org", "a.b.example.org", "x_y.example.org"} { |
| @@ -149,6 +156,18 @@ func TestPages(t *testing.T) { | ||
| 149 | 156 | t.Error("main site lost its headers") |
| 150 | 157 | } |
| 151 | 158 | |
| 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 | + | |
| 152 | 171 | bob, _ := s.store.CreateUser("bob", "correct horse") |
| 153 | 172 | _, bg := newRepo(t, s, bob, "pages", Public) |
| 154 | 173 | addEntry(t, bg, bg.Head(), "100644", "index.html", "fresh") |
forge/pages.go modified
| @@ -93,12 +93,12 @@ func (s *Server) pagesEnabled(owner User) bool { | ||
| 93 | 93 | |
| 94 | 94 | func (s *Server) pageSite(owner User, name string, branchOnly bool) (pageSite, bool) { |
| 95 | 95 | repo, err := s.store.RepoByPath(owner.Name, name) |
| 96 | - if err != nil || repo.Private { | |
| 96 | + if err != nil { | |
| 97 | 97 | return pageSite{}, false |
| 98 | 98 | } |
| 99 | 99 | g := Git{Dir: s.repoDir(repo)} |
| 100 | 100 | commit, err := g.Resolve("refs/heads/" + pagesBranch) |
| 101 | - if err != nil && !branchOnly { | |
| 101 | + if err != nil && !branchOnly && !repo.Private { | |
| 102 | 102 | commit, err = g.Resolve("HEAD") |
| 103 | 103 | } |
| 104 | 104 | if err != nil { |
web/templates/settings.tmpl modified
| @@ -48,7 +48,7 @@ | ||
| 48 | 48 | </form> |
| 49 | 49 | <h3 class="rule" id="pages">Pages</h3> |
| 50 | 50 | {{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> | |
| 52 | 52 | {{with .Domain}} |
| 53 | 53 | <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> |
| 54 | 54 | {{if not .Verified}}<table class="keys"> |