Merge branch 'forgejo' into upload_with_path_structure

This commit is contained in:
David Rotermund 2025-06-11 12:53:35 +02:00
commit 63c9d504fd
27 changed files with 608 additions and 241 deletions

View file

@ -6,7 +6,7 @@
"ghcr.io/devcontainers/features/node:1": {
"version": "22"
},
"ghcr.io/devcontainers/features/git-lfs:1.2.3": {},
"ghcr.io/devcontainers/features/git-lfs:1.2.4": {},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
},
"customizations": {

View file

@ -18,7 +18,7 @@ runs:
- name: install packages
run: |
apt-get update -qq
apt-get -q install -qq -y ${PACKAGES}
apt-get -q install --allow-downgrades -qq -y ${PACKAGES}
env:
PACKAGES: ${{inputs.packages}}
- name: remove temporary package list to prevent using it in other steps

View file

@ -0,0 +1,71 @@
#
# Additional integration tests designed to run once a day when
# `mirror.yml` pushes to https://codeberg.org/forgejo-integration/forgejo
# and send a notification via email should they fail.
#
# For debug purposes:
#
# - uncomment [on].pull_request
# - swap 'forgejo-integration' and 'forgejo-coding'
# - open a pull request at https://codeberg.org/forgejo/forgejo and fix things
# - swap 'forgejo-integration' and 'forgejo-coding'
# - comment [on].pull_request
#
name: testing-integration
on:
# pull_request:
push:
tags: 'v[0-9]+.[0-9]+.*'
branches:
- 'forgejo'
- 'v*/forgejo'
jobs:
test-unit:
# if: vars.ROLE == 'forgejo-coding'
if: vars.ROLE == 'forgejo-integration'
runs-on: docker
container:
image: 'data.forgejo.org/oci/node:22-bookworm'
options: --tmpfs /tmp:exec,noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env
- name: install git 2.30
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git/bullseye git-lfs/bullseye
release: bullseye
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-backend test-check'
timeout-minutes: 120
env:
RACE_ENABLED: 'true'
TAGS: bindata
test-sqlite:
# if: vars.ROLE == 'forgejo-coding'
if: vars.ROLE == 'forgejo-integration'
runs-on: docker
container:
image: 'data.forgejo.org/oci/node:22-bookworm'
options: --tmpfs /tmp:exec,noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env
- name: install git 2.30
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git/bullseye git-lfs/bullseye
release: bullseye
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-sqlite-migration test-sqlite'
timeout-minutes: 120
env:
TAGS: sqlite sqlite_unlock_notify
RACE_ENABLED: true
TEST_TAGS: sqlite sqlite_unlock_notify
USE_REPO_TEST_DIR: 1

View file

@ -1,14 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build vendor
package main
// Libraries that are included to vendor utilities used during build.
// These libraries will not be included in a normal compilation.
import (
// for embed
_ "github.com/shurcooL/vfsgen"
)

View file

@ -1,5 +1,6 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build ignore
@ -7,15 +8,18 @@ package main
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"text/template"
"github.com/shurcooL/vfsgen"
"github.com/klauspost/compress/zstd"
)
func needsUpdate(dir, filename string) (bool, []byte) {
@ -30,7 +34,7 @@ func needsUpdate(dir, filename string) (bool, []byte) {
oldHash = []byte{}
}
hasher := sha1.New()
hasher := sha256.New()
err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
@ -51,7 +55,7 @@ func needsUpdate(dir, filename string) (bool, []byte) {
newHash := hasher.Sum([]byte{})
if bytes.Compare(oldHash, newHash) != 0 {
if !bytes.Equal(oldHash, newHash) {
return true, newHash
}
@ -77,16 +81,268 @@ func main() {
}
fmt.Printf("generating bindata for %s\n", packageName)
var fsTemplates http.FileSystem = http.Dir(dir)
err := vfsgen.Generate(fsTemplates, vfsgen.Options{
PackageName: packageName,
BuildTags: "bindata",
VariableName: "Assets",
Filename: filename,
UseGlobalModTime: useGlobalModTime,
})
root, err := os.OpenRoot(dir)
if err != nil {
log.Fatalf("%v\n", err)
log.Fatal(err)
}
out, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
log.Fatal(err)
}
defer out.Close()
if err := generate(root.FS(), packageName, useGlobalModTime, out); err != nil {
log.Fatal(err)
}
_ = os.WriteFile(filename+".hash", newHash, 0o666)
}
type file struct {
Path string
Name string
UncompressedSize int
CompressedData []byte
UncompressedData []byte
}
type direntry struct {
Name string
IsDir bool
}
func generate(fsRoot fs.FS, packageName string, globalTime bool, output io.Writer) error {
enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
return err
}
files := []file{}
dirs := map[string][]direntry{}
if err := fs.WalkDir(fsRoot, ".", func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
entries, err := fs.ReadDir(fsRoot, filePath)
if err != nil {
return err
}
dirEntries := make([]direntry, 0, len(entries))
for _, entry := range entries {
dirEntries = append(dirEntries, direntry{Name: entry.Name(), IsDir: entry.IsDir()})
}
dirs[filePath] = dirEntries
return nil
}
src, err := fs.ReadFile(fsRoot, filePath)
if err != nil {
return err
}
dst := enc.EncodeAll(src, nil)
if len(dst) < len(src) {
files = append(files, file{
Path: filePath,
Name: path.Base(filePath),
UncompressedSize: len(src),
CompressedData: dst,
})
} else {
files = append(files, file{
Path: filePath,
Name: path.Base(filePath),
UncompressedData: src,
})
}
return nil
}); err != nil {
return err
}
return generatedTmpl.Execute(output, map[string]any{
"Packagename": packageName,
"GlobalTime": globalTime,
"Files": files,
"Dirs": dirs,
})
}
var generatedTmpl = template.Must(template.New("").Parse(`// Code generated by efs-gen. DO NOT EDIT.
//go:build bindata
package {{.Packagename}}
import (
"bytes"
"time"
"io"
"io/fs"
"github.com/klauspost/compress/zstd"
)
type normalFile struct {
name string
content []byte
}
type compressedFile struct {
name string
uncompressedSize int64
data []byte
}
var files = map[string]any{
{{- range .Files}}
"{{.Path}}": {{if .CompressedData}}compressedFile{"{{.Name}}", {{.UncompressedSize}}, []byte({{printf "%+q" .CompressedData}})}{{else}}normalFile{"{{.Name}}", []byte({{printf "%+q" .UncompressedData}})}{{end}},
{{- end}}
}
var dirs = map[string][]fs.DirEntry{
{{- range $key, $entry := .Dirs}}
"{{$key}}": {
{{- range $entry}}
direntry{"{{.Name}}", {{.IsDir}}},
{{- end}}
},
{{- end}}
}
type assets struct{}
var Assets = assets{}
func (a assets) Open(name string) (fs.File, error) {
f, ok := files[name]
if !ok {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
switch f := f.(type) {
case normalFile:
return file{name: f.name, size: int64(len(f.content)), data: bytes.NewReader(f.content)}, nil
case compressedFile:
r, _ := zstd.NewReader(bytes.NewReader(f.data))
return &compressFile{name: f.name, size: f.uncompressedSize, data: r, content: f.data}, nil
default:
panic("unknown file type")
}
}
func (a assets) ReadDir(name string) ([]fs.DirEntry, error) {
d, ok := dirs[name]
if !ok {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return d, nil
}
type file struct {
name string
size int64
data io.ReadSeeker
}
var _ io.ReadSeeker = (*file)(nil)
func (f file) Stat() (fs.FileInfo, error) {
return fileinfo{name: f.name, size: f.size}, nil
}
func (f file) Read(p []byte) (int, error) {
return f.data.Read(p)
}
func (f file) Seek(offset int64, whence int) (int64, error) {
return f.data.Seek(offset, whence)
}
func (f file) Close() error { return nil }
type compressFile struct {
name string
size int64
data *zstd.Decoder
content []byte
zstdPos int64
seekPos int64
}
var _ io.ReadSeeker = (*compressFile)(nil)
func (f *compressFile) Stat() (fs.FileInfo, error) {
return fileinfo{name: f.name, size: f.size}, nil
}
func (f *compressFile) Read(p []byte) (int, error) {
if f.zstdPos > f.seekPos {
if err := f.data.Reset(bytes.NewReader(f.content)); err != nil {
return 0, err
}
f.zstdPos = 0
}
if f.zstdPos < f.seekPos {
if _, err := io.CopyN(io.Discard, f.data, f.seekPos - f.zstdPos); err != nil {
return 0, err
}
f.zstdPos = f.seekPos
}
n, err := f.data.Read(p)
f.zstdPos += int64(n)
f.seekPos = f.zstdPos
return n, err
}
func (f *compressFile) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
f.seekPos = 0 + offset
case io.SeekCurrent:
f.seekPos += offset
case io.SeekEnd:
f.seekPos = f.size + offset
}
return f.seekPos, nil
}
func (f *compressFile) Close() error {
f.data.Close()
return nil
}
func (f *compressFile) ZstdBytes() []byte { return f.content }
type fileinfo struct {
name string
size int64
}
func (f fileinfo) Name() string { return f.name }
func (f fileinfo) Size() int64 { return f.size }
func (f fileinfo) Mode() fs.FileMode { return 0o444 }
func (f fileinfo) ModTime() time.Time { return {{if .GlobalTime}}GlobalModTime(f.name){{else}}time.Unix(0, 0){{end}} }
func (f fileinfo) IsDir() bool { return false }
func (f fileinfo) Sys() any { return nil }
type direntry struct {
name string
isDir bool
}
func (d direntry) Name() string { return d.name }
func (d direntry) IsDir() bool { return d.isDir }
func (d direntry) Type() fs.FileMode {
if d.isDir {
return 0o755 | fs.ModeDir
}
return 0o444
}
func (direntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist }
`))

