mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-07-05 10:00:02 +02:00
Merge branch 'forgejo' into upload_with_path_structure
This commit is contained in:
commit
63c9d504fd
27 changed files with 608 additions and 241 deletions
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
71
.forgejo/workflows/testing-integration.yml
Normal file
71
.forgejo/workflows/testing-integration.yml
Normal 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
|
14
build.go
14
build.go
|
@ -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"
|
||||
)
|
|
@ -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
6
flake.lock
generated
|
@ -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": {
|
||||
|
|
31
flake.nix
31
flake.nix
|
@ -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
4
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
196
package-lock.json
generated
|
@ -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",
|
||||
|
|
10
package.json
10
package.json
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
28
shell.nix
Normal 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
|
||||
];
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue