+15
-12
.forgejo/workflows/ci.yaml
+15
-12
.forgejo/workflows/ci.yaml
···
10
10
11
11
jobs:
12
12
check:
13
-
runs-on: codeberg-small-lazy
13
+
runs-on: debian-trixie
14
14
container:
15
-
image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4
15
+
image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce
16
16
steps:
17
17
- name: Check out source code
18
-
uses: https://code.forgejo.org/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
18
+
uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19
19
- name: Set up toolchain
20
20
uses: https://code.forgejo.org/actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
21
21
with:
···
28
28
- name: Build service
29
29
run: |
30
30
go build
31
+
- name: Run tests
32
+
run: |
33
+
go test ./...
31
34
- name: Run static analysis
32
35
run: |
33
-
go vet
34
-
staticcheck
36
+
go vet ./...
37
+
staticcheck ./...
35
38
36
39
release:
37
40
# IMPORTANT: This workflow step will not work without the Releases unit enabled!
38
41
if: ${{ forge.ref == 'refs/heads/main' || startsWith(forge.event.ref, 'refs/tags/v') }}
39
42
needs: [check]
40
-
runs-on: codeberg-medium-lazy
43
+
runs-on: debian-trixie
41
44
container:
42
-
image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4
45
+
image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce
43
46
steps:
44
47
- name: Check out source code
45
-
uses: https://code.forgejo.org/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
48
+
uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
46
49
- name: Set up toolchain
47
50
uses: https://code.forgejo.org/actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
48
51
with:
···
72
75
package:
73
76
if: ${{ forge.ref == 'refs/heads/main' || startsWith(forge.event.ref, 'refs/tags/v') }}
74
77
needs: [check]
75
-
runs-on: codeberg-medium-lazy
78
+
runs-on: debian-trixie
76
79
container:
77
-
image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4
80
+
image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce
78
81
steps:
79
82
- name: Install dependencies
80
83
run: |
81
84
apt-get -y update
82
-
apt-get -y install buildah ca-certificates
85
+
apt-get -y install ca-certificates buildah qemu-user-binfmt
83
86
- name: Check out source code
84
-
uses: https://code.forgejo.org/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
87
+
uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
85
88
- name: Authenticate with Docker
86
89
run: |
87
90
buildah login --authfile=/tmp/authfile-${FORGE}.json \
+8
-8
Dockerfile
+8
-8
Dockerfile
···
1
1
# Install CA certificates.
2
-
FROM docker.io/library/alpine:latest@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 AS ca-certificates-builder
2
+
FROM docker.io/library/alpine:3 AS ca-certificates-builder
3
3
RUN apk --no-cache add ca-certificates
4
4
5
5
# Build supervisor.
6
-
FROM docker.io/library/golang:1.25-alpine@sha256:26111811bc967321e7b6f852e914d14bede324cd1accb7f81811929a6a57fea9 AS supervisor-builder
6
+
FROM docker.io/library/golang:1.25-alpine@sha256:ac09a5f469f307e5da71e766b0bd59c9c49ea460a528cc3e6686513d64a6f1fb AS supervisor-builder
7
7
RUN apk --no-cache add git
8
8
WORKDIR /build
9
9
RUN git clone https://github.com/ochinchina/supervisord . && \
···
11
11
RUN GOBIN=/usr/bin go install -ldflags "-s -w"
12
12
13
13
# Build Caddy with S3 storage backend.
14
-
FROM docker.io/library/caddy:2.10.2-builder@sha256:fe404674d209455fdef351db5437758ee0e70a6b59abe770663c09cfa05dbddf AS caddy-builder
14
+
FROM docker.io/library/caddy:2.10.2-builder@sha256:6644af24bde2b4dbb07eb57637051abd2aa713e9787fa1eb544c3f31a0620898 AS caddy-builder
15
15
RUN xcaddy build ${CADDY_VERSION} \
16
16
--with=github.com/ss098/certmagic-s3@v0.0.0-20250922022452-8af482af5f39
17
17
18
18
# Build git-pages.
19
-
FROM docker.io/library/golang:1.25-alpine@sha256:26111811bc967321e7b6f852e914d14bede324cd1accb7f81811929a6a57fea9 AS git-pages-builder
19
+
FROM docker.io/library/golang:1.25-alpine@sha256:ac09a5f469f307e5da71e766b0bd59c9c49ea460a528cc3e6686513d64a6f1fb AS git-pages-builder
20
20
RUN apk --no-cache add git
21
21
WORKDIR /build
22
22
COPY go.mod go.sum ./
···
26
26
RUN go build -ldflags "-s -w" -o git-pages .
27
27
28
28
# Compose git-pages and Caddy.
29
-
FROM docker.io/library/busybox:1.37.0-musl@sha256:ef13e7482851632be3faf5bd1d28d4727c0810901d564b35416f309975a12a30
29
+
FROM docker.io/library/busybox:1.37.0-musl@sha256:03db190ed4c1ceb1c55d179a0940e2d71d42130636a780272629735893292223
30
30
COPY --from=ca-certificates-builder /etc/ssl/cert.pem /etc/ssl/cert.pem
31
31
COPY --from=supervisor-builder /usr/bin/supervisord /bin/supervisord
32
32
COPY --from=caddy-builder /usr/bin/caddy /bin/caddy
···
36
36
RUN mkdir /app/data
37
37
COPY conf/supervisord.conf /app/supervisord.conf
38
38
COPY conf/Caddyfile /app/Caddyfile
39
-
COPY conf/config.example.toml /app/config.toml
39
+
COPY conf/config.docker.toml /app/config.toml
40
40
41
41
# Caddy ports:
42
42
EXPOSE 80/tcp 443/tcp 443/udp
···
46
46
# While the default command is to run git-pages standalone, the intended configuration
47
47
# is to use it with Caddy and store both site data and credentials to an S3-compatible
48
48
# object store.
49
-
# * In a standalone configuration, the default, git-caddy listens on port 3000 (http).
50
-
# * In a combined configuration, supervisord launches both git-caddy and Caddy, and
49
+
# * In a standalone configuration, the default, git-pages listens on port 3000 (http).
50
+
# * In a combined configuration, supervisord launches both git-pages and Caddy, and
51
51
# Caddy listens on ports 80 (http) and 443 (https).
52
52
CMD ["git-pages"]
53
53
# CMD ["supervisord"]
+5
-4
README.md
+5
-4
README.md
···
137
137
* If `SENTRY_DSN` environment variable is set, panics are reported to Sentry.
138
138
* If `SENTRY_DSN` and `SENTRY_LOGS=1` environment variables are set, logs are uploaded to Sentry.
139
139
* If `SENTRY_DSN` and `SENTRY_TRACING=1` environment variables are set, traces are uploaded to Sentry.
140
-
* Optional syslog integration allows transmitting application logs to a syslog daemon. When present, the `SYSLOG_ADDR` environment variable enables the integration, and the variable's value is used to configure the absolute path to a Unix socket (usually located at `/dev/log` on Unix systems) or a network address of one of the following formats:
141
-
* for TLS over TCP: `tcp+tls://host:port`;
142
-
* for plain TCP: `tcp://host:post`;
143
-
* for UDP: `udp://host:port`.
140
+
* Optional syslog integration allows transmitting application logs to a syslog daemon. When present, the `SYSLOG_ADDR` environment variable enables the integration, and the value is used to configure the syslog destination. The value must follow the format `family/address` and is usually one of the following:
141
+
* a Unix datagram socket: `unixgram//dev/log`;
142
+
* TLS over TCP: `tcp+tls/host:port`;
143
+
* plain TCP: `tcp/host:post`;
144
+
* UDP: `udp/host:port`.
144
145
145
146
146
147
Architecture (v2)
-6
conf/Caddyfile
-6
conf/Caddyfile
···
25
25
on_demand
26
26
}
27
27
28
-
# initial PUT/POST for a new domain has to happen over HTTP
29
-
@upgrade `method('GET') && protocol('http')`
30
-
redir @upgrade https://{host}{uri} 301
31
-
32
28
reverse_proxy http://localhost:3000
33
-
header Alt-Svc `h3=":443"; persist=1, h2=":443"; persist=1`
34
-
encode
35
29
}
+4
conf/config.docker.toml
+4
conf/config.docker.toml
+4
-5
conf/config.example.toml
+4
-5
conf/config.example.toml
···
2
2
# as the intrinsic default value.
3
3
4
4
log-format = "text"
5
-
log-level = "info"
6
5
7
6
[server]
8
7
# Use "-" to disable the handler.
9
-
pages = "tcp/:3000"
10
-
caddy = "tcp/:3001"
11
-
metrics = "tcp/:3002"
8
+
pages = "tcp/localhost:3000"
9
+
caddy = "tcp/localhost:3001"
10
+
metrics = "tcp/localhost:3002"
12
11
13
12
[[wildcard]] # non-default section
14
13
domain = "codeberg.page"
···
51
50
update-timeout = "60s"
52
51
max-heap-size-ratio = 0.5 # * RAM_size
53
52
forbidden-domains = []
54
-
# allowed-repository-url-prefixes = <nil>
53
+
allowed-repository-url-prefixes = []
55
54
allowed-custom-headers = ["X-Clacks-Overhead"]
56
55
57
56
[audit]
+24
flake.lock
+24
flake.lock
···
18
18
"type": "github"
19
19
}
20
20
},
21
+
"gomod2nix": {
22
+
"inputs": {
23
+
"flake-utils": [
24
+
"flake-utils"
25
+
],
26
+
"nixpkgs": [
27
+
"nixpkgs"
28
+
]
29
+
},
30
+
"locked": {
31
+
"lastModified": 1763982521,
32
+
"narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=",
33
+
"owner": "nix-community",
34
+
"repo": "gomod2nix",
35
+
"rev": "02e63a239d6eabd595db56852535992c898eba72",
36
+
"type": "github"
37
+
},
38
+
"original": {
39
+
"owner": "nix-community",
40
+
"repo": "gomod2nix",
41
+
"type": "github"
42
+
}
43
+
},
21
44
"nix-filter": {
22
45
"locked": {
23
46
"lastModified": 1757882181,
···
52
75
"root": {
53
76
"inputs": {
54
77
"flake-utils": "flake-utils",
78
+
"gomod2nix": "gomod2nix",
55
79
"nix-filter": "nix-filter",
56
80
"nixpkgs": "nixpkgs"
57
81
}
+19
-4
flake.nix
+19
-4
flake.nix
···
3
3
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
4
4
flake-utils.url = "github:numtide/flake-utils";
5
5
nix-filter.url = "github:numtide/nix-filter";
6
+
7
+
gomod2nix = {
8
+
url = "github:nix-community/gomod2nix";
9
+
inputs.nixpkgs.follows = "nixpkgs";
10
+
inputs.flake-utils.follows = "flake-utils";
11
+
};
6
12
};
7
13
8
14
outputs =
···
11
17
nixpkgs,
12
18
flake-utils,
13
19
nix-filter,
14
-
}:
20
+
...
21
+
}@inputs:
15
22
flake-utils.lib.eachDefaultSystem (
16
23
system:
17
24
let
18
-
pkgs = nixpkgs.legacyPackages.${system};
25
+
pkgs = import nixpkgs {
26
+
inherit system;
27
+
28
+
overlays = [
29
+
inputs.gomod2nix.overlays.default
30
+
];
31
+
};
19
32
20
-
git-pages = pkgs.buildGo125Module {
33
+
git-pages = pkgs.buildGoApplication {
21
34
pname = "git-pages";
22
35
version = "0";
23
36
···
43
56
"-s -w"
44
57
];
45
58
46
-
vendorHash = "sha256-CIEDUWnd5Sth3yYNtw+w1ucYqLCacO34G+EDXVe4+6o=";
59
+
go = pkgs.go_1_25;
60
+
modules = ./gomod2nix.toml;
47
61
};
48
62
in
49
63
{
···
56
70
57
71
packages = with pkgs; [
58
72
caddy
73
+
gomod2nix
59
74
];
60
75
};
61
76
+14
-11
go.mod
+14
-11
go.mod
···
3
3
go 1.25.0
4
4
5
5
require (
6
-
codeberg.org/git-pages/go-headers v1.1.0
7
-
codeberg.org/git-pages/go-slog-syslog v0.0.0-20251122144254-06c45d430fb9
6
+
codeberg.org/git-pages/go-headers v1.1.1
7
+
codeberg.org/git-pages/go-slog-syslog v0.0.0-20251207093707-892f654e80b7
8
8
github.com/KimMachineGun/automemlimit v0.7.5
9
9
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
10
10
github.com/creasty/defaults v1.8.0
···
12
12
github.com/fatih/color v1.18.0
13
13
github.com/getsentry/sentry-go v0.40.0
14
14
github.com/getsentry/sentry-go/slog v0.40.0
15
-
github.com/go-git/go-billy/v6 v6.0.0-20251206100608-d4862421331a
16
-
github.com/go-git/go-git/v6 v6.0.0-20251206100705-e633db5b9a34
15
+
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd
16
+
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19
17
17
github.com/jpillora/backoff v1.0.0
18
18
github.com/kankanreno/go-snowflake v1.2.0
19
19
github.com/klauspost/compress v1.18.2
20
-
github.com/maypok86/otter/v2 v2.2.1
20
+
github.com/maypok86/otter/v2 v2.3.0
21
21
github.com/minio/minio-go/v7 v7.0.97
22
22
github.com/pelletier/go-toml/v2 v2.2.4
23
23
github.com/pquerna/cachecontrol v0.2.0
24
24
github.com/prometheus/client_golang v1.23.2
25
-
github.com/samber/slog-multi v1.6.0
25
+
github.com/samber/slog-multi v1.7.0
26
26
github.com/tj/go-redirects v0.0.0-20200911105812-fd1ba1020b37
27
27
github.com/valyala/fasttemplate v1.2.2
28
-
google.golang.org/protobuf v1.36.10
28
+
golang.org/x/net v0.48.0
29
+
google.golang.org/protobuf v1.36.11
29
30
)
30
31
31
32
require (
···
35
36
github.com/cespare/xxhash/v2 v2.3.0 // indirect
36
37
github.com/cloudflare/circl v1.6.1 // indirect
37
38
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
39
+
github.com/davecgh/go-spew v1.1.1 // indirect
38
40
github.com/dustin/go-humanize v1.0.1 // indirect
39
41
github.com/emirpasic/gods v1.18.1 // indirect
40
42
github.com/go-git/gcfg/v2 v2.0.2 // indirect
···
54
56
github.com/philhofer/fwd v1.2.0 // indirect
55
57
github.com/pjbgf/sha1cd v0.5.0 // indirect
56
58
github.com/pkg/errors v0.9.1 // indirect
59
+
github.com/pmezard/go-difflib v1.0.0 // indirect
57
60
github.com/prometheus/client_model v0.6.2 // indirect
58
61
github.com/prometheus/common v0.66.1 // indirect
59
62
github.com/prometheus/procfs v0.16.1 // indirect
···
61
64
github.com/samber/lo v1.52.0 // indirect
62
65
github.com/samber/slog-common v0.19.0 // indirect
63
66
github.com/sergi/go-diff v1.4.0 // indirect
67
+
github.com/stretchr/testify v1.11.1 // indirect
64
68
github.com/tinylib/msgp v1.3.0 // indirect
65
69
github.com/tj/assert v0.0.3 // indirect
66
70
github.com/valyala/bytebufferpool v1.0.0 // indirect
67
71
go.yaml.in/yaml/v2 v2.4.2 // indirect
68
-
golang.org/x/crypto v0.45.0 // indirect
69
-
golang.org/x/net v0.47.0 // indirect
70
-
golang.org/x/sys v0.38.0 // indirect
71
-
golang.org/x/text v0.31.0 // indirect
72
+
golang.org/x/crypto v0.46.0 // indirect
73
+
golang.org/x/sys v0.39.0 // indirect
74
+
golang.org/x/text v0.32.0 // indirect
72
75
gopkg.in/yaml.v3 v3.0.1 // indirect
73
76
)
+26
-28
go.sum
+26
-28
go.sum
···
1
-
codeberg.org/git-pages/go-headers v1.1.0 h1:rk7/SOSsn+XuL7PUQZFYUaWKHEaj6K8mXmUV9rF2VxE=
2
-
codeberg.org/git-pages/go-headers v1.1.0/go.mod h1:N4gwH0U3YPwmuyxqH7xBA8j44fTPX+vOEP7ejJVBPts=
3
-
codeberg.org/git-pages/go-slog-syslog v0.0.0-20251122144254-06c45d430fb9 h1:xfPDg8ThBt3+t+C+pvM3bEH4ePUzP5t5kY2v19TqgKc=
4
-
codeberg.org/git-pages/go-slog-syslog v0.0.0-20251122144254-06c45d430fb9/go.mod h1:8NPSXbYcVb71qqNM5cIgn1/uQgMisLbu2dVD1BNxsUw=
1
+
codeberg.org/git-pages/go-headers v1.1.1 h1:fpIBELKo66Z2k+gCeYl5mCEXVQ99Lmx1iup1nbo2shE=
2
+
codeberg.org/git-pages/go-headers v1.1.1/go.mod h1:N4gwH0U3YPwmuyxqH7xBA8j44fTPX+vOEP7ejJVBPts=
3
+
codeberg.org/git-pages/go-slog-syslog v0.0.0-20251207093707-892f654e80b7 h1:+rkrAxhNZo/eKEcKOqVOsF6ohAPv5amz0JLburOeRjs=
4
+
codeberg.org/git-pages/go-slog-syslog v0.0.0-20251207093707-892f654e80b7/go.mod h1:8NPSXbYcVb71qqNM5cIgn1/uQgMisLbu2dVD1BNxsUw=
5
5
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
6
6
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
7
7
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
···
31
31
github.com/dghubble/trie v0.1.0/go.mod h1:sOmnzfBNH7H92ow2292dDFWNsVQuh/izuD7otCYb1ak=
32
32
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
33
33
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
34
-
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
35
-
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
36
34
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
37
35
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
38
36
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
···
47
45
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
48
46
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
49
47
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
50
-
github.com/go-git/go-billy/v6 v6.0.0-20251206100608-d4862421331a h1:8JM2eaLX/ObLssDAowWTqw53RIKrMKC9n6QUGq9hA8g=
51
-
github.com/go-git/go-billy/v6 v6.0.0-20251206100608-d4862421331a/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
52
-
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251203093322-2d981fbae6b7 h1:f8lec5CHzeDgHKzEBZKD6MwAUeaYDfIT+aCL9bU/TqY=
53
-
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251203093322-2d981fbae6b7/go.mod h1:LzlZlYf8eQeXZKsd2azifbQGsaiTkcjI5WxzH1Wiyhg=
54
-
github.com/go-git/go-git/v6 v6.0.0-20251206100705-e633db5b9a34 h1:zvQHay88dsz9zO+61k0CmmFo3VAcTBtGlxTwDbnHG0w=
55
-
github.com/go-git/go-git/v6 v6.0.0-20251206100705-e633db5b9a34/go.mod h1:djt5SZ0fMrkORuVAxrZlwtRMw+hnqfZZVqWFH/uQAMI=
48
+
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
49
+
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
50
+
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
51
+
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
52
+
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
53
+
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
56
54
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
57
55
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
58
56
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
···
90
88
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
91
89
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
92
90
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
93
-
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
94
-
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
91
+
github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w=
92
+
github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8=
95
93
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
96
94
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
97
95
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
···
132
130
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
133
131
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
134
132
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
135
-
github.com/samber/slog-multi v1.6.0 h1:i1uBY+aaln6ljwdf7Nrt4Sys8Kk6htuYuXDHWJsHtZg=
136
-
github.com/samber/slog-multi v1.6.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts=
133
+
github.com/samber/slog-multi v1.7.0 h1:GKhbkxU3ujkyMsefkuz4qvE6EcgtSuqjFisPnfdzVLI=
134
+
github.com/samber/slog-multi v1.7.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts=
137
135
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
138
136
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
139
137
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
···
155
153
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
156
154
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
157
155
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
158
-
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
159
-
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
160
-
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
161
-
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
156
+
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
157
+
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
158
+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
159
+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
162
160
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
163
161
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
164
-
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
165
-
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
166
-
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
167
-
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
168
-
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
169
-
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
170
-
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
171
-
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
162
+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
163
+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
164
+
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
165
+
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
166
+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
167
+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
168
+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
169
+
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
172
170
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
173
171
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
174
172
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+204
gomod2nix.toml
+204
gomod2nix.toml
···
1
+
schema = 3
2
+
3
+
[mod]
4
+
[mod."codeberg.org/git-pages/go-headers"]
5
+
version = "v1.1.1"
6
+
hash = "sha256-qgL7l1FHXxcBWhBnBLEI0yENd6P+frvwlKxEAXLA3VY="
7
+
[mod."codeberg.org/git-pages/go-slog-syslog"]
8
+
version = "v0.0.0-20251207093707-892f654e80b7"
9
+
hash = "sha256-ye+DBIyxqTEOViYRrQPWyGJCaLmyKSDwH5btlqDPizM="
10
+
[mod."github.com/KimMachineGun/automemlimit"]
11
+
version = "v0.7.5"
12
+
hash = "sha256-lH/ip9j2hbYUc2W/XIYve/5TScQPZtEZe3hu76CY//k="
13
+
[mod."github.com/Microsoft/go-winio"]
14
+
version = "v0.6.2"
15
+
hash = "sha256-tVNWDUMILZbJvarcl/E7tpSnkn7urqgSHa2Eaka5vSU="
16
+
[mod."github.com/ProtonMail/go-crypto"]
17
+
version = "v1.3.0"
18
+
hash = "sha256-TUG+C4MyeWglOmiwiW2/NUVurFHXLgEPRd3X9uQ1NGI="
19
+
[mod."github.com/beorn7/perks"]
20
+
version = "v1.0.1"
21
+
hash = "sha256-h75GUqfwJKngCJQVE5Ao5wnO3cfKD9lSIteoLp/3xJ4="
22
+
[mod."github.com/c2h5oh/datasize"]
23
+
version = "v0.0.0-20231215233829-aa82cc1e6500"
24
+
hash = "sha256-8MqL7xCvE6fIjanz2jwkaLP1OE5kLu62TOcQx452DHQ="
25
+
[mod."github.com/cespare/xxhash/v2"]
26
+
version = "v2.3.0"
27
+
hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY="
28
+
[mod."github.com/cloudflare/circl"]
29
+
version = "v1.6.1"
30
+
hash = "sha256-Dc69V12eIFnJoUNmwg6VKXHfAMijbAeEVSDe8AiOaLo="
31
+
[mod."github.com/creasty/defaults"]
32
+
version = "v1.8.0"
33
+
hash = "sha256-I1LE1cfOhMS5JxB7+fWTKieefw2Gge1UhIZh+A6pa6s="
34
+
[mod."github.com/cyphar/filepath-securejoin"]
35
+
version = "v0.6.1"
36
+
hash = "sha256-obqip8c1c9mjXFznyXF8aDnpcMw7ttzv+e28anCa/v0="
37
+
[mod."github.com/davecgh/go-spew"]
38
+
version = "v1.1.1"
39
+
hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI="
40
+
[mod."github.com/dghubble/trie"]
41
+
version = "v0.1.0"
42
+
hash = "sha256-hVh7uYylpMCCSPcxl70hJTmzSwaA1MxBmJFBO5Xdncc="
43
+
[mod."github.com/dustin/go-humanize"]
44
+
version = "v1.0.1"
45
+
hash = "sha256-yuvxYYngpfVkUg9yAmG99IUVmADTQA0tMbBXe0Fq0Mc="
46
+
[mod."github.com/emirpasic/gods"]
47
+
version = "v1.18.1"
48
+
hash = "sha256-hGDKddjLj+5dn2woHtXKUdd49/3xdsqnhx7VEdCu1m4="
49
+
[mod."github.com/fatih/color"]
50
+
version = "v1.18.0"
51
+
hash = "sha256-pP5y72FSbi4j/BjyVq/XbAOFjzNjMxZt2R/lFFxGWvY="
52
+
[mod."github.com/getsentry/sentry-go"]
53
+
version = "v0.40.0"
54
+
hash = "sha256-mJ+EzM8WRzJ2Yp7ithDJNceU4+GbzQyi46yc8J8d13Y="
55
+
[mod."github.com/getsentry/sentry-go/slog"]
56
+
version = "v0.40.0"
57
+
hash = "sha256-uc9TpKiWMEpRbxwV2uGQeq1DDdZi+APOgu2StVzzEkw="
58
+
[mod."github.com/go-git/gcfg/v2"]
59
+
version = "v2.0.2"
60
+
hash = "sha256-icqMDeC/tEg/3979EuEN67Ml5KjdDA0R3QvR6iLLrSI="
61
+
[mod."github.com/go-git/go-billy/v6"]
62
+
version = "v6.0.0-20251217170237-e9738f50a3cd"
63
+
hash = "sha256-b2yunYcPUiLTU+Rr8qTBdsDEfsIhZDYmyqKW5udmpFY="
64
+
[mod."github.com/go-git/go-git/v6"]
65
+
version = "v6.0.0-20251224103503-78aff6aa5ea9"
66
+
hash = "sha256-kYjDqH0NZ+sxQnj5K8xKfO2WOVKtQ/7tWcqY6KYqAZE="
67
+
[mod."github.com/go-ini/ini"]
68
+
version = "v1.67.0"
69
+
hash = "sha256-V10ahGNGT+NLRdKUyRg1dos5RxLBXBk1xutcnquc/+4="
70
+
[mod."github.com/golang/groupcache"]
71
+
version = "v0.0.0-20241129210726-2c02b8208cf8"
72
+
hash = "sha256-AdLZ3dJLe/yduoNvZiXugZxNfmwJjNQyQGsIdzYzH74="
73
+
[mod."github.com/google/uuid"]
74
+
version = "v1.6.0"
75
+
hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw="
76
+
[mod."github.com/jpillora/backoff"]
77
+
version = "v1.0.0"
78
+
hash = "sha256-uxHg68NN8hrwPCrPfLYYprZHf7dMyEoPoF46JFx0IHU="
79
+
[mod."github.com/kankanreno/go-snowflake"]
80
+
version = "v1.2.0"
81
+
hash = "sha256-713xGEqjwaUGIu2EHII5sldWmcquFpxZmte/7R/O6LA="
82
+
[mod."github.com/kevinburke/ssh_config"]
83
+
version = "v1.4.0"
84
+
hash = "sha256-UclxB7Ll1FZCgU2SrGkiGdr4CoSRJ127MNnZtxKTsvg="
85
+
[mod."github.com/klauspost/compress"]
86
+
version = "v1.18.2"
87
+
hash = "sha256-mRa+6qEi5joqQao13ZFogmq67rOQzHCVbCCjKA+HKEc="
88
+
[mod."github.com/klauspost/cpuid/v2"]
89
+
version = "v2.3.0"
90
+
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
91
+
[mod."github.com/klauspost/crc32"]
92
+
version = "v1.3.0"
93
+
hash = "sha256-RsS/MDJbVzVB+i74whqABgwZJWMw+AutF6HhJBVgbag="
94
+
[mod."github.com/leodido/go-syslog/v4"]
95
+
version = "v4.3.0"
96
+
hash = "sha256-fCJ2rgrrPR/Ey/PoAsJhd8Sl8mblAnnMAmBuoWFBTgg="
97
+
[mod."github.com/mattn/go-colorable"]
98
+
version = "v0.1.13"
99
+
hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
100
+
[mod."github.com/mattn/go-isatty"]
101
+
version = "v0.0.20"
102
+
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
103
+
[mod."github.com/maypok86/otter/v2"]
104
+
version = "v2.3.0"
105
+
hash = "sha256-ELzmi/s2WqDeUmzSGnfx+ys2Hs28XHqF7vlEzyRotIA="
106
+
[mod."github.com/minio/crc64nvme"]
107
+
version = "v1.1.0"
108
+
hash = "sha256-OwlE70X91WO4HdbpGsOaB4w12Qrk0duCpfLeAskiqY8="
109
+
[mod."github.com/minio/md5-simd"]
110
+
version = "v1.1.2"
111
+
hash = "sha256-vykcXvy2VBBAXnJott/XsGTT0gk2UL36JzZKfJ1KAUY="
112
+
[mod."github.com/minio/minio-go/v7"]
113
+
version = "v7.0.97"
114
+
hash = "sha256-IwF14tWVYjBi28jUG9iFYd4Lpbc7Fvyy0zRzEZ82UEE="
115
+
[mod."github.com/munnerz/goautoneg"]
116
+
version = "v0.0.0-20191010083416-a7dc8b61c822"
117
+
hash = "sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q="
118
+
[mod."github.com/pbnjay/memory"]
119
+
version = "v0.0.0-20210728143218-7b4eea64cf58"
120
+
hash = "sha256-QI+F1oPLOOtwNp8+m45OOoSfYFs3QVjGzE0rFdpF/IA="
121
+
[mod."github.com/pelletier/go-toml/v2"]
122
+
version = "v2.2.4"
123
+
hash = "sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q="
124
+
[mod."github.com/philhofer/fwd"]
125
+
version = "v1.2.0"
126
+
hash = "sha256-cGx2/0QQay46MYGZuamFmU0TzNaFyaO+J7Ddzlr/3dI="
127
+
[mod."github.com/pjbgf/sha1cd"]
128
+
version = "v0.5.0"
129
+
hash = "sha256-11XBkhdciQGsQ7jEMZ6PgphRKjruTSc7ZxfOwDuPCr8="
130
+
[mod."github.com/pkg/errors"]
131
+
version = "v0.9.1"
132
+
hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
133
+
[mod."github.com/pmezard/go-difflib"]
134
+
version = "v1.0.0"
135
+
hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA="
136
+
[mod."github.com/pquerna/cachecontrol"]
137
+
version = "v0.2.0"
138
+
hash = "sha256-tuTERCFfwmqPepw/rs5cyv9fArCD30BqgjZqwMV+vzQ="
139
+
[mod."github.com/prometheus/client_golang"]
140
+
version = "v1.23.2"
141
+
hash = "sha256-3GD4fBFa1tJu8MS4TNP6r2re2eViUE+kWUaieIOQXCg="
142
+
[mod."github.com/prometheus/client_model"]
143
+
version = "v0.6.2"
144
+
hash = "sha256-q6Fh6v8iNJN9ypD47LjWmx66YITa3FyRjZMRsuRTFeQ="
145
+
[mod."github.com/prometheus/common"]
146
+
version = "v0.66.1"
147
+
hash = "sha256-bqHPaV9IV70itx63wqwgy2PtxMN0sn5ThVxDmiD7+Tk="
148
+
[mod."github.com/prometheus/procfs"]
149
+
version = "v0.16.1"
150
+
hash = "sha256-OBCvKlLW2obct35p0L9Q+1ZrxZjpTmbgHMP2rng9hpo="
151
+
[mod."github.com/rs/xid"]
152
+
version = "v1.6.0"
153
+
hash = "sha256-rJB7h3KuH1DPp5n4dY3MiGnV1Y96A10lf5OUl+MLkzU="
154
+
[mod."github.com/samber/lo"]
155
+
version = "v1.52.0"
156
+
hash = "sha256-xgMsPJv3rydHH10NZU8wz/DhK2VbbR8ymivOg1ChTp0="
157
+
[mod."github.com/samber/slog-common"]
158
+
version = "v0.19.0"
159
+
hash = "sha256-OYXVbZML7c3mFClVy8GEnNoWW+4OfcBsxWDtKh1u7B8="
160
+
[mod."github.com/samber/slog-multi"]
161
+
version = "v1.6.0"
162
+
hash = "sha256-uebbTcvsBP2LdOUIjDptES+HZOXxThnIt3+FKL0qJy4="
163
+
[mod."github.com/sergi/go-diff"]
164
+
version = "v1.4.0"
165
+
hash = "sha256-rs9NKpv/qcQEMRg7CmxGdP4HGuFdBxlpWf9LbA9wS4k="
166
+
[mod."github.com/stretchr/testify"]
167
+
version = "v1.11.1"
168
+
hash = "sha256-sWfjkuKJyDllDEtnM8sb/pdLzPQmUYWYtmeWz/5suUc="
169
+
[mod."github.com/tinylib/msgp"]
170
+
version = "v1.3.0"
171
+
hash = "sha256-PnpndO7k5Yl036vhWJGDsrcz0jsTX8sUiTqm/D3rAVw="
172
+
[mod."github.com/tj/assert"]
173
+
version = "v0.0.3"
174
+
hash = "sha256-4xhmZcHpUafabaXejE9ucVnGxG/txomvKzBg6cbkusg="
175
+
[mod."github.com/tj/go-redirects"]
176
+
version = "v0.0.0-20200911105812-fd1ba1020b37"
177
+
hash = "sha256-GpYpxdT4F7PkwGXLo7cYVcIRJrzd1sKHtFDH+bRb6Tk="
178
+
[mod."github.com/valyala/bytebufferpool"]
179
+
version = "v1.0.0"
180
+
hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY="
181
+
[mod."github.com/valyala/fasttemplate"]
182
+
version = "v1.2.2"
183
+
hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0="
184
+
[mod."go.yaml.in/yaml/v2"]
185
+
version = "v2.4.2"
186
+
hash = "sha256-oC8RWdf1zbMYCtmR0ATy/kCkhIwPR9UqFZSMOKLVF/A="
187
+
[mod."golang.org/x/crypto"]
188
+
version = "v0.46.0"
189
+
hash = "sha256-I8N/spcw3/h0DFA+V1WK38HctckWIB9ep93DEVCALxU="
190
+
[mod."golang.org/x/net"]
191
+
version = "v0.48.0"
192
+
hash = "sha256-oZpddsiJwWCH3Aipa+XXpy7G/xHY5fEagUSok7T0bXE="
193
+
[mod."golang.org/x/sys"]
194
+
version = "v0.39.0"
195
+
hash = "sha256-dxTBu/JAWUkPbjFIXXRFdhQWyn+YyEpIC+tWqGo0Y6U="
196
+
[mod."golang.org/x/text"]
197
+
version = "v0.32.0"
198
+
hash = "sha256-9PXtWBKKY9rG4AgjSP4N+I1DhepXhy8SF/vWSIDIoWs="
199
+
[mod."google.golang.org/protobuf"]
200
+
version = "v1.36.11"
201
+
hash = "sha256-7W+6jntfI/awWL3JP6yQedxqP5S9o3XvPgJ2XxxsIeE="
202
+
[mod."gopkg.in/yaml.v3"]
203
+
version = "v3.0.1"
204
+
hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="
+3
-1
renovate.json
+3
-1
renovate.json
+1
-5
src/audit.go
+1
-5
src/audit.go
···
265
265
var _ Backend = (*auditedBackend)(nil)
266
266
267
267
func NewAuditedBackend(backend Backend) Backend {
268
-
if config.Feature("audit") {
269
-
return &auditedBackend{backend}
270
-
} else {
271
-
return backend
272
-
}
268
+
return &auditedBackend{backend}
273
269
}
274
270
275
271
// This function does not retry appending audit records; as such, if it returns an error,
+32
-7
src/auth.go
+32
-7
src/auth.go
···
12
12
"slices"
13
13
"strings"
14
14
"time"
15
+
16
+
"golang.org/x/net/idna"
15
17
)
16
18
17
19
type AuthError struct {
···
42
44
return nil
43
45
}
44
46
47
+
var idnaProfile = idna.New(idna.MapForLookup(), idna.BidiRule())
48
+
45
49
func GetHost(r *http.Request) (string, error) {
46
-
// FIXME: handle IDNA
47
50
host, _, err := net.SplitHostPort(r.Host)
48
51
if err != nil {
49
-
// dirty but the go stdlib doesn't have a "split port if present" function
50
52
host = r.Host
51
53
}
52
-
if strings.HasPrefix(host, ".") {
54
+
// this also rejects invalid characters and labels
55
+
host, err = idnaProfile.ToASCII(host)
56
+
if err != nil {
57
+
if config.Feature("relaxed-idna") {
58
+
// unfortunately, the go IDNA library has some significant issues around its
59
+
// Unicode TR46 implementation: https://github.com/golang/go/issues/76804
60
+
// we would like to allow *just* the _ here, but adding `idna.StrictDomainName(false)`
61
+
// would also accept domains like `*.foo.bar` which should clearly be disallowed.
62
+
// as a workaround, accept a domain name if it is valid with all `_` characters
63
+
// replaced with an alphanumeric character (we use `a`); this allows e.g. `foo_bar.xxx`
64
+
// and `foo__bar.xxx`, as well as `_foo.xxx` and `foo_.xxx`. labels starting with
65
+
// an underscore are explicitly rejected below.
66
+
_, err = idnaProfile.ToASCII(strings.ReplaceAll(host, "_", "a"))
67
+
}
68
+
if err != nil {
69
+
return "", AuthError{http.StatusBadRequest,
70
+
fmt.Sprintf("malformed host name %q", host)}
71
+
}
72
+
}
73
+
if strings.HasPrefix(host, ".") || strings.HasPrefix(host, "_") {
53
74
return "", AuthError{http.StatusBadRequest,
54
-
fmt.Sprintf("host name %q is reserved", host)}
75
+
fmt.Sprintf("reserved host name %q", host)}
55
76
}
56
77
host = strings.TrimSuffix(host, ".")
57
78
return host, nil
58
79
}
59
80
81
+
func IsValidProjectName(name string) bool {
82
+
return !strings.HasPrefix(name, ".") && !strings.Contains(name, "%")
83
+
}
84
+
60
85
func GetProjectName(r *http.Request) (string, error) {
61
86
// path must be either `/` or `/foo/` (`/foo` is accepted as an alias)
62
87
path := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/")
63
-
if path == ".index" || strings.HasPrefix(path, ".index/") {
88
+
if !IsValidProjectName(path) {
64
89
return "", AuthError{http.StatusBadRequest,
65
90
fmt.Sprintf("directory name %q is reserved", ".index")}
66
91
} else if strings.Contains(path, "/") {
···
436
461
}
437
462
438
463
func checkAllowedURLPrefix(repoURL string) error {
439
-
if config.Limits.AllowedRepositoryURLPrefixes != nil {
464
+
if len(config.Limits.AllowedRepositoryURLPrefixes) > 0 {
440
465
allowedPrefix := false
441
466
repoURL = strings.ToLower(repoURL)
442
467
for _, allowedRepoURLPrefix := range config.Limits.AllowedRepositoryURLPrefixes {
···
658
683
return auth, nil
659
684
}
660
685
661
-
if config.Limits.AllowedRepositoryURLPrefixes != nil {
686
+
if len(config.Limits.AllowedRepositoryURLPrefixes) > 0 {
662
687
causes = append(causes, AuthError{http.StatusUnauthorized, "DNS challenge not allowed"})
663
688
} else {
664
689
// DNS challenge gives absolute authority.
+4
-5
src/config.go
+4
-5
src/config.go
···
63
63
Insecure bool `toml:"-" env:"insecure"`
64
64
Features []string `toml:"features"`
65
65
LogFormat string `toml:"log-format" default:"text"`
66
-
LogLevel string `toml:"log-level" default:"info"`
67
66
Server ServerConfig `toml:"server"`
68
67
Wildcard []WildcardConfig `toml:"wildcard"`
69
68
Fallback FallbackConfig `toml:"fallback"`
···
74
73
}
75
74
76
75
type ServerConfig struct {
77
-
Pages string `toml:"pages" default:"tcp/:3000"`
78
-
Caddy string `toml:"caddy" default:"tcp/:3001"`
79
-
Metrics string `toml:"metrics" default:"tcp/:3002"`
76
+
Pages string `toml:"pages" default:"tcp/localhost:3000"`
77
+
Caddy string `toml:"caddy" default:"tcp/localhost:3001"`
78
+
Metrics string `toml:"metrics" default:"tcp/localhost:3002"`
80
79
}
81
80
82
81
type WildcardConfig struct {
···
140
139
// List of domains unconditionally forbidden for uploads.
141
140
ForbiddenDomains []string `toml:"forbidden-domains" default:"[]"`
142
141
// List of allowed repository URL prefixes. Setting this option prohibits uploading archives.
143
-
AllowedRepositoryURLPrefixes []string `toml:"allowed-repository-url-prefixes"`
142
+
AllowedRepositoryURLPrefixes []string `toml:"allowed-repository-url-prefixes" default:"[]"`
144
143
// List of allowed custom headers. Header name must be in the MIME canonical form,
145
144
// e.g. `Foo-Bar`. Setting this option permits including this custom header in `_headers`,
146
145
// unless it is fundamentally unsafe.
+20
src/extract.go
+20
src/extract.go
···
9
9
"errors"
10
10
"fmt"
11
11
"io"
12
+
"math"
12
13
"os"
13
14
"strings"
14
15
···
144
145
return nil, UnresolvedRefError{missing}
145
146
}
146
147
148
+
// Ensure parent directories exist for all entries.
149
+
EnsureLeadingDirectories(manifest)
150
+
147
151
logc.Printf(ctx,
148
152
"reuse: %s recycled, %s transferred\n",
149
153
datasize.ByteSize(dataBytesRecycled).HR(),
···
153
157
return manifest, nil
154
158
}
155
159
160
+
// Used for zstd decompression inside zip files, it is recommended to share this.
161
+
var zstdDecomp = zstd.ZipDecompressor()
162
+
156
163
func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (*Manifest, error) {
157
164
data, err := io.ReadAll(reader)
158
165
if err != nil {
···
163
170
if err != nil {
164
171
return nil, err
165
172
}
173
+
174
+
// Support zstd compression inside zip files.
175
+
archive.RegisterDecompressor(zstd.ZipMethodWinZip, zstdDecomp)
176
+
archive.RegisterDecompressor(zstd.ZipMethodPKWare, zstdDecomp)
166
177
167
178
// Detect and defuse zipbombs.
168
179
var totalSize uint64
169
180
for _, file := range archive.File {
181
+
if totalSize+file.UncompressedSize64 < totalSize {
182
+
// Would overflow
183
+
totalSize = math.MaxUint64
184
+
break
185
+
}
170
186
totalSize += file.UncompressedSize64
171
187
}
172
188
if totalSize > config.Limits.MaxSiteSize.Bytes() {
···
213
229
return nil, UnresolvedRefError{missing}
214
230
}
215
231
232
+
// Ensure parent directories exist for all entries.
233
+
EnsureLeadingDirectories(manifest)
234
+
216
235
logc.Printf(ctx,
217
236
"reuse: %s recycled, %s transferred\n",
218
237
datasize.ByteSize(dataBytesRecycled).HR(),
···
221
240
222
241
return manifest, nil
223
242
}
243
+
+19
-6
src/fetch.go
+19
-6
src/fetch.go
···
23
23
"google.golang.org/protobuf/proto"
24
24
)
25
25
26
+
var ErrRepositoryTooLarge = errors.New("repository too large")
27
+
26
28
func FetchRepository(
27
29
ctx context.Context, repoURL string, branch string, oldManifest *Manifest,
28
30
) (
···
57
59
repo, err = git.CloneContext(ctx, storer, nil, &git.CloneOptions{
58
60
Bare: true,
59
61
URL: repoURL,
60
-
ReferenceName: plumbing.ReferenceName(branch),
62
+
ReferenceName: plumbing.NewBranchReferenceName(branch),
61
63
SingleBranch: true,
62
64
Depth: 1,
63
65
Tags: git.NoTags,
···
152
154
// This will only succeed if a `blob:none` filter isn't supported and we got a full
153
155
// clone despite asking for a partial clone.
154
156
for hash, manifestEntry := range blobsNeeded {
155
-
if err := readGitBlob(repo, hash, manifestEntry); err == nil {
156
-
dataBytesTransferred += manifestEntry.GetOriginalSize()
157
+
if err := readGitBlob(repo, hash, manifestEntry, &dataBytesTransferred); err == nil {
157
158
delete(blobsNeeded, hash)
159
+
} else if errors.Is(err, ErrRepositoryTooLarge) {
160
+
return nil, err
158
161
}
159
162
}
160
163
···
193
196
194
197
// All remaining blobs should now be available.
195
198
for hash, manifestEntry := range blobsNeeded {
196
-
if err := readGitBlob(repo, hash, manifestEntry); err != nil {
199
+
if err := readGitBlob(repo, hash, manifestEntry, &dataBytesTransferred); err != nil {
197
200
return nil, err
198
201
}
199
-
dataBytesTransferred += manifestEntry.GetOriginalSize()
200
202
delete(blobsNeeded, hash)
201
203
}
202
204
}
···
210
212
return manifest, nil
211
213
}
212
214
213
-
func readGitBlob(repo *git.Repository, hash plumbing.Hash, entry *Entry) error {
215
+
func readGitBlob(
216
+
repo *git.Repository, hash plumbing.Hash, entry *Entry, bytesTransferred *int64,
217
+
) error {
214
218
blob, err := repo.BlobObject(hash)
215
219
if err != nil {
216
220
return fmt.Errorf("git blob %s: %w", hash, err)
···
239
243
entry.Transform = Transform_Identity.Enum()
240
244
entry.OriginalSize = proto.Int64(blob.Size)
241
245
entry.CompressedSize = proto.Int64(blob.Size)
246
+
247
+
*bytesTransferred += blob.Size
248
+
if uint64(*bytesTransferred) > config.Limits.MaxSiteSize.Bytes() {
249
+
return fmt.Errorf("%w: fetch exceeds %s limit",
250
+
ErrRepositoryTooLarge,
251
+
config.Limits.MaxSiteSize.HR(),
252
+
)
253
+
}
254
+
242
255
return nil
243
256
}
+34
-1
src/main.go
+34
-1
src/main.go
···
14
14
"net/http/httputil"
15
15
"net/url"
16
16
"os"
17
+
"path"
17
18
"runtime/debug"
18
19
"strings"
19
20
"time"
···
217
218
"display audit log")
218
219
auditRead := flag.String("audit-read", "",
219
220
"extract contents of audit record `id` to files '<id>-*'")
221
+
auditRollback := flag.String("audit-rollback", "",
222
+
"restore site from contents of audit record `id`")
220
223
auditServer := flag.String("audit-server", "",
221
224
"listen for notifications on `endpoint` and spawn a process for each audit event")
222
225
runMigration := flag.String("run-migration", "",
···
237
240
*unfreezeDomain != "",
238
241
*auditLog,
239
242
*auditRead != "",
243
+
*auditRollback != "",
240
244
*auditServer != "",
241
245
*runMigration != "",
242
246
*traceGarbage,
···
248
252
if cliOperations > 1 {
249
253
logc.Fatalln(ctx, "-list-blobs, -list-manifests, -get-blob, -get-manifest, -get-archive, "+
250
254
"-update-site, -freeze-domain, -unfreeze-domain, -audit-log, -audit-read, "+
251
-
"-audit-server, -run-migration, and -trace-garbage are mutually exclusive")
255
+
"-audit-rollback, -audit-server, -run-migration, and -trace-garbage are "+
256
+
"mutually exclusive")
252
257
}
253
258
254
259
if *configTomlPath != "" && *noConfig {
···
480
485
}
481
486
482
487
if err = ExtractAuditRecord(ctx, id, record, "."); err != nil {
488
+
logc.Fatalln(ctx, err)
489
+
}
490
+
491
+
case *auditRollback != "":
492
+
ctx = WithPrincipal(ctx)
493
+
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
494
+
495
+
id, err := ParseAuditID(*auditRollback)
496
+
if err != nil {
497
+
logc.Fatalln(ctx, err)
498
+
}
499
+
500
+
record, err := backend.QueryAuditLog(ctx, id)
501
+
if err != nil {
502
+
logc.Fatalln(ctx, err)
503
+
}
504
+
505
+
if record.GetManifest() == nil || record.GetDomain() == "" || record.GetProject() == "" {
506
+
logc.Fatalln(ctx, "no manifest in audit record")
507
+
}
508
+
509
+
webRoot := path.Join(record.GetDomain(), record.GetProject())
510
+
err = backend.StageManifest(ctx, record.GetManifest())
511
+
if err != nil {
512
+
logc.Fatalln(ctx, err)
513
+
}
514
+
err = backend.CommitManifest(ctx, webRoot, record.GetManifest(), ModifyManifestOptions{})
515
+
if err != nil {
483
516
logc.Fatalln(ctx, err)
484
517
}
485
518
+28
-4
src/manifest.go
+28
-4
src/manifest.go
···
144
144
return fmt.Errorf("%s: %s", pathName, cause)
145
145
}
146
146
147
+
// EnsureLeadingDirectories adds directory entries for any parent directories
148
+
// that are implicitly referenced by files in the manifest but don't have
149
+
// explicit directory entries. (This can be the case if an archive is created
150
+
// via globs rather than including a whole directory.)
151
+
func EnsureLeadingDirectories(manifest *Manifest) {
152
+
for name := range manifest.Contents {
153
+
for dir := path.Dir(name); dir != "." && dir != ""; dir = path.Dir(dir) {
154
+
if _, exists := manifest.Contents[dir]; !exists {
155
+
AddDirectory(manifest, dir)
156
+
}
157
+
}
158
+
}
159
+
}
160
+
147
161
func GetProblemReport(manifest *Manifest) []string {
148
162
var report []string
149
163
for _, problem := range manifest.Problems {
150
164
report = append(report,
151
-
fmt.Sprintf("%s: %s", problem.GetPath(), problem.GetCause()))
165
+
fmt.Sprintf("/%s: %s", problem.GetPath(), problem.GetCause()))
152
166
}
153
167
return report
154
168
}
···
283
297
return nil
284
298
}
285
299
300
+
var ErrSiteTooLarge = errors.New("site too large")
286
301
var ErrManifestTooLarge = errors.New("manifest too large")
287
302
288
303
// Uploads inline file data over certain size to the storage backend. Returns a copy of
···
325
340
}
326
341
}
327
342
328
-
// Compute the deduplicated storage size.
329
-
var blobSizes = make(map[string]int64)
330
-
for _, entry := range manifest.Contents {
343
+
// Compute the total and deduplicated storage size.
344
+
totalSize := int64(0)
345
+
blobSizes := map[string]int64{}
346
+
for _, entry := range extManifest.Contents {
347
+
totalSize += entry.GetOriginalSize()
331
348
if entry.GetType() == Type_ExternalFile {
332
349
blobSizes[string(entry.Data)] = entry.GetCompressedSize()
333
350
}
351
+
}
352
+
if uint64(totalSize) > config.Limits.MaxSiteSize.Bytes() {
353
+
return nil, fmt.Errorf("%w: contents size %s exceeds %s limit",
354
+
ErrSiteTooLarge,
355
+
datasize.ByteSize(totalSize).HR(),
356
+
config.Limits.MaxSiteSize.HR(),
357
+
)
334
358
}
335
359
for _, blobSize := range blobSizes {
336
360
*extManifest.StoredSize += blobSize
+68
-58
src/observe.go
+68
-58
src/observe.go
···
13
13
"os"
14
14
"runtime/debug"
15
15
"strconv"
16
-
"strings"
17
16
"sync"
18
17
"time"
19
18
···
52
51
return os.Getenv("SENTRY_DSN") != ""
53
52
}
54
53
54
+
func chainSentryMiddleware(
55
+
middleware ...func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event,
56
+
) func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
57
+
return func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
58
+
for idx := 0; idx < len(middleware) && event != nil; idx++ {
59
+
event = middleware[idx](event, hint)
60
+
}
61
+
return event
62
+
}
63
+
}
64
+
65
+
// sensitiveHTTPHeaders extends the list of sensitive headers defined in the Sentry Go SDK with our
66
+
// own application-specific header field names.
67
+
var sensitiveHTTPHeaders = map[string]struct{}{
68
+
"Forge-Authorization": {},
69
+
}
70
+
71
+
// scrubSentryEvent removes sensitive HTTP header fields from the Sentry event.
72
+
func scrubSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
73
+
if event.Request != nil && event.Request.Headers != nil {
74
+
for key := range event.Request.Headers {
75
+
if _, ok := sensitiveHTTPHeaders[key]; ok {
76
+
delete(event.Request.Headers, key)
77
+
}
78
+
}
79
+
}
80
+
return event
81
+
}
82
+
83
+
// sampleSentryEvent returns a function that discards a Sentry event according to the sample rate,
84
+
// unless the associated HTTP request triggers a mutation or it took too long to produce a response,
85
+
// in which case the event is never discarded.
86
+
func sampleSentryEvent(sampleRate float64) func(*sentry.Event, *sentry.EventHint) *sentry.Event {
87
+
return func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
88
+
newSampleRate := sampleRate
89
+
if event.Request != nil {
90
+
switch event.Request.Method {
91
+
case "PUT", "POST", "DELETE":
92
+
newSampleRate = 1
93
+
}
94
+
}
95
+
duration := event.Timestamp.Sub(event.StartTime)
96
+
threshold := time.Duration(config.Observability.SlowResponseThreshold)
97
+
if duration >= threshold {
98
+
newSampleRate = 1
99
+
}
100
+
if rand.Float64() < newSampleRate {
101
+
return event
102
+
}
103
+
return nil
104
+
}
105
+
}
106
+
55
107
func InitObservability() {
56
108
debug.SetPanicOnFault(true)
57
109
···
62
114
63
115
logHandlers := []slog.Handler{}
64
116
65
-
logLevel := slog.LevelInfo
66
-
switch strings.ToLower(config.LogLevel) {
67
-
case "debug":
68
-
logLevel = slog.LevelDebug
69
-
case "info":
70
-
logLevel = slog.LevelInfo
71
-
case "warn":
72
-
logLevel = slog.LevelWarn
73
-
case "error":
74
-
logLevel = slog.LevelError
75
-
default:
76
-
log.Println("unknown log level", config.LogLevel)
77
-
}
78
-
79
117
switch config.LogFormat {
80
118
case "none":
81
119
// nothing to do
82
120
case "text":
83
121
logHandlers = append(logHandlers,
84
-
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
122
+
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
85
123
case "json":
86
124
logHandlers = append(logHandlers,
87
-
slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
125
+
slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{}))
88
126
default:
89
127
log.Println("unknown log format", config.LogFormat)
90
128
}
···
111
149
enableTracing := false
112
150
if value, err := strconv.ParseBool(os.Getenv("SENTRY_TRACING")); err == nil {
113
151
enableTracing = value
152
+
}
153
+
154
+
tracesSampleRate := 1.00
155
+
switch environment {
156
+
case "development", "staging":
157
+
default:
158
+
tracesSampleRate = 0.05
114
159
}
115
160
116
161
options := sentry.ClientOptions{}
···
118
163
options.Environment = environment
119
164
options.EnableLogs = enableLogs
120
165
options.EnableTracing = enableTracing
121
-
options.TracesSampleRate = 1
122
-
switch environment {
123
-
case "development", "staging":
124
-
default:
125
-
options.BeforeSendTransaction = func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
126
-
sampleRate := 0.05
127
-
if trace, ok := event.Contexts["trace"]; ok {
128
-
if data, ok := trace["data"].(map[string]any); ok {
129
-
if method, ok := data["http.request.method"].(string); ok {
130
-
switch method {
131
-
case "PUT", "DELETE", "POST":
132
-
sampleRate = 1
133
-
default:
134
-
duration := event.Timestamp.Sub(event.StartTime)
135
-
threshold := time.Duration(config.Observability.SlowResponseThreshold)
136
-
if duration >= threshold {
137
-
sampleRate = 1
138
-
}
139
-
}
140
-
}
141
-
}
142
-
}
143
-
if rand.Float64() < sampleRate {
144
-
return event
145
-
}
146
-
return nil
147
-
}
148
-
}
166
+
options.TracesSampleRate = 1 // use our own custom sampling logic
167
+
options.BeforeSend = scrubSentryEvent
168
+
options.BeforeSendTransaction = chainSentryMiddleware(
169
+
sampleSentryEvent(tracesSampleRate),
170
+
scrubSentryEvent,
171
+
)
149
172
if err := sentry.Init(options); err != nil {
150
173
log.Fatalf("sentry: %s\n", err)
151
174
}
···
153
176
if enableLogs {
154
177
logHandlers = append(logHandlers, sentryslog.Option{
155
178
AddSource: true,
156
-
LogLevel: levelsFromMinimum(logLevel),
157
179
}.NewSentryHandler(context.Background()))
158
180
}
159
181
}
160
182
161
183
slog.SetDefault(slog.New(slogmulti.Fanout(logHandlers...)))
162
-
}
163
-
164
-
// From sentryslog, because for some reason they don't make it public.
165
-
func levelsFromMinimum(minLevel slog.Level) []slog.Level {
166
-
allLevels := []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal}
167
-
var result []slog.Level
168
-
for _, level := range allLevels {
169
-
if level >= minLevel {
170
-
result = append(result, level)
171
-
}
172
-
}
173
-
return result
174
184
}
175
185
176
186
func FiniObservability() {
+39
-22
src/pages.go
+39
-22
src/pages.go
···
132
132
err = nil
133
133
sitePath = strings.TrimPrefix(r.URL.Path, "/")
134
134
if projectName, projectPath, hasProjectSlash := strings.Cut(sitePath, "/"); projectName != "" {
135
-
var projectManifest *Manifest
136
-
var projectMetadata ManifestMetadata
137
-
projectManifest, projectMetadata, err = backend.GetManifest(
138
-
r.Context(), makeWebRoot(host, projectName),
139
-
GetManifestOptions{BypassCache: bypassCache},
140
-
)
141
-
if err == nil {
142
-
if !hasProjectSlash {
143
-
writeRedirect(w, http.StatusFound, r.URL.Path+"/")
144
-
return nil
135
+
if IsValidProjectName(projectName) {
136
+
var projectManifest *Manifest
137
+
var projectMetadata ManifestMetadata
138
+
projectManifest, projectMetadata, err = backend.GetManifest(
139
+
r.Context(), makeWebRoot(host, projectName),
140
+
GetManifestOptions{BypassCache: bypassCache},
141
+
)
142
+
if err == nil {
143
+
if !hasProjectSlash {
144
+
writeRedirect(w, http.StatusFound, r.URL.Path+"/")
145
+
return nil
146
+
}
147
+
sitePath, manifest, metadata = projectPath, projectManifest, projectMetadata
145
148
}
146
-
sitePath, manifest, metadata = projectPath, projectManifest, projectMetadata
147
149
}
148
150
}
149
151
if manifest == nil && (err == nil || errors.Is(err, ErrObjectNotFound)) {
···
214
216
// we only offer `/.git-pages/archive.tar` and not the `.tar.gz`/`.tar.zst` variants
215
217
// because HTTP can already request compression using the `Content-Encoding` mechanism
216
218
acceptedEncodings := ParseAcceptEncodingHeader(r.Header.Get("Accept-Encoding"))
219
+
w.Header().Add("Vary", "Accept-Encoding")
217
220
negotiated := acceptedEncodings.Negotiate("zstd", "gzip", "identity")
218
221
if negotiated != "" {
219
222
w.Header().Set("Content-Encoding", negotiated)
···
325
328
326
329
var offeredEncodings []string
327
330
acceptedEncodings := ParseAcceptEncodingHeader(r.Header.Get("Accept-Encoding"))
331
+
w.Header().Add("Vary", "Accept-Encoding")
328
332
negotiatedEncoding := true
329
333
switch entry.GetTransform() {
330
334
case Transform_Identity:
···
415
419
io.Copy(w, reader)
416
420
}
417
421
} else {
418
-
// consider content fresh for 60 seconds (the same as the freshness interval of
419
-
// manifests in the S3 backend), and use stale content anyway as long as it's not
420
-
// older than a hour; while it is cheap to handle If-Modified-Since queries
421
-
// server-side, on the client `max-age=0, must-revalidate` causes every resource
422
-
// to block the page load every time
423
-
w.Header().Set("Cache-Control", "max-age=60, stale-while-revalidate=3600")
424
-
// see https://web.dev/articles/stale-while-revalidate for details
422
+
if _, hasCacheControl := w.Header()["Cache-Control"]; !hasCacheControl {
423
+
// consider content fresh for 60 seconds (the same as the freshness interval of
424
+
// manifests in the S3 backend), and use stale content anyway as long as it's not
425
+
// older than a hour; while it is cheap to handle If-Modified-Since queries
426
+
// server-side, on the client `max-age=0, must-revalidate` causes every resource
427
+
// to block the page load every time
428
+
w.Header().Set("Cache-Control", "max-age=60, stale-while-revalidate=3600")
429
+
// see https://web.dev/articles/stale-while-revalidate for details
430
+
}
425
431
426
432
// http.ServeContent handles conditional requests and range requests
427
433
http.ServeContent(w, r, entryPath, mtime, reader)
···
597
603
598
604
switch result.outcome {
599
605
case UpdateError:
600
-
if errors.Is(result.err, ErrManifestTooLarge) {
601
-
w.WriteHeader(http.StatusRequestEntityTooLarge)
606
+
if errors.Is(result.err, ErrSiteTooLarge) {
607
+
w.WriteHeader(http.StatusUnprocessableEntity)
608
+
} else if errors.Is(result.err, ErrManifestTooLarge) {
609
+
w.WriteHeader(http.StatusUnprocessableEntity)
602
610
} else if errors.Is(result.err, errArchiveFormat) {
603
611
w.WriteHeader(http.StatusUnsupportedMediaType)
604
612
} else if errors.Is(result.err, ErrArchiveTooLarge) {
605
613
w.WriteHeader(http.StatusRequestEntityTooLarge)
614
+
} else if errors.Is(result.err, ErrRepositoryTooLarge) {
615
+
w.WriteHeader(http.StatusUnprocessableEntity)
606
616
} else if errors.Is(result.err, ErrMalformedPatch) {
607
617
w.WriteHeader(http.StatusUnprocessableEntity)
608
618
} else if errors.Is(result.err, ErrPreconditionFailed) {
···
762
772
result := UpdateFromRepository(ctx, webRoot, repoURL, auth.branch)
763
773
resultChan <- result
764
774
observeSiteUpdate("webhook", &result)
765
-
}(r.Context())
775
+
}(context.WithoutCancel(r.Context()))
766
776
767
777
var result UpdateResult
768
778
select {
···
810
820
// any intentional deviation is an opportunity to miss an issue that will affect our
811
821
// visitors but not our health checks.
812
822
if r.Header.Get("Health-Check") == "" {
813
-
logc.Println(r.Context(), "pages:", r.Method, r.Host, r.URL, r.Header.Get("Content-Type"))
823
+
var mediaType string
824
+
switch r.Method {
825
+
case "HEAD", "GET":
826
+
mediaType = r.Header.Get("Accept")
827
+
default:
828
+
mediaType = r.Header.Get("Content-Type")
829
+
}
830
+
logc.Println(r.Context(), "pages:", r.Method, r.Host, r.URL, mediaType)
814
831
if region := os.Getenv("FLY_REGION"); region != "" {
815
832
machine_id := os.Getenv("FLY_MACHINE_ID")
816
833
w.Header().Add("Server", fmt.Sprintf("git-pages (fly.io; %s; %s)", region, machine_id))
+55
src/pages_test.go
+55
src/pages_test.go
···
1
+
package git_pages
2
+
3
+
import (
4
+
"net/http"
5
+
"strings"
6
+
"testing"
7
+
)
8
+
9
+
func checkHost(t *testing.T, host string, expectOk string, expectErr string) {
10
+
host, err := GetHost(&http.Request{Host: host})
11
+
if expectErr != "" {
12
+
if err == nil || !strings.HasPrefix(err.Error(), expectErr) {
13
+
t.Errorf("%s: expect err %s, got err %s", host, expectErr, err)
14
+
}
15
+
}
16
+
if expectOk != "" {
17
+
if err != nil {
18
+
t.Errorf("%s: expect ok %s, got err %s", host, expectOk, err)
19
+
} else if host != expectOk {
20
+
t.Errorf("%s: expect ok %s, got ok %s", host, expectOk, host)
21
+
}
22
+
}
23
+
}
24
+
25
+
func TestHelloName(t *testing.T) {
26
+
config = &Config{Features: []string{}}
27
+
28
+
checkHost(t, "foo.bar", "foo.bar", "")
29
+
checkHost(t, "foo-baz.bar", "foo-baz.bar", "")
30
+
checkHost(t, "foo--baz.bar", "foo--baz.bar", "")
31
+
checkHost(t, "foo.bar.", "foo.bar", "")
32
+
checkHost(t, ".foo.bar", "", "reserved host name")
33
+
checkHost(t, "..foo.bar", "", "reserved host name")
34
+
35
+
checkHost(t, "ร.bar", "xn--zca.bar", "")
36
+
checkHost(t, "xn--zca.bar", "xn--zca.bar", "")
37
+
38
+
checkHost(t, "foo-.bar", "", "malformed host name")
39
+
checkHost(t, "-foo.bar", "", "malformed host name")
40
+
checkHost(t, "foo_.bar", "", "malformed host name")
41
+
checkHost(t, "_foo.bar", "", "malformed host name")
42
+
checkHost(t, "foo_baz.bar", "", "malformed host name")
43
+
checkHost(t, "foo__baz.bar", "", "malformed host name")
44
+
checkHost(t, "*.foo.bar", "", "malformed host name")
45
+
46
+
config = &Config{Features: []string{"relaxed-idna"}}
47
+
48
+
checkHost(t, "foo-.bar", "", "malformed host name")
49
+
checkHost(t, "-foo.bar", "", "malformed host name")
50
+
checkHost(t, "foo_.bar", "foo_.bar", "")
51
+
checkHost(t, "_foo.bar", "", "reserved host name")
52
+
checkHost(t, "foo_baz.bar", "foo_baz.bar", "")
53
+
checkHost(t, "foo__baz.bar", "foo__baz.bar", "")
54
+
checkHost(t, "*.foo.bar", "", "malformed host name")
55
+
}
+29
src/signal.go
+29
src/signal.go
···
1
+
// See https://pkg.go.dev/os/signal#hdr-Windows for a description of what this module
2
+
// will do on Windows (tl;dr nothing calls the reload handler, the interrupt handler works
3
+
// more or less how you'd expect).
4
+
5
+
package git_pages
6
+
7
+
import (
8
+
"os"
9
+
"os/signal"
10
+
"syscall"
11
+
)
12
+
13
+
func OnReload(handler func()) {
14
+
sighup := make(chan os.Signal, 1)
15
+
signal.Notify(sighup, syscall.SIGHUP)
16
+
go func() {
17
+
for {
18
+
<-sighup
19
+
handler()
20
+
}
21
+
}()
22
+
}
23
+
24
+
func WaitForInterrupt() {
25
+
sigint := make(chan os.Signal, 1)
26
+
signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM)
27
+
<-sigint
28
+
signal.Stop(sigint)
29
+
}
-13
src/signal_other.go
-13
src/signal_other.go
-27
src/signal_posix.go
-27
src/signal_posix.go
···
1
-
//go:build unix
2
-
3
-
package git_pages
4
-
5
-
import (
6
-
"os"
7
-
"os/signal"
8
-
"syscall"
9
-
)
10
-
11
-
func OnReload(handler func()) {
12
-
sighup := make(chan os.Signal, 1)
13
-
signal.Notify(sighup, syscall.SIGHUP)
14
-
go func() {
15
-
for {
16
-
<-sighup
17
-
handler()
18
-
}
19
-
}()
20
-
}
21
-
22
-
func WaitForInterrupt() {
23
-
sigint := make(chan os.Signal, 1)
24
-
signal.Notify(sigint, syscall.SIGINT)
25
-
<-sigint
26
-
signal.Stop(sigint)
27
-
}
+3
src/update.go
+3
src/update.go
···
182
182
// `*Manifest` objects, which should never be mutated.
183
183
newManifest := &Manifest{}
184
184
proto.Merge(newManifest, oldManifest)
185
+
newManifest.RepoUrl = nil
186
+
newManifest.Branch = nil
187
+
newManifest.Commit = nil
185
188
if err := ApplyTarPatch(newManifest, reader, parents); err != nil {
186
189
return nil, err
187
190
} else {