···6969 - If the `PUT` method receives an `application/x-tar`, `application/x-tar+gzip`, `application/x-tar+zstd`, or `application/zip` body, it contains an archive to be extracted.
7070 - The `POST` method requires an `application/json` body containing a Forgejo/Gitea/Gogs/GitHub webhook event payload. Requests where the `ref` key contains anything other than `refs/heads/pages` are ignored, and only the `pages` branch is used. The `repository.clone_url` key contains a repository URL to be shallowly cloned.
7171 - If the received contents is empty, performs the same action as `DELETE`.
7272+* **With feature `patch`:** In response to a `PATCH` request, the server partially updates a site with new content. The URL of the request must be the root URL of the site that is being published.
7373+ - The request must have a `application/x-tar`, `application/x-tar+gzip`, or `application/x-tar+zstd` body, whose contents is *merged* with the existing site contents as follows:
7474+ - A character device entry with major 0 and minor 0 is treated as a "whiteout marker" (following [unionfs][whiteout]): it causes any existing file or directory with the same name to be deleted.
7575+ - A directory entry replaces any existing file or directory with the same name (if any), recursively removing the old contents.
7676+ - A file or symlink entry replaces any existing file or directory with the same name (if any).
7777+ - In any case, the parent of an entry must exist and be a directory.
7878+ - The request must have a `Race-Free: yes` or `Race-Free: no` header. Not every backend configuration makes it possible to perform atomic compare-and-swap operations; on backends without atomic CAS support, `Race-Free: yes` requests will fail, while `Race-Free: no` requests will provide a best-effort approximation.
7979+ - If a `PATCH` request loses a race against another content update request, it may return `409 Conflict`. This is true regardless of the `Race-Free:` header value. Whenever this happens, resubmit the request as-is.
8080+ - If the site has no contents after the update is applied, performs the same action as `DELETE`.
7281* In response to a `DELETE` request, the server unpublishes a site. The URL of the request must be the root URL of the site that is being unpublished. Site data remains stored for an indeterminate period of time, but becomes completely inaccessible.
7373-* If a `Dry-Run: yes` header is provided with a `PUT`, `DELETE`, or `POST` request, only the authorization checks are run; no destructive updates are made. Note that this functionality was added in _git-pages_ v0.2.0.
8282+* If a `Dry-Run: yes` header is provided with a `PUT`, `PATCH`, `DELETE`, or `POST` request, only the authorization checks are run; no destructive updates are made. Note that this functionality was added in _git-pages_ v0.2.0.
7483* All updates to site content are atomic (subject to consistency guarantees of the storage backend). That is, there is an instantaneous moment during an update before which the server will return the old content and after which it will return the new content.
7584* Files with a certain name, when placed in the root of a site, have special functions:
7685 - [Netlify `_redirects`][_redirects] file can be used to specify HTTP redirect and rewrite rules. The _git-pages_ implementation currently does not support placeholders, query parameters, or conditions, and may differ from Netlify in other minor ways. If you find that a supported `_redirects` file feature does not work the same as on Netlify, please file an issue. (Note that _git-pages_ does not perform URL normalization; `/foo` and `/foo/` are *not* the same, unlike with Netlify.)
···8190[_headers]: https://docs.netlify.com/manage/routing/headers/
8291[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
8392[go-git-sha256]: https://github.com/go-git/go-git/issues/706
9393+[whiteout]: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
849485958696Authorization
···88988999DNS is the primary authorization method, using either TXT records or wildcard matching. In certain cases, git forge authorization is used in addition to DNS.
901009191-The authorization flow for content updates (`PUT`, `DELETE`, `POST` requests) proceeds sequentially in the following order, with the first of multiple applicable rule taking precedence:
101101+The authorization flow for content updates (`PUT`, `PATCH`, `DELETE`, `POST` requests) proceeds sequentially in the following order, with the first of multiple applicable rule taking precedence:
92102931031. **Development Mode:** If the environment variable `PAGES_INSECURE` is set to a truthful value like `1`, the request is authorized.
9494-2. **DNS Challenge:** If the method is `PUT`, `DELETE`, `POST`, and a well-formed `Authorization:` header is provided containing a `<token>`, and a TXT record lookup at `_git-pages-challenge.<host>` returns a record whose concatenated value equals `SHA256("<host> <token>")`, the request is authorized.
104104+2. **DNS Challenge:** If the method is `PUT`, `PATCH`, `DELETE`, `POST`, and a well-formed `Authorization:` header is provided containing a `<token>`, and a TXT record lookup at `_git-pages-challenge.<host>` returns a record whose concatenated value equals `SHA256("<host> <token>")`, the request is authorized.
95105 - **`Pages` scheme:** Request includes an `Authorization: Pages <token>` header.
96106 - **`Basic` scheme:** Request includes an `Authorization: Basic <basic>` header, where `<basic>` is equal to `Base64("Pages:<token>")`. (Useful for non-Forgejo forges.)
971073. **DNS Allowlist:** If the method is `PUT` or `POST`, and the request URL is `scheme://<user>.<host>/`, and a TXT record lookup at `_git-pages-repository.<host>` returns a set of well-formed absolute URLs, and (for `PUT` requests) the body contains a repository URL, and the requested clone URLs is contained in this set of URLs, the request is authorized.
981084. **Wildcard Match (content):** If the method is `POST`, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and (for `PUT` requests) the body contains a repository URL, and the requested clone URL is a *matching* clone URL, the request is authorized.
99109 - **Index repository:** If the request URL is `scheme://<user>.<host>/`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `<user>` and `<project>`, where `<project>` is computed by templating each element of `[[wildcard]].index-repos` with `<user>`, and `[[wildcard]]` is the section where the match occurred.
100110 - **Project repository:** If the request URL is `scheme://<user>.<host>/<project>/`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `<user>` and `<project>`, and `[[wildcard]]` is the section where the match occurred.
101101-5. **Forge Authorization:** If the method is `PUT`, and the body contains an archive, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and `[[wildcard]].authorization` is non-empty, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at the *matching* clone URL (as defined above) as determined by an API call to the forge, the request is authorized. (This enables publishing a site for a private repository.)
111111+5. **Forge Authorization:** If the method is `PUT` or `PATCH`, and the body contains an archive, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and `[[wildcard]].authorization` is non-empty, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at the *matching* clone URL (as defined above) as determined by an API call to the forge, the request is authorized. (This enables publishing a site for a private repository.)
1021125. **Default Deny:** Otherwise, the request is not authorized.
103113104114The authorization flow for metadata retrieval (`GET` requests with site paths starting with `.git-pages/`) in the following order, with the first of multiple applicable rule taking precedence:
···1212)
13131414var ErrObjectNotFound = errors.New("not found")
1515+var ErrPreconditionFailed = errors.New("precondition failed")
1616+var ErrWriteConflict = errors.New("write conflict")
1517var ErrDomainFrozen = errors.New("domain administratively frozen")
16181719func splitBlobName(name string) []string {
···3335 // If true and the manifest is past the cache `MaxAge`, `GetManifest` blocks and returns
3436 // a fresh object instead of revalidating in background and returning a stale object.
3537 BypassCache bool
3838+}
3939+4040+type ModifyManifestOptions struct {
4141+ // If non-zero, the request will only succeed if the manifest hasn't been changed since
4242+ // the given time. Whether this is racy or not is can be determined via `HasAtomicCAS()`.
4343+ IfUnmodifiedSince time.Time
3644}
37453846type QueryAuditLogOptions struct {
···8189 // effects.
8290 StageManifest(ctx context.Context, manifest *Manifest) error
83919292+ // Whether a compare-and-swap operation on a manifest is truly race-free, or only best-effort
9393+ // atomic with a small but non-zero window where two requests may race where the one committing
9494+ // first will have its update lost. (Plain swap operations are always guaranteed to be atomic.)
9595+ HasAtomicCAS(ctx context.Context) bool
9696+8497 // Commit a manifest. This is an atomic operation; `GetManifest` calls will return either
8598 // the old version or the new version of the manifest, never anything else.
8686- CommitManifest(ctx context.Context, name string, manifest *Manifest) error
9999+ CommitManifest(ctx context.Context, name string, manifest *Manifest, opts ModifyManifestOptions) error
8710088101 // Delete a manifest.
8989- DeleteManifest(ctx context.Context, name string) error
102102+ DeleteManifest(ctx context.Context, name string, opts ModifyManifestOptions) error
9010391104 // List all manifests.
92105 ListManifests(ctx context.Context) (manifests []string, err error)
···114127func CreateBackend(config *StorageConfig) (backend Backend, err error) {
115128 switch config.Type {
116129 case "fs":
117117- if backend, err = NewFSBackend(&config.FS); err != nil {
130130+ if backend, err = NewFSBackend(context.Background(), &config.FS); err != nil {
118131 err = fmt.Errorf("fs backend: %w", err)
119132 }
120133 case "s3":