6
flake.lock generated
View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1733392399,
"narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=",
"lastModified": 1749285348,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
"type": "github"
},
"original": {

View file

@ -3,35 +3,20 @@
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
outputs =
{
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system: let
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
# frontend
nodejs_20
# backend
gofumpt
sqlite
go
gopls
];
};
in
{
devShells.default = import ./shell.nix { inherit pkgs; };
formatter = pkgs.nixfmt-rfc-style;
}
);
}

4
go.mod
View file

@ -89,7 +89,6 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/sergi/go-diff v1.4.0
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
github.com/ulikunitz/xz v0.5.12
@ -222,7 +221,6 @@ require (
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
@ -246,8 +244,6 @@ require (
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.26.0
replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1

4
go.sum
View file

@ -384,8 +384,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 h1:F/3FfGmKdiKFa8kL3YrpZ7pe9H4l4AzA1pbaOUnRvPI=
github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0/go.mod h1:JEfTc3+2DF9Z4PXhLLvXL42zexJyh8rIq3OzUj/0rAk=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
@ -504,8 +502,6 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=

View file

@ -5,10 +5,10 @@ package assetfs
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"slices"
@ -25,7 +25,7 @@ import (
// Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
type Layer struct {
name string
fs http.FileSystem
fs fs.FS
localPath string
}
@ -34,10 +34,18 @@ func (l *Layer) Name() string {
}
// Open opens the named file. The caller is responsible for closing the file.
func (l *Layer) Open(name string) (http.File, error) {
func (l *Layer) Open(name string) (fs.File, error) {
return l.fs.Open(name)
}
func (l *Layer) ReadDir(name string) ([]fs.DirEntry, error) {
dirEntries, err := fs.ReadDir(l.fs, name)
if err != nil && errors.Is(err, fs.ErrNotExist) {
err = nil
}
return dirEntries, err
}
// Local returns a new Layer with the given name, it serves files from the given local path.
func Local(name, base string, sub ...string) *Layer {
// TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
@ -48,11 +56,18 @@ func Local(name, base string, sub ...string) *Layer {
panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
}
root := util.FilePathJoinAbs(base, sub...)
return &Layer{name: name, fs: http.Dir(root), localPath: root}
fsRoot, err := os.OpenRoot(root)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
panic(fmt.Sprintf("Unable to open layer %q", err))
}
return &Layer{name: name, fs: fsRoot.FS(), localPath: root}
}
// Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
func Bindata(name string, fs http.FileSystem) *Layer {
func Bindata(name string, fs fs.FS) *Layer {
return &Layer{name: name, fs: fs}
}
@ -65,11 +80,11 @@ type LayeredFS struct {
// Layered returns a new LayeredFS with the given layers. The first layer is the top layer.
func Layered(layers ...*Layer) *LayeredFS {
return &LayeredFS{layers: layers}
return &LayeredFS{layers: slices.DeleteFunc(layers, func(layer *Layer) bool { return layer == nil })}
}
// Open opens the named file. The caller is responsible for closing the file.
func (l *LayeredFS) Open(name string) (http.File, error) {
func (l *LayeredFS) Open(name string) (fs.File, error) {
for _, layer := range l.layers {
f, err := layer.Open(name)
if err == nil || !os.IsNotExist(err) {
@ -102,29 +117,18 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
return nil, "", fs.ErrNotExist
}
func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
func shouldInclude(info fs.DirEntry, fileMode ...bool) bool {
if util.CommonSkip(info.Name()) {
return false
}
if len(fileMode) == 0 {
return true
} else if len(fileMode) == 1 {
return fileMode[0] == !info.Mode().IsDir()
return fileMode[0] == !info.IsDir()
}
panic("too many arguments for fileMode in shouldInclude")
}
func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
f, err := layer.Open(name)
if os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, err
}
defer f.Close()
return f.Readdir(-1)
}
// ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
// * omitted: all files and directories will be returned.
// * true: only files will be returned.
@ -133,7 +137,7 @@ func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
fileSet := make(container.Set[string])
for _, layer := range l.layers {
infos, err := readDir(layer, name)
infos, err := layer.ReadDir(name)
if err != nil {
return nil, err
}
@ -162,7 +166,7 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err
var list func(dir string) error
list = func(dir string) error {
for _, layer := range layers {
infos, err := readDir(layer, dir)
infos, err := layer.ReadDir(dir)
if err != nil {
return err
}

View file

@ -105,6 +105,10 @@ func TestSyncConfigGPGFormat(t *testing.T) {
})
t.Run("SSH format", func(t *testing.T) {
if CheckGitVersionAtLeast("2.34.0") != nil {
t.SkipNow()
}
r, err := os.OpenRoot(t.TempDir())
require.NoError(t, err)
f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700)

View file

@ -6,6 +6,7 @@ package public
import (
"bytes"
"io"
"io/fs"
"net/http"
"os"
"path"
@ -59,7 +60,7 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
}
}
func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) {
func handleRequest(w http.ResponseWriter, req *http.Request, fs fs.FS, file string) {
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
f, err := fs.Open(util.PathJoinRelX(file))
if err != nil {
@ -86,33 +87,31 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem,
return
}
serveContent(w, req, fi, fi.ModTime(), f)
serveContent(w, req, fi.Name(), fi.ModTime(), f.(io.ReadSeeker))
}
type GzipBytesProvider interface {
GzipBytes() []byte
type ZstdBytesProvider interface {
ZstdBytes() []byte
}
// serveContent serve http content
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
setWellKnownContentType(w, fi.Name())
func serveContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) {
setWellKnownContentType(w, name)
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
if encodings.Contains("gzip") {
// try to provide gzip content directly from bindata (provided by vfsgen۰CompressedFileInfo)
if compressed, ok := fi.(GzipBytesProvider); ok {
rdGzip := bytes.NewReader(compressed.GzipBytes())
// all gzipped static files (from bindata) are managed by Gitea, so we can make sure every file has the correct ext name
// then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data
if encodings.Contains("zstd") {
// If the file was compressed, use the bytes directly.
if compressed, ok := content.(ZstdBytesProvider); ok {
rdZstd := bytes.NewReader(compressed.ZstdBytes())
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/octet-stream")
}
w.Header().Set("Content-Encoding", "gzip")
httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip)
w.Header().Set("Content-Encoding", "zstd")
httpcache.ServeContentWithCacheControl(w, req, name, modtime, rdZstd)
return
}
}
httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content)
httpcache.ServeContentWithCacheControl(w, req, name, modtime, content)
return
}

View file

@ -12,8 +12,6 @@ import (
"forgejo.org/modules/timeutil"
)
var _ GzipBytesProvider = (*vfsgen۰CompressedFileInfo)(nil)
// GlobalModTime provide a global mod time for embedded asset files
func GlobalModTime(filename string) time.Time {
return timeutil.GetExecutableModTime()

View file

@ -93,5 +93,6 @@
"editor.textarea.tab_hint": "Line already indented. Press <kbd>Tab</kbd> again or <kbd>Escape</kbd> to leave the editor.",
"editor.textarea.shift_tab_hint": "No indentation on this line. Press <kbd>Shift</kbd> + <kbd>Tab</kbd> again or <kbd>Escape</kbd> to leave the editor.",
"admin.dashboard.cleanup_offline_runners": "Cleanup offline runners",
"settings.visibility.description": "Profile visibility affects others' ability to access your non-private repositories. <a href=\"%s\" target=\"_blank\">Learn more</a>",
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
}

196
package-lock.json generated
View file

@ -39,7 +39,7 @@
"pdfobject": "2.3.0",
"postcss": "8.5.4",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"postcss-nesting": "13.0.2",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.6",
"swagger-ui-dist": "5.17.14",
@ -62,13 +62,13 @@
"devDependencies": {
"@axe-core/playwright": "4.10.2",
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
"@playwright/test": "1.52.0",
"@playwright/test": "1.53.0",
"@stoplight/spectral-cli": "6.15.0",
"@stylistic/eslint-plugin-js": "4.4.1",
"@stylistic/stylelint-plugin": "3.1.2",
"@vitejs/plugin-vue": "5.2.4",
"@vitest/coverage-v8": "3.2.3",
"@vitest/eslint-plugin": "1.2.1",
"@vitest/eslint-plugin": "1.2.2",
"@vue/test-utils": "2.4.6",
"eslint": "9.28.0",
"eslint-import-resolver-typescript": "4.4.3",
@ -86,7 +86,7 @@
"eslint-plugin-vue-scoped-css": "2.10.0",
"eslint-plugin-wc": "2.2.1",
"globals": "16.1.0",
"happy-dom": "17.6.3",
"happy-dom": "18.0.0",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.45.0",
"postcss-html": "1.8.0",
@ -97,7 +97,7 @@
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.2.0",
"typescript": "5.8.3",
"typescript-eslint": "8.33.1",
"typescript-eslint": "8.34.0",
"vite-string-plugin": "1.3.4",
"vitest": "3.2.3"
},
@ -2131,13 +2131,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
"integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz",
"integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.52.0"
"playwright": "1.53.0"
},
"bin": {
"playwright": "cli.js"
@ -3448,9 +3448,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
"version": "20.19.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@ -3493,18 +3493,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/whatwg-mimetype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz",
"integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz",
"integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.33.1",
"@typescript-eslint/type-utils": "8.33.1",
"@typescript-eslint/utils": "8.33.1",
"@typescript-eslint/visitor-keys": "8.33.1",
"@typescript-eslint/scope-manager": "8.34.0",
"@typescript-eslint/type-utils": "8.34.0",
"@typescript-eslint/utils": "8.34.0",
"@typescript-eslint/visitor-keys": "8.34.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -3518,7 +3525,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.33.1",
"@typescript-eslint/parser": "^8.34.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -3534,16 +3541,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz",
"integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz",
"integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.33.1",
"@typescript-eslint/types": "8.33.1",
"@typescript-eslint/typescript-estree": "8.33.1",
"@typescript-eslint/visitor-keys": "8.33.1",
"@typescript-eslint/scope-manager": "8.34.0",
"@typescript-eslint/types": "8.34.0",
"@typescript-eslint/typescript-estree": "8.34.0",
"@typescript-eslint/visitor-keys": "8.34.0",
"debug": "^4.3.4"
},
"engines": {
@ -3559,14 +3566,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz",
"integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz",
"integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.33.1",
"@typescript-eslint/types": "^8.33.1",
"@typescript-eslint/tsconfig-utils": "^8.34.0",
"@typescript-eslint/types": "^8.34.0",
"debug": "^4.3.4"
},
"engines": {
@ -3581,14 +3588,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz",
"integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz",
"integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.1",
"@typescript-eslint/visitor-keys": "8.33.1"
"@typescript-eslint/types": "8.34.0",
"@typescript-eslint/visitor-keys": "8.34.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3599,9 +3606,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz",
"integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz",
"integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==",
"dev": true,
"license": "MIT",
"engines": {
@ -3616,14 +3623,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz",
"integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz",
"integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.1",
"@typescript-eslint/utils": "8.33.1",
"@typescript-eslint/typescript-estree": "8.34.0",
"@typescript-eslint/utils": "8.34.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -3640,9 +3647,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz",
"integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz",
"integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==",
"dev": true,
"license": "MIT",
"engines": {
@ -3654,16 +3661,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz",
"integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz",
"integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.33.1",
"@typescript-eslint/tsconfig-utils": "8.33.1",
"@typescript-eslint/types": "8.33.1",
"@typescript-eslint/visitor-keys": "8.33.1",
"@typescript-eslint/project-service": "8.34.0",
"@typescript-eslint/tsconfig-utils": "8.34.0",
"@typescript-eslint/types": "8.34.0",
"@typescript-eslint/visitor-keys": "8.34.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -3699,16 +3706,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz",
"integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz",
"integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.33.1",
"@typescript-eslint/types": "8.33.1",
"@typescript-eslint/typescript-estree": "8.33.1"
"@typescript-eslint/scope-manager": "8.34.0",
"@typescript-eslint/types": "8.34.0",
"@typescript-eslint/typescript-estree": "8.34.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3723,13 +3730,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz",
"integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz",
"integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.1",
"@typescript-eslint/types": "8.34.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -4040,9 +4047,9 @@
}
},
"node_modules/@vitest/eslint-plugin": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.2.1.tgz",
"integrity": "sha512-JQr1jdVcrsoS7Sdzn83h9sq4DvREf9Q/onTZbJCqTVlv/76qb+TZrLv/9VhjnjSMHweQH5FdpMDeCR6aDe2fgw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.2.2.tgz",
"integrity": "sha512-R8NwW+VxyKqVGcMfYsUbdThQyMbtNcoeg+jJeTgMHqWdFdcS0nrODAQXhkplvWzgd7jIJ+GQeydGqFLibsxMxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -8429,13 +8436,14 @@
}
},
"node_modules/happy-dom": {
"version": "17.6.3",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.6.3.tgz",
"integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==",
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.0.tgz",
"integrity": "sha512-o3p2Axi1EdIfMaOUulDzO/5yXzLLV0g/54eLPVrkt3u20r3yOuOenHpyp2clAJ0eHMc+HyE139ulQxl+8pEJIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"webidl-conversions": "^7.0.0",
"@types/node": "^20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"whatwg-mimetype": "^3.0.0"
},
"engines": {
@ -11750,13 +11758,13 @@
}
},
"node_modules/playwright": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz",
"integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.52.0"
"playwright-core": "1.53.0"
},
"bin": {
"playwright": "cli.js"
@ -11769,9 +11777,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz",
"integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -12049,9 +12057,9 @@
}
},
"node_modules/postcss-nesting": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz",
"integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==",
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz",
"integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==",
"funding": [
{
"type": "github",
@ -12064,7 +12072,7 @@
],
"license": "MIT-0",
"dependencies": {
"@csstools/selector-resolve-nested": "^3.0.0",
"@csstools/selector-resolve-nested": "^3.1.0",
"@csstools/selector-specificity": "^5.0.0",
"postcss-selector-parser": "^7.0.0"
},
@ -14609,15 +14617,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.33.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz",
"integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==",
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz",
"integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.33.1",
"@typescript-eslint/parser": "8.33.1",
"@typescript-eslint/utils": "8.33.1"
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@typescript-eslint/utils": "8.34.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -15286,16 +15294,6 @@
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/webpack": {
"version": "5.99.9",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz",

View file

@ -38,7 +38,7 @@
"pdfobject": "2.3.0",
"postcss": "8.5.4",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"postcss-nesting": "13.0.2",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.6",
"swagger-ui-dist": "5.17.14",
@ -61,13 +61,13 @@
"devDependencies": {
"@axe-core/playwright": "4.10.2",
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
"@playwright/test": "1.52.0",
"@playwright/test": "1.53.0",
"@stoplight/spectral-cli": "6.15.0",
"@stylistic/eslint-plugin-js": "4.4.1",
"@stylistic/stylelint-plugin": "3.1.2",
"@vitejs/plugin-vue": "5.2.4",
"@vitest/coverage-v8": "3.2.3",
"@vitest/eslint-plugin": "1.2.1",
"@vitest/eslint-plugin": "1.2.2",
"@vue/test-utils": "2.4.6",
"eslint": "9.28.0",
"eslint-import-resolver-typescript": "4.4.3",
@ -85,7 +85,7 @@
"eslint-plugin-vue-scoped-css": "2.10.0",
"eslint-plugin-wc": "2.2.1",
"globals": "16.1.0",
"happy-dom": "17.6.3",
"happy-dom": "18.0.0",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.45.0",
"postcss-html": "1.8.0",
@ -96,7 +96,7 @@
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.2.0",
"typescript": "5.8.3",
"typescript-eslint": "8.33.1",
"typescript-eslint": "8.34.0",
"vite-string-plugin": "1.3.4",
"vitest": "3.2.3"
},

View file

@ -225,7 +225,7 @@ func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, ser
idToken := &oauth2.OIDCToken{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
Issuer: setting.AppURL,
Issuer: strings.TrimSuffix(setting.AppURL, "/"),
Audience: []string{app.ClientID},
Subject: fmt.Sprint(grant.UserID),
},
@ -409,7 +409,7 @@ func IntrospectOAuth(ctx *context.Context) {
if err == nil && app != nil {
response.Active = true
response.Scope = grant.Scope
response.Issuer = setting.AppURL
response.Issuer = strings.TrimSuffix(setting.AppURL, "/")
response.Audience = []string{app.ClientID}
response.Subject = fmt.Sprint(grant.UserID)
}
@ -669,6 +669,7 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) {
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
ctx.Data["Issuer"] = strings.TrimSuffix(setting.AppURL, "/")
ctx.JSONTemplate("user/auth/oidc_wellknown")
}

View file

@ -51,6 +51,7 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
// Scopes: openid
oidcToken := createAndParseToken(t, grants[0])
assert.Equal(t, "https://try.gitea.io", oidcToken.RegisteredClaims.Issuer)
assert.Empty(t, oidcToken.Name)
assert.Empty(t, oidcToken.PreferredUsername)
assert.Empty(t, oidcToken.Profile)
@ -67,6 +68,7 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
// Scopes: openid profile email
oidcToken = createAndParseToken(t, grants[0])
assert.Equal(t, "https://try.gitea.io", oidcToken.RegisteredClaims.Issuer)
assert.Equal(t, "User Five", oidcToken.Name)
assert.Equal(t, "user5", oidcToken.PreferredUsername)
assert.Equal(t, "https://try.gitea.io/user5", oidcToken.Profile)

28
shell.nix Normal file
View file

@ -0,0 +1,28 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
name = "forgejo";
nativeBuildInputs = with pkgs; [
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
# frontend
nodejs
# backend
gofumpt
sqlite
go
gopls
# tests
openssh
];
}

View file

@ -1,5 +1,5 @@
{
"issuer": "{{AppUrl | JSEscape}}",
"issuer": "{{.Issuer | JSEscape}}",
"authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize",
"token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token",
"jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys",

View file

@ -83,6 +83,7 @@
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "settings.visibility.description" "https://forgejo.org/docs/latest/user/repo-permissions/#profile-and-visibility"}}</span>
</label>
<label>

View file

@ -27,6 +27,10 @@ import (
func forEachObjectFormat(t *testing.T, f func(t *testing.T, objectFormat git.ObjectFormat)) {
for _, objectFormat := range []git.ObjectFormat{git.Sha256ObjectFormat, git.Sha1ObjectFormat} {
if !git.SupportHashSha256 && objectFormat == git.Sha256ObjectFormat {
continue
}
t.Run(objectFormat.Name(), func(t *testing.T) {
f(t, objectFormat)
})

View file

@ -632,6 +632,19 @@ func TestSignInOAuthCallbackPKCE(t *testing.T) {
})
}
func TestWellKnownDocumentIssuerDoesNotEndWithASlash(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/.well-known/openid-configuration")
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
Issuer string `json:"issuer"`
}
parsed := new(response)
DecodeJSON(t, resp, parsed)
assert.Equal(t, strings.TrimSuffix(setting.AppURL, "/"), parsed.Issuer)
}
func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@ -697,7 +710,7 @@ func setupMockOIDCServer() *httptest.Server {
case "/.well-known/openid-configuration":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
"issuer": "` + strings.TrimSuffix(mockServer.URL, "/") + `",
"authorization_endpoint": "` + mockServer.URL + `/authorize",
"token_endpoint": "` + mockServer.URL + `/token",
"userinfo_endpoint": "` + mockServer.URL + `/userinfo"

View file

@ -97,6 +97,17 @@ samp,
font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */
}
kbd {
padding: 0.15em 0.35em;
line-height: 10px;
color: var(--color-text-light);
vertical-align: middle;
background-color: var(--color-markup-code-inline);
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
box-shadow: inset 0 -1px 0 var(--color-secondary);
}
b,
strong,
h1,

View file

@ -500,16 +500,7 @@
}
.markup kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: var(--color-text-light);
vertical-align: middle;
background-color: var(--color-markup-code-inline);
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius);
box-shadow: inset 0 -1px 0 var(--color-secondary);
padding: 0 0.35em;
}
.markup .ui.list .list,

View file

@ -130,21 +130,42 @@ export function DoUpdateRelativeTime(object, now) {
return HALF_MINUTE;
}
/** Update the displayed text of one relative-time DOM element with its human-readable, localized relative time string. */
function UpdateRelativeTime(object) {
const next = DoUpdateRelativeTime(object);
if (next !== null) setTimeout(() => { UpdateRelativeTime(object) }, next);
}
window.customElements.define('relative-time', class extends HTMLElement {
static observedAttributes = ['datetime'];
/** Update the displayed text of all relative-time DOM elements with their respective human-readable, localized relative time string. */
function UpdateAllRelativeTimes() {
for (const object of document.querySelectorAll('relative-time')) UpdateRelativeTime(object);
}
alive = false;
contentSpan = null;
document.addEventListener('DOMContentLoaded', () => {
UpdateAllRelativeTimes();
// Also update relative-time DOM elements after htmx swap events.
document.body.addEventListener('htmx:afterSwap', () => {
for (const object of document.querySelectorAll('relative-time')) DoUpdateRelativeTime(object);
});
update = (recurring) => {
if (!this.alive) return;
if (!this.shadowRoot) {
this.attachShadow({mode: 'open'});
this.contentSpan = document.createElement('span');
this.shadowRoot.append(this.contentSpan);
}
const next = DoUpdateRelativeTime(this);
if (recurring && next !== null) setTimeout(() => { this.update(true) }, next);
};
connectedCallback() {
this.alive = true;
this.update(true);
}
disconnectedCallback() {
this.alive = false;
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'datetime' && oldValue !== newValue) this.update(false);
}
set textContent(value) {
if (this.contentSpan) this.contentSpan.textContent = value;
}
get textContent() {
return this.contentSpan?.textContent;
}
});

View file

@ -23,6 +23,7 @@ test('CalculateRelativeTimes', () => {
'relativetime.years': ['%d year ago', '%d years ago'],
};
const mock = document.createElement('relative-time');
document.body.append(mock);
const now = Date.parse('2024-10-27T04:05:30+01:00'); // One hour after DST switchover, CET.