diff --git a/go.mod b/go.mod index 08886f7b94..fbb744b6e4 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/go-ldap/ldap/v3 v3.4.6 + github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.8.1 github.com/go-swagger/go-swagger v0.31.0 github.com/go-testfixtures/testfixtures/v3 v3.11.0 @@ -219,7 +220,9 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.16 // indirect diff --git a/go.sum b/go.sum index f8e9a458ed..91f075e7ea 100644 --- a/go.sum +++ b/go.sum @@ -358,6 +358,14 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= +github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= @@ -418,6 +426,8 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -475,10 +485,15 @@ github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -755,6 +770,8 @@ github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKc github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= +github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhysd/actionlint v1.7.1 h1:WJaDzyT1StBWVKGSsZPYnbV0HF9Y9/vD6KFdZQL42qE= @@ -856,6 +873,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= diff --git a/modules/globallock/globallock.go b/modules/globallock/globallock.go new file mode 100644 index 0000000000..aa53557729 --- /dev/null +++ b/modules/globallock/globallock.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "sync" +) + +var ( + defaultLocker Locker + initOnce sync.Once + initFunc = func() { + // TODO: read the setting and initialize the default locker. + // Before implementing this, don't use it. + } // define initFunc as a variable to make it possible to change it in tests +) + +// DefaultLocker returns the default locker. +func DefaultLocker() Locker { + initOnce.Do(func() { + initFunc() + }) + return defaultLocker +} + +// Lock tries to acquire a lock for the given key, it uses the default locker. +// Read the documentation of Locker.Lock for more information about the behavior. +func Lock(ctx context.Context, key string) (ReleaseFunc, error) { + return DefaultLocker().Lock(ctx, key) +} + +// TryLock tries to acquire a lock for the given key, it uses the default locker. +// Read the documentation of Locker.TryLock for more information about the behavior. +func TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) { + return DefaultLocker().TryLock(ctx, key) +} + +// LockAndDo tries to acquire a lock for the given key and then calls the given function. +// It uses the default locker, and it will return an error if failed to acquire the lock. +func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error { + release, err := Lock(ctx, key) + if err != nil { + return err + } + defer release() + + return f(ctx) +} + +// TryLockAndDo tries to acquire a lock for the given key and then calls the given function. +// It uses the default locker, and it will return false if failed to acquire the lock. +func TryLockAndDo(ctx context.Context, key string, f func(context.Context) error) (bool, error) { + ok, release, err := TryLock(ctx, key) + if err != nil { + return false, err + } + defer release() + + if !ok { + return false, nil + } + + return true, f(ctx) +} diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go new file mode 100644 index 0000000000..88a555c86f --- /dev/null +++ b/modules/globallock/globallock_test.go @@ -0,0 +1,96 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "os" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLockAndDo(t *testing.T) { + t.Run("redis", func(t *testing.T) { + url := "redis://127.0.0.1:6379/0" + if os.Getenv("CI") == "" { + // Make it possible to run tests against a local redis instance + url = os.Getenv("TEST_REDIS_URL") + if url == "" { + t.Skip("TEST_REDIS_URL not set and not running in CI") + return + } + } + + oldDefaultLocker := defaultLocker + oldInitFunc := initFunc + defer func() { + defaultLocker = oldDefaultLocker + initFunc = oldInitFunc + if defaultLocker == nil { + initOnce = sync.Once{} + } + }() + + initOnce = sync.Once{} + initFunc = func() { + defaultLocker = NewRedisLocker(url) + } + + testLockAndDo(t) + require.NoError(t, defaultLocker.(*redisLocker).Close()) + }) + t.Run("memory", func(t *testing.T) { + oldDefaultLocker := defaultLocker + oldInitFunc := initFunc + defer func() { + defaultLocker = oldDefaultLocker + initFunc = oldInitFunc + if defaultLocker == nil { + initOnce = sync.Once{} + } + }() + + initOnce = sync.Once{} + initFunc = func() { + defaultLocker = NewMemoryLocker() + } + + testLockAndDo(t) + }) +} + +func testLockAndDo(t *testing.T) { + const concurrency = 1000 + + ctx := context.Background() + count := 0 + wg := sync.WaitGroup{} + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + err := LockAndDo(ctx, "test", func(ctx context.Context) error { + count++ + + // It's impossible to acquire the lock inner the function + ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error { + assert.Fail(t, "should not acquire the lock") + return nil + }) + assert.False(t, ok) + assert.NoError(t, err) + + return nil + }) + require.NoError(t, err) + }() + } + + wg.Wait() + + assert.Equal(t, concurrency, count) +} diff --git a/modules/globallock/locker.go b/modules/globallock/locker.go new file mode 100644 index 0000000000..682e24d052 --- /dev/null +++ b/modules/globallock/locker.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" +) + +type Locker interface { + // Lock tries to acquire a lock for the given key, it blocks until the lock is acquired or the context is canceled. + // + // Lock returns a ReleaseFunc to release the lock, it cannot be nil. + // It's always safe to call this function even if it fails to acquire the lock, and it will do nothing in that case. + // And it's also safe to call it multiple times, but it will only release the lock once. + // That's why it's called ReleaseFunc, not UnlockFunc. + // But be aware that it's not safe to not call it at all; it could lead to a memory leak. + // So a recommended pattern is to use defer to call it: + // release, err := locker.Lock(ctx, "key") + // if err != nil { + // return err + // } + // defer release() + // + // Lock returns an error if failed to acquire the lock. + // Be aware that even the context is not canceled, it's still possible to fail to acquire the lock. + // For example, redis is down, or it reached the maximum number of tries. + Lock(ctx context.Context, key string) (ReleaseFunc, error) + + // TryLock tries to acquire a lock for the given key, it returns immediately. + // It follows the same pattern as Lock, but it doesn't block. + // And if it fails to acquire the lock because it's already locked, not other reasons like redis is down, + // it will return false without any error. + TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) +} + +// ReleaseFunc is a function that releases a lock. +type ReleaseFunc func() diff --git a/modules/globallock/locker_test.go b/modules/globallock/locker_test.go new file mode 100644 index 0000000000..bee4d34b34 --- /dev/null +++ b/modules/globallock/locker_test.go @@ -0,0 +1,181 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/go-redsync/redsync/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLocker(t *testing.T) { + t.Run("redis", func(t *testing.T) { + url := "redis://127.0.0.1:6379/0" + if os.Getenv("CI") == "" { + // Make it possible to run tests against a local redis instance + url = os.Getenv("TEST_REDIS_URL") + if url == "" { + t.Skip("TEST_REDIS_URL not set and not running in CI") + return + } + } + oldExpiry := redisLockExpiry + redisLockExpiry = 5 * time.Second // make it shorter for testing + defer func() { + redisLockExpiry = oldExpiry + }() + + locker := NewRedisLocker(url) + testLocker(t, locker) + testRedisLocker(t, locker.(*redisLocker)) + require.NoError(t, locker.(*redisLocker).Close()) + }) + t.Run("memory", func(t *testing.T) { + locker := NewMemoryLocker() + testLocker(t, locker) + testMemoryLocker(t, locker.(*memoryLocker)) + }) +} + +func testLocker(t *testing.T, locker Locker) { + t.Run("lock", func(t *testing.T) { + parentCtx := context.Background() + release, err := locker.Lock(parentCtx, "test") + defer release() + + assert.NoError(t, err) + + func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + release, err := locker.Lock(ctx, "test") + defer release() + + assert.Error(t, err) + }() + + release() + + func() { + release, err := locker.Lock(context.Background(), "test") + defer release() + + assert.NoError(t, err) + }() + }) + + t.Run("try lock", func(t *testing.T) { + parentCtx := context.Background() + ok, release, err := locker.TryLock(parentCtx, "test") + defer release() + + assert.True(t, ok) + assert.NoError(t, err) + + func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + ok, release, err := locker.TryLock(ctx, "test") + defer release() + + assert.False(t, ok) + assert.NoError(t, err) + }() + + release() + + func() { + ok, release, _ := locker.TryLock(context.Background(), "test") + defer release() + + assert.True(t, ok) + }() + }) + + t.Run("wait and acquired", func(t *testing.T) { + ctx := context.Background() + release, err := locker.Lock(ctx, "test") + require.NoError(t, err) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + started := time.Now() + release, err := locker.Lock(context.Background(), "test") // should be blocked for seconds + defer release() + assert.Greater(t, time.Since(started), time.Second) + assert.NoError(t, err) + }() + + time.Sleep(2 * time.Second) + release() + + wg.Wait() + }) + + t.Run("multiple release", func(t *testing.T) { + ctx := context.Background() + + release1, err := locker.Lock(ctx, "test") + require.NoError(t, err) + + release1() + + release2, err := locker.Lock(ctx, "test") + defer release2() + require.NoError(t, err) + + // Call release1 again, + // it should not panic or block, + // and it shouldn't affect the other lock + release1() + + ok, release3, err := locker.TryLock(ctx, "test") + defer release3() + require.NoError(t, err) + // It should be able to acquire the lock; + // otherwise, it means the lock has been released by release1 + assert.False(t, ok) + }) +} + +// testMemoryLocker does specific tests for memoryLocker +func testMemoryLocker(t *testing.T, locker *memoryLocker) { + // nothing to do +} + +// testRedisLocker does specific tests for redisLocker +func testRedisLocker(t *testing.T, locker *redisLocker) { + defer func() { + // This case should be tested at the end. + // Otherwise, it will affect other tests. + t.Run("close", func(t *testing.T) { + assert.NoError(t, locker.Close()) + _, err := locker.Lock(context.Background(), "test") + assert.Error(t, err) + }) + }() + + t.Run("failed extend", func(t *testing.T) { + release, err := locker.Lock(context.Background(), "test") + defer release() + require.NoError(t, err) + + // It simulates that there are some problems with extending like network issues or redis server down. + v, ok := locker.mutexM.Load("test") + require.True(t, ok) + m := v.(*redsync.Mutex) + _, _ = m.Unlock() // release it to make it impossible to extend + + // In current design, callers can't know the lock can't be extended. + // Just keep this case to improve the test coverage. + }) +} diff --git a/modules/globallock/memory_locker.go b/modules/globallock/memory_locker.go new file mode 100644 index 0000000000..3f818d8d43 --- /dev/null +++ b/modules/globallock/memory_locker.go @@ -0,0 +1,67 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "sync" + "time" +) + +type memoryLocker struct { + locks sync.Map +} + +var _ Locker = &memoryLocker{} + +func NewMemoryLocker() Locker { + return &memoryLocker{} +} + +func (l *memoryLocker) Lock(ctx context.Context, key string) (ReleaseFunc, error) { + if l.tryLock(key) { + releaseOnce := sync.Once{} + return func() { + releaseOnce.Do(func() { + l.locks.Delete(key) + }) + }, nil + } + + ticker := time.NewTicker(time.Millisecond * 100) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return func() {}, ctx.Err() + case <-ticker.C: + if l.tryLock(key) { + releaseOnce := sync.Once{} + return func() { + releaseOnce.Do(func() { + l.locks.Delete(key) + }) + }, nil + } + } + } +} + +func (l *memoryLocker) TryLock(_ context.Context, key string) (bool, ReleaseFunc, error) { + if l.tryLock(key) { + releaseOnce := sync.Once{} + return true, func() { + releaseOnce.Do(func() { + l.locks.Delete(key) + }) + }, nil + } + + return false, func() {}, nil +} + +func (l *memoryLocker) tryLock(key string) bool { + _, loaded := l.locks.LoadOrStore(key, struct{}{}) + return !loaded +} diff --git a/modules/globallock/redis_locker.go b/modules/globallock/redis_locker.go new file mode 100644 index 0000000000..34ed9e389b --- /dev/null +++ b/modules/globallock/redis_locker.go @@ -0,0 +1,137 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/nosql" + + "github.com/go-redsync/redsync/v4" + "github.com/go-redsync/redsync/v4/redis/goredis/v9" +) + +const redisLockKeyPrefix = "gitea:globallock:" + +// redisLockExpiry is the default expiry time for a lock. +// Define it as a variable to make it possible to change it in tests. +var redisLockExpiry = 30 * time.Second + +type redisLocker struct { + rs *redsync.Redsync + + mutexM sync.Map + closed atomic.Bool + extendWg sync.WaitGroup +} + +var _ Locker = &redisLocker{} + +func NewRedisLocker(connection string) Locker { + l := &redisLocker{ + rs: redsync.New( + goredis.NewPool( + nosql.GetManager().GetRedisClient(connection), + ), + ), + } + + l.extendWg.Add(1) + l.startExtend() + + return l +} + +func (l *redisLocker) Lock(ctx context.Context, key string) (ReleaseFunc, error) { + return l.lock(ctx, key, 0) +} + +func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) { + f, err := l.lock(ctx, key, 1) + + var ( + errTaken *redsync.ErrTaken + errNodeTaken *redsync.ErrNodeTaken + ) + if errors.As(err, &errTaken) || errors.As(err, &errNodeTaken) { + return false, f, nil + } + return err == nil, f, err +} + +// Close closes the locker. +// It will stop extending the locks and refuse to acquire new locks. +// In actual use, it is not necessary to call this function. +// But it's useful in tests to release resources. +// It could take some time since it waits for the extending goroutine to finish. +func (l *redisLocker) Close() error { + l.closed.Store(true) + l.extendWg.Wait() + return nil +} + +func (l *redisLocker) lock(ctx context.Context, key string, tries int) (ReleaseFunc, error) { + if l.closed.Load() { + return func() {}, fmt.Errorf("locker is closed") + } + + options := []redsync.Option{ + redsync.WithExpiry(redisLockExpiry), + } + if tries > 0 { + options = append(options, redsync.WithTries(tries)) + } + mutex := l.rs.NewMutex(redisLockKeyPrefix+key, options...) + if err := mutex.LockContext(ctx); err != nil { + return func() {}, err + } + + l.mutexM.Store(key, mutex) + + releaseOnce := sync.Once{} + return func() { + releaseOnce.Do(func() { + l.mutexM.Delete(key) + + // It's safe to ignore the error here, + // if it failed to unlock, it will be released automatically after the lock expires. + // Do not call mutex.UnlockContext(ctx) here, or it will fail to release when ctx has timed out. + _, _ = mutex.Unlock() + }) + }, nil +} + +func (l *redisLocker) startExtend() { + if l.closed.Load() { + l.extendWg.Done() + return + } + + toExtend := make([]*redsync.Mutex, 0) + l.mutexM.Range(func(_, value any) bool { + m := value.(*redsync.Mutex) + + // Extend the lock if it is not expired. + // Although the mutex will be removed from the map before it is released, + // it still can be expired because of a failed extension. + // If it happens, it does not need to be extended anymore. + if time.Now().After(m.Until()) { + return true + } + + toExtend = append(toExtend, m) + return true + }) + for _, v := range toExtend { + // If it failed to extend, it will be released automatically after the lock expires. + _, _ = v.Extend() + } + + time.AfterFunc(redisLockExpiry/2, l.startExtend) +} diff --git a/options/license/DocBook-Schema b/options/license/DocBook-Schema new file mode 100644 index 0000000000..56203a0878 --- /dev/null +++ b/options/license/DocBook-Schema @@ -0,0 +1,22 @@ +Copyright 1992-2011 HaL Computer Systems, Inc., +O'Reilly & Associates, Inc., ArborText, Inc., Fujitsu Software +Corporation, Norman Walsh, Sun Microsystems, Inc., and the +Organization for the Advancement of Structured Information +Standards (OASIS). + +Permission to use, copy, modify and distribute the DocBook schema +and its accompanying documentation for any purpose and without fee +is hereby granted in perpetuity, provided that the above copyright +notice and this paragraph appear in all copies. The copyright +holders make no representation about the suitability of the schema +for any purpose. It is provided "as is" without expressed or implied +warranty. + +If you modify the DocBook schema in any way, label your schema as a +variant of DocBook. See the reference documentation +(http://docbook.org/tdg5/en/html/ch05.html#s-notdocbook) +for more information. + +Please direct all questions, bug reports, or suggestions for changes +to the docbook@lists.oasis-open.org mailing list. For more +information, see http://www.oasis-open.org/docbook/. diff --git a/options/license/DocBook-XML b/options/license/DocBook-XML new file mode 100644 index 0000000000..9553feee6b --- /dev/null +++ b/options/license/DocBook-XML @@ -0,0 +1,48 @@ +Copyright +--------- +Copyright (C) 1999-2007 Norman Walsh +Copyright (C) 2003 Jiří Kosek +Copyright (C) 2004-2007 Steve Ball +Copyright (C) 2005-2014 The DocBook Project +Copyright (C) 2011-2012 O'Reilly Media + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the ``Software''), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +Except as contained in this notice, the names of individuals +credited with contribution to this software shall not be used in +advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization +from the individuals in question. + +Any stylesheet derived from this Software that is publically +distributed will be identified with a different name and the +version strings in any derived Software will be changed so that +no possibility of confusion between the derived package and this +Software will exist. + +Warranty +-------- +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL NORMAN WALSH OR ANY OTHER +CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Contacting the Author +--------------------- +The DocBook XSL stylesheets are maintained by Norman Walsh, +, and members of the DocBook Project, + diff --git a/options/license/Ubuntu-font-1.0 b/options/license/Ubuntu-font-1.0 new file mode 100644 index 0000000000..ae78a8f94e --- /dev/null +++ b/options/license/Ubuntu-font-1.0 @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 3a6dadf9f8..d8897735c2 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -206,7 +206,7 @@ buttons.list.unordered.tooltip=添加待办清单 buttons.list.ordered.tooltip=添加编号列表 buttons.list.task.tooltip=添加任务列表 buttons.mention.tooltip=提及用户或团队 -buttons.ref.tooltip=引用一个问题或拉取请求 +buttons.ref.tooltip=引用一个问题或合并请求 buttons.switch_to_legacy.tooltip=使用旧版编辑器 buttons.enable_monospace_font=启用等宽字体 buttons.disable_monospace_font=禁用等宽字体 @@ -1752,8 +1752,9 @@ compare.compare_head=比较 pulls.desc=启用合并请求和代码评审。 pulls.new=创建合并请求 pulls.new.blocked_user=无法创建合并请求,因为您已被仓库所有者屏蔽。 +pulls.new.must_collaborator=您必须是仓库的协作者才能创建合并请求。 pulls.edit.already_changed=无法保存对合并请求的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改 -pulls.view=查看拉取请求 +pulls.view=查看合并请求 pulls.compare_changes=创建合并请求 pulls.allow_edits_from_maintainers=允许维护者编辑 pulls.allow_edits_from_maintainers_desc=对基础分支有写入权限的用户也可以推送到此分支 @@ -1830,8 +1831,8 @@ pulls.wrong_commit_id=提交 id 必须在目标分支 上 pulls.no_merge_desc=由于未启用合并选项,此合并请求无法被合并。 pulls.no_merge_helper=在仓库设置中启用合并选项或者手工合并请求。 pulls.no_merge_wip=这个合并请求无法合并,因为被标记为尚未完成的工作。 -pulls.no_merge_not_ready=此拉取请求尚未准备好合并,请检查审核状态和状态检查。 -pulls.no_merge_access=您无权合并此拉取请求。 +pulls.no_merge_not_ready=此合并请求尚未准备好合并,请检查审核状态和状态检查。 +pulls.no_merge_access=您无权合并此合并请求。 pulls.merge_pull_request=创建合并提交 pulls.rebase_merge_pull_request=变基后快进 pulls.rebase_merge_commit_pull_request=变基后创建合并提交 @@ -1876,6 +1877,7 @@ pulls.cmd_instruction_checkout_title=检出 pulls.cmd_instruction_checkout_desc=从你的仓库中检出一个新的分支并测试变更。 pulls.cmd_instruction_merge_title=合并 pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上 +pulls.cmd_instruction_merge_warning=警告:此操作不能合并该合并请求,因为“自动检测手动合并”未启用 pulls.clear_merge_message=清除合并信息 pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 git 附加内容,如“Co-Authored-By …”。 @@ -1888,11 +1890,11 @@ pulls.auto_merge_cancel_schedule=取消自动合并 pulls.auto_merge_not_scheduled=此合并请求没有计划自动合并。 pulls.auto_merge_canceled_schedule=此合并请求的自动合并已取消。 -pulls.auto_merge_newly_scheduled_comment=`已于 %[1]s 设置此拉取请求在所有检查成功后自动合并` +pulls.auto_merge_newly_scheduled_comment=`已于 %[1]s 设置此合并请求在所有检查成功后自动合并` pulls.auto_merge_canceled_schedule_comment=`已于 %[1]s 取消了自动合并设置 ` -pulls.delete.title=删除此拉取请求? -pulls.delete.text=你真的要删除这个拉取请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) +pulls.delete.title=删除此合并请求? +pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 %[1]s @@ -2125,7 +2127,7 @@ settings.allow_only_contributors_to_track_time=仅允许成员跟踪时间 settings.pulls_desc=启用合并请求 settings.pulls.ignore_whitespace=忽略空白冲突 settings.pulls.enable_autodetect_manual_merge=启用自动检测手动合并 (注意:在某些特殊情况下可能发生错误判断) -settings.pulls.allow_rebase_update=允许通过变基更新拉取请求分支 +settings.pulls.allow_rebase_update=允许通过变基更新合并请求分支 settings.pulls.default_delete_branch_after_merge=默认合并后删除合并请求分支 settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者编辑 settings.releases_desc=启用发布 @@ -2375,7 +2377,7 @@ settings.protect_status_check_matched=匹配 settings.protect_invalid_status_check_pattern=无效的状态检查规则:“%s”。 settings.protect_no_valid_status_check_patterns=没有有效的状态检查规则。 settings.protect_required_approvals=所需的批准: -settings.protect_required_approvals_desc=只允许合并有足够审核人数的拉取请求。 +settings.protect_required_approvals_desc=只允许合并有足够审核人数的合并请求。 settings.dismiss_stale_approvals=取消过时的批准 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。 settings.ignore_stale_approvals=忽略过期批准 @@ -2400,7 +2402,7 @@ settings.block_rejected_reviews=拒绝审核阻止了此合并 settings.block_rejected_reviews_desc=如果官方审查人员要求作出改动,即使有足够的批准,合并也不允许。 settings.block_on_official_review_requests=有官方审核阻止了代码合并 settings.block_on_official_review_requests_desc=处于评审状态时,即使有足够的批准,也不能合并。 -settings.block_outdated_branch=如果拉取请求已经过时,阻止合并 +settings.block_outdated_branch=如果合并请求已经过时,阻止合并 settings.block_outdated_branch_desc=当头部分支落后基础分支时,不能合并。 settings.default_branch_desc=请选择一个默认的分支用于合并请求和提交: settings.merge_style_desc=合并方式 diff --git a/package-lock.json b/package-lock.json index eb3ba5fb48..737dbd12b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "jquery": "3.7.1", "katex": "0.16.11", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "10.9.1", + "mermaid": "11.0.2", "mini-css-extract-plugin": "2.9.0", "minimatch": "10.0.1", "monaco-editor": "0.50.0", @@ -318,11 +318,50 @@ } }, "node_modules/@braintree/sanitize-url": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", - "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz", + "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==", "license": "MIT" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, "node_modules/@citation-js/core": { "version": "0.7.14", "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz", @@ -1464,6 +1503,15 @@ "@mcaptcha/core-glue": "^0.1.0-alpha-5" } }, + "node_modules/@mermaid-js/parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.2.0.tgz", + "integrity": "sha512-33dyFdhwsX9n4+E8SRj1ulxwAgwCj9RyCMtoqXD5cDfS9F6y9xmvmjFjHoPaViH4H7I7BXD8yP/XEWig5XrHSQ==", + "license": "MIT", + "dependencies": { + "langium": "3.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2445,36 +2493,6 @@ "@types/tern": "*" } }, - "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", - "license": "MIT" - }, - "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", - "license": "MIT" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, "node_modules/@types/dropzone": { "version": "5.7.8", "resolved": "https://registry.npmjs.org/@types/dropzone/-/dropzone-5.7.8.tgz", @@ -2567,21 +2585,6 @@ "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", "license": "MIT" }, - "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", @@ -2687,12 +2690,6 @@ "source-map": "^0.6.1" } }, - "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", - "license": "MIT" - }, "node_modules/@types/urijs": { "version": "1.19.25", "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", @@ -4028,16 +4025,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/chart.js": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", @@ -4085,6 +4072,32 @@ "node": ">= 16" } }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5038,6 +5051,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -5051,19 +5065,6 @@ } } }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -5138,6 +5139,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5149,15 +5151,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5299,12 +5292,6 @@ "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", "license": "ISC" }, - "node_modules/elkjs": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", - "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", - "license": "EPL-2.0" - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7493,6 +7480,12 @@ "dev": true, "license": "MIT" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hammerjs": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -8248,15 +8241,6 @@ "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/known-css-properties": { "version": "0.34.0", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", @@ -8264,6 +8248,22 @@ "dev": true, "license": "MIT" }, + "node_modules/langium": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz", + "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -8724,43 +8724,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -8804,480 +8767,46 @@ } }, "node_modules/mermaid": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz", - "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.0.2.tgz", + "integrity": "sha512-KFM1o560odBHvXTTSx47ne/SE4aJKb2GbysHAVdQafIJtB6O3c0K4F+v3nC+zqS6CJhk7sXaagectNrTG+ARDw==", "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^6.0.1", - "@types/d3-scale": "^4.0.3", - "@types/d3-scale-chromatic": "^3.0.0", - "cytoscape": "^3.28.1", + "@braintree/sanitize-url": "^7.0.1", + "@mermaid-js/parser": "^0.2.0", + "cytoscape": "^3.29.2", "cytoscape-cose-bilkent": "^4.1.0", - "d3": "^7.4.0", + "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.10", - "dayjs": "^1.11.7", - "dompurify": "^3.0.5", - "elkjs": "^0.9.0", + "dayjs": "^1.11.10", + "dompurify": "^3.0.11", "katex": "^0.16.9", - "khroma": "^2.0.0", + "khroma": "^2.1.0", "lodash-es": "^4.17.21", - "mdast-util-from-markdown": "^1.3.0", - "non-layered-tidy-tree-layout": "^2.0.2", - "stylis": "^4.1.3", + "marked": "^13.0.2", + "roughjs": "^4.6.6", + "stylis": "^4.3.1", "ts-dedent": "^2.2.0", - "uuid": "^9.0.0", - "web-worker": "^1.2.0" + "uuid": "^9.0.1" } }, - "node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mermaid/node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" } }, - "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "license": "MIT", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -9409,19 +8938,11 @@ "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", "license": "BSD-3-Clause" }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -9550,12 +9071,6 @@ "node": ">=12.4.0" } }, - "node_modules/non-layered-tidy-tree-layout": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", - "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", - "license": "MIT" - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -9841,6 +9356,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10065,6 +9586,22 @@ "node": ">=4" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/pony-cause": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", @@ -10879,6 +10416,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-con": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz", @@ -10924,18 +10473,6 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12349,19 +11886,6 @@ "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", "license": "MIT" }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -12460,24 +11984,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -12724,6 +12230,55 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, "node_modules/vue": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.35.tgz", @@ -12855,12 +12410,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-worker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", - "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", - "license": "Apache-2.0" - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 7d9e857e90..9d3556475e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "jquery": "3.7.1", "katex": "0.16.11", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "10.9.1", + "mermaid": "11.0.2", "mini-css-extract-plugin": "2.9.0", "minimatch": "10.0.1", "monaco-editor": "0.50.0", diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 77d49f5b78..366a7b20de 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" @@ -42,6 +41,14 @@ func Home(ctx *context.Context) { return } + home(ctx, false) +} + +func Repositories(ctx *context.Context) { + home(ctx, true) +} + +func home(ctx *context.Context, viewRepositories bool) { org := ctx.Org.Organization ctx.Data["PageIsUserProfile"] = true @@ -101,10 +108,34 @@ func Home(ctx *context.Context) { private := ctx.FormOptionalBool("private") ctx.Data["IsPrivate"] = private + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + opts := &organization.FindOrgMembersOpts{ + OrgID: org.ID, + PublicOnly: ctx.Org.PublicMemberOnly, + ListOptions: db.ListOptions{Page: 1, PageSize: 25}, + } + members, _, err := organization.FindOrgMembers(ctx, opts) + if err != nil { + ctx.ServerError("FindOrgMembers", err) + return + } + ctx.Data["Members"] = members + ctx.Data["Teams"] = ctx.Org.Teams + ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull + ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 + + if !prepareOrgProfileReadme(ctx, viewRepositories) { + ctx.Data["PageIsViewRepositories"] = true + } + var ( repos []*repo_model.Repository count int64 - err error ) repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -129,29 +160,8 @@ func Home(ctx *context.Context) { return } - opts := &organization.FindOrgMembersOpts{ - OrgID: org.ID, - PublicOnly: ctx.Org.PublicMemberOnly, - ListOptions: db.ListOptions{Page: 1, PageSize: 25}, - } - members, _, err := organization.FindOrgMembers(ctx, opts) - if err != nil { - ctx.ServerError("FindOrgMembers", err) - return - } - ctx.Data["Repos"] = repos ctx.Data["Total"] = count - ctx.Data["Members"] = members - ctx.Data["Teams"] = ctx.Org.Teams - ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull - ctx.Data["PageIsViewRepositories"] = true - - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) pager.SetDefaultParams(ctx) @@ -173,18 +183,16 @@ func Home(ctx *context.Context) { } ctx.Data["Page"] = pager - ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 - - profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - prepareOrgProfileReadme(ctx, profileGitRepo, profileDbRepo, profileReadmeBlob) - ctx.HTML(http.StatusOK, tplOrgHome) } -func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repository, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) { - if profileGitRepo == nil || profileReadme == nil { - return +func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool { + profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) + defer profileClose() + ctx.Data["HasProfileReadme"] = profileReadme != nil + + if profileGitRepo == nil || profileReadme == nil || viewRepositories { + return false } if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { @@ -206,4 +214,7 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor ctx.Data["ProfileReadme"] = profileContent } } + + ctx.Data["PageIsViewOverview"] = true + return true } diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 58d0b9b8c4..8ff75b0651 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,9 @@ func Members(ctx *context.Context) { return } - err = shared_user.LoadHeaderCount(ctx) + err = shared_user.RenderOrgHeader(ctx) if err != nil { - ctx.ServerError("LoadHeaderCount", err) + ctx.ServerError("RenderOrgHeader", err) return } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index aaac1177ae..31b9601ce7 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -59,9 +59,9 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - err := shared_user.LoadHeaderCount(ctx) + err := shared_user.RenderOrgHeader(ctx) if err != nil { - ctx.ServerError("LoadHeaderCount", err) + ctx.ServerError("RenderOrgHeader", err) return } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 7531e1ba26..ef111cff80 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -162,3 +162,15 @@ func LoadHeaderCount(ctx *context.Context) error { return nil } + +func RenderOrgHeader(ctx *context.Context) error { + if err := LoadHeaderCount(ctx); err != nil { + return err + } + + _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) + defer profileClose() + ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil + + return nil +} diff --git a/routers/web/web.go b/routers/web/web.go index 4e917b5ede..98b4252cb9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -995,6 +995,8 @@ func registerRoutes(m *web.Router) { }, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) } + m.Get("/repositories", org.Repositories) + m.Group("/projects", func() { m.Group("", func() { m.Get("", org.Projects) diff --git a/services/pull/merge.go b/services/pull/merge.go index e19292c31c..eb67e06946 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -219,6 +219,10 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U // Reset cached commit count cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) + return handleCloseCrossReferences(ctx, pr, doer) +} + +func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) error { // Resolve cross references refs, err := pr.ResolveCrossReferences(ctx) if err != nil { @@ -542,5 +546,6 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use notify_service.MergePullRequest(baseGitRepo.Ctx, doer, pr) log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commitID) - return nil + + return handleCloseCrossReferences(ctx, pr, doer) } diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 698a9559c5..29238f8d6b 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -1,7 +1,12 @@
- + {{if .HasProfileReadme}} + + {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} + + {{end}} + {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} {{if .RepoCount}}
{{.RepoCount}}
diff --git a/types.d.ts b/types.d.ts index a8dc09e064..68081af606 100644 --- a/types.d.ts +++ b/types.d.ts @@ -10,22 +10,52 @@ declare module '*.css' { declare let __webpack_public_path__: string; -interface Window { - config: import('./web_src/js/types.ts').Config; - $: typeof import('@types/jquery'), - jQuery: typeof import('@types/jquery'), - htmx: typeof import('htmx.org'), - _globalHandlerErrors: Array & { - _inited: boolean, - push: (e: ErrorEvent & PromiseRejectionEvent) => void | number, - }, -} - declare module 'htmx.org/dist/htmx.esm.js' { const value = await import('htmx.org'); export default value; } +declare module 'uint8-to-base64' { + export function encode(arrayBuffer: ArrayBuffer): string; + export function decode(base64str: string): ArrayBuffer; +} + +declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { + const value = await import('swagger-ui-dist'); + export default value.SwaggerUIBundle; +} + +interface JQuery { + api: any, // fomantic + areYouSure: any, // jquery.are-you-sure + dimmer: any, // fomantic + dropdown: any; // fomantic + modal: any; // fomantic + tab: any; // fomantic + transition: any, // fomantic +} + +interface JQueryStatic { + api: any, // fomantic +} + interface Element { _tippy: import('tippy.js').Instance; } + +type Writable = { -readonly [K in keyof T]: T[K] }; + +interface Window { + config: import('./web_src/js/types.ts').Config; + $: typeof import('@types/jquery'), + jQuery: typeof import('@types/jquery'), + htmx: Omit & { + config?: Writable, + }, + ui?: any, + _globalHandlerErrors: Array & { + _inited: boolean, + push: (e: ErrorEvent & PromiseRejectionEvent) => void | number, + }, + __webpack_public_path__: string; +} diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts index bfc2147736..d4f317ee5a 100644 --- a/web_src/js/htmx.ts +++ b/web_src/js/htmx.ts @@ -1,20 +1,21 @@ import {showErrorToast} from './modules/toast.ts'; +import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/idiomorph#htmx +import type {HtmxResponseInfo} from 'htmx.org'; -// https://github.com/bigskysoftware/idiomorph#htmx -import 'idiomorph/dist/idiomorph-ext.js'; +type HtmxEvent = Event & {detail: HtmxResponseInfo}; // https://htmx.org/reference/#config window.htmx.config.requestClass = 'is-loading'; window.htmx.config.scrollIntoViewOnBoost = false; // https://htmx.org/events/#htmx:sendError -document.body.addEventListener('htmx:sendError', (event) => { +document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => { // TODO: add translations showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`); }); // https://htmx.org/events/#htmx:responseError -document.body.addEventListener('htmx:responseError', (event) => { +document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => { // TODO: add translations showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); }); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2bdc8655fe..db678a25ba 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -98,12 +98,12 @@ initGiteaFomantic(); initDirAuto(); initSubmitEventPolyfill(); -function callInitFunctions(functions) { +function callInitFunctions(functions: (() => any)[]) { // Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1" // It is a quick check, no side effect so no need to do slow URL parsing. const initStart = performance.now(); if (window.location.search.includes('_ui_performance_trace=1')) { - let results = []; + let results: {name: string, dur: number}[] = []; for (const func of functions) { const start = performance.now(); func(); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index da07161ed1..f2754e659b 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -20,6 +20,7 @@ export async function renderMermaid() { startOnLoad: false, theme: isDarkTheme() ? 'dark' : 'neutral', securityLevel: 'strict', + suppressErrorRendering: true, }); for (const el of els) { diff --git a/web_src/js/render/ansi.ts b/web_src/js/render/ansi.ts index bb622dd1eb..685e916c9a 100644 --- a/web_src/js/render/ansi.ts +++ b/web_src/js/render/ansi.ts @@ -1,12 +1,12 @@ import {AnsiUp} from 'ansi_up'; -const replacements = [ +const replacements: Array<[RegExp, string]> = [ [/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op [/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return ]; // render ANSI to HTML -export function renderAnsi(line) { +export function renderAnsi(line: string): string { // create a fresh ansi_up instance because otherwise previous renders can influence // the output of future renders, because ansi_up is stateful and remembers things like // unclosed opening tags for colors. diff --git a/web_src/js/standalone/swagger.ts b/web_src/js/standalone/swagger.ts index 2928813167..63b676b2ea 100644 --- a/web_src/js/standalone/swagger.ts +++ b/web_src/js/standalone/swagger.ts @@ -8,7 +8,7 @@ window.addEventListener('load', async () => { // Make the page's protocol be at the top of the schemes list const proto = window.location.protocol.slice(0, -1); - spec.schemes.sort((a, b) => { + spec.schemes.sort((a: string, b: string) => { if (a === proto) return -1; if (b === proto) return 1; return 0; diff --git a/web_src/js/svg.test.ts b/web_src/js/svg.test.ts index 015758a271..7f3e0496ec 100644 --- a/web_src/js/svg.test.ts +++ b/web_src/js/svg.test.ts @@ -17,7 +17,7 @@ test('svgParseOuterInner', () => { test('SvgIcon', () => { const root = document.createElement('div'); createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root); - const node = root.firstChild; + const node = root.firstChild as Element; expect(node.nodeName).toEqual('svg'); expect(node.getAttribute('width')).toEqual('24'); expect(node.getAttribute('height')).toEqual('24'); diff --git a/web_src/js/types.ts b/web_src/js/types.ts index 3bd1c072a8..f3ac305162 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -29,3 +29,10 @@ export type RequestData = string | FormData | URLSearchParams; export type RequestOpts = { data?: RequestData, } & RequestInit; + +export type IssueData = { + owner: string, + repo: string, + type: string, + index: string, +} diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index 4c09f49ba8..55896706ff 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -95,23 +95,20 @@ test('toAbsoluteUrl', () => { }); test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { - // TextEncoder is Node.js API while Uint8Array is jsdom API and their outputs are not - // structurally comparable, so we convert to array to compare. The conversion can be - // removed once https://github.com/jsdom/jsdom/issues/2524 is resolved. const encoder = new TextEncoder(); const uint8array = encoder.encode.bind(encoder); expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/" expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+" - expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?'))); - expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~'))); - expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?'))); - expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~'))); + expect(new Uint8Array(decodeURLEncodedBase64('QUE/'))).toEqual(uint8array('AA?')); + expect(new Uint8Array(decodeURLEncodedBase64('QUF+'))).toEqual(uint8array('AA~')); + expect(new Uint8Array(decodeURLEncodedBase64('QUE_'))).toEqual(uint8array('AA?')); + expect(new Uint8Array(decodeURLEncodedBase64('QUF-'))).toEqual(uint8array('AA~')); expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ==" - expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a'))); - expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a'))); + expect(new Uint8Array(decodeURLEncodedBase64('YQ'))).toEqual(uint8array('a')); + expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a')); }); test('file detection', () => { diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index 2d40fa20a8..c52bf500d4 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -1,13 +1,14 @@ import {encode, decode} from 'uint8-to-base64'; +import type {IssueData} from './types.ts'; // transform /path/to/file.ext to file.ext -export function basename(path) { +export function basename(path: string): string { const lastSlashIndex = path.lastIndexOf('/'); return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1); } // transform /path/to/file.ext to .ext -export function extname(path) { +export function extname(path: string): string { const lastSlashIndex = path.lastIndexOf('/'); const lastPointIndex = path.lastIndexOf('.'); if (lastSlashIndex > lastPointIndex) return ''; @@ -15,54 +16,54 @@ export function extname(path) { } // test whether a variable is an object -export function isObject(obj) { +export function isObject(obj: any): boolean { return Object.prototype.toString.call(obj) === '[object Object]'; } // returns whether a dark theme is enabled -export function isDarkTheme() { +export function isDarkTheme(): boolean { const style = window.getComputedStyle(document.documentElement); return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true'; } // strip from a string -export function stripTags(text) { +export function stripTags(text: string): string { return text.replace(/<[^>]*>?/g, ''); } -export function parseIssueHref(href) { +export function parseIssueHref(href: string): IssueData { const path = (href || '').replace(/[#?].*$/, ''); const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; return {owner, repo, type, index}; } // parse a URL, either relative '/path' or absolute 'https://localhost/path' -export function parseUrl(str) { +export function parseUrl(str: string): URL { return new URL(str, str.startsWith('http') ? undefined : window.location.origin); } // return current locale chosen by user -export function getCurrentLocale() { +export function getCurrentLocale(): string { return document.documentElement.lang; } // given a month (0-11), returns it in the documents language -export function translateMonth(month) { +export function translateMonth(month: number) { return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'}); } // given a weekday (0-6, Sunday to Saturday), returns it in the documents language -export function translateDay(day) { +export function translateDay(day: number) { return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'}); } // convert a Blob to a DataURI -export function blobToDataURI(blob) { +export function blobToDataURI(blob: Blob): Promise { return new Promise((resolve, reject) => { try { const reader = new FileReader(); reader.addEventListener('load', (e) => { - resolve(e.target.result); + resolve(e.target.result as string); }); reader.addEventListener('error', () => { reject(new Error('FileReader failed')); @@ -75,7 +76,7 @@ export function blobToDataURI(blob) { } // convert image Blob to another mime-type format. -export function convertImage(blob, mime) { +export function convertImage(blob: Blob, mime: string): Promise { return new Promise(async (resolve, reject) => { try { const img = new Image(); @@ -104,7 +105,7 @@ export function convertImage(blob, mime) { }); } -export function toAbsoluteUrl(url) { +export function toAbsoluteUrl(url: string): string { if (url.startsWith('http://') || url.startsWith('https://')) { return url; } @@ -118,15 +119,15 @@ export function toAbsoluteUrl(url) { } // Encode an ArrayBuffer into a URLEncoded base64 string. -export function encodeURLEncodedBase64(arrayBuffer) { +export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string { return encode(arrayBuffer) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } -// Decode a URLEncoded base64 to an ArrayBuffer string. -export function decodeURLEncodedBase64(base64url) { +// Decode a URLEncoded base64 to an ArrayBuffer. +export function decodeURLEncodedBase64(base64url: string): ArrayBuffer { return decode(base64url .replace(/_/g, '/') .replace(/-/g, '+')); @@ -135,20 +136,22 @@ export function decodeURLEncodedBase64(base64url) { const domParser = new DOMParser(); const xmlSerializer = new XMLSerializer(); -export function parseDom(text, contentType) { +export function parseDom(text: string, contentType: DOMParserSupportedType): Document { return domParser.parseFromString(text, contentType); } -export function serializeXml(node) { +export function serializeXml(node: Element | Node): string { return xmlSerializer.serializeToString(node); } -export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} -export function isImageFile({name, type}) { +export function isImageFile({name, type}: {name: string, type?: string}): boolean { return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/'); } -export function isVideoFile({name, type}) { +export function isVideoFile({name, type}: {name: string, type?: string}): boolean { return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); } diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts index 3ee32395fb..a0409353d2 100644 --- a/web_src/js/utils/color.ts +++ b/web_src/js/utils/color.ts @@ -3,23 +3,23 @@ import type {ColorInput} from 'tinycolor2'; // Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance // Keep this in sync with modules/util/color.go -function getRelativeLuminance(color: ColorInput) { +function getRelativeLuminance(color: ColorInput): number { const {r, g, b} = tinycolor(color).toRgb(); return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -function useLightText(backgroundColor: ColorInput) { +function useLightText(backgroundColor: ColorInput): boolean { return getRelativeLuminance(backgroundColor) < 0.453; } // Given a background color, returns a black or white foreground color that the highest // contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better. // https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 -export function contrastColor(backgroundColor: ColorInput) { +export function contrastColor(backgroundColor: ColorInput): string { return useLightText(backgroundColor) ? '#fff' : '#000'; } -function resolveColors(obj: Record) { +function resolveColors(obj: Record): Record { const styles = window.getComputedStyle(document.documentElement); const getColor = (name: string) => styles.getPropertyValue(name).trim(); return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 5fc2183194..7dd63ecbbf 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -266,10 +266,8 @@ export function initSubmitEventPolyfill() { /** * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. * Note: This function doesn't account for all possible visibility scenarios. - * @param {HTMLElement} element The element to check. - * @returns {boolean} True if the element is visible. */ -export function isElemVisible(element: HTMLElement) { +export function isElemVisible(element: HTMLElement): boolean { if (!element) return false; return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); diff --git a/web_src/js/utils/image.ts b/web_src/js/utils/image.ts index c71d715941..558a63f22e 100644 --- a/web_src/js/utils/image.ts +++ b/web_src/js/utils/image.ts @@ -1,6 +1,11 @@ -export async function pngChunks(blob) { +type PngChunk = { + name: string, + data: Uint8Array, +} + +export async function pngChunks(blob: Blob): Promise { const uint8arr = new Uint8Array(await blob.arrayBuffer()); - const chunks = []; + const chunks: PngChunk[] = []; if (uint8arr.length < 12) return chunks; const view = new DataView(uint8arr.buffer); if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; @@ -19,9 +24,14 @@ export async function pngChunks(blob) { return chunks; } +type ImageInfo = { + width?: number, + dppx?: number, +} + // decode a image and try to obtain width and dppx. It will never throw but instead // return default values. -export async function imageInfo(blob) { +export async function imageInfo(blob: Blob): Promise { let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens if (blob.type === 'image/png') { // only png is supported currently diff --git a/web_src/js/utils/match.ts b/web_src/js/utils/match.ts index 17fdfed113..0ce7e2b1a2 100644 --- a/web_src/js/utils/match.ts +++ b/web_src/js/utils/match.ts @@ -2,17 +2,17 @@ import emojis from '../../../assets/emoji.json'; const maxMatches = 6; -function sortAndReduce(map) { +function sortAndReduce(map: Map) { const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1])); return Array.from(sortedMap.keys()).slice(0, maxMatches); } -export function matchEmoji(queryText) { +export function matchEmoji(queryText: string): string[] { const query = queryText.toLowerCase().replaceAll('_', ' '); if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]); // results is a map of weights, lower is better - const results = new Map(); + const results = new Map(); for (const {aliases} of emojis) { const mainAlias = aliases[0]; for (const [aliasIndex, alias] of aliases.entries()) { @@ -27,7 +27,7 @@ export function matchEmoji(queryText) { return sortAndReduce(results); } -export function matchMention(queryText) { +export function matchMention(queryText: string): string[] { const query = queryText.toLowerCase(); // results is a map of weights, lower is better diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts index d3a986e736..5251386230 100644 --- a/web_src/js/utils/time.ts +++ b/web_src/js/utils/time.ts @@ -1,16 +1,17 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc.js'; import {getCurrentLocale} from '../utils.ts'; +import type {ConfigType} from 'dayjs'; dayjs.extend(utc); /** * Returns an array of millisecond-timestamps of start-of-week days (Sundays) * - * @param startConfig The start date. Can take any type that `Date` accepts. - * @param endConfig The end date. Can take any type that `Date` accepts. + * @param startDate The start date. Can take any type that dayjs accepts. + * @param endDate The end date. Can take any type that dayjs accepts. */ -export function startDaysBetween(startDate, endDate) { +export function startDaysBetween(startDate: ConfigType, endDate: ConfigType): number[] { const start = dayjs.utc(startDate); const end = dayjs.utc(endDate); @@ -21,7 +22,7 @@ export function startDaysBetween(startDate, endDate) { current = current.add(1, 'day'); } - const startDays = []; + const startDays: number[] = []; while (current.isBefore(end)) { startDays.push(current.valueOf()); current = current.add(1, 'week'); @@ -30,7 +31,7 @@ export function startDaysBetween(startDate, endDate) { return startDays; } -export function firstStartDateAfterDate(inputDate) { +export function firstStartDateAfterDate(inputDate: Date): number { if (!(inputDate instanceof Date)) { throw new Error('Invalid date'); } @@ -41,7 +42,14 @@ export function firstStartDateAfterDate(inputDate) { return resultDate.valueOf(); } -export function fillEmptyStartDaysWithZeroes(startDays, data) { +type DayData = { + week: number, + additions: number, + deletions: number, + commits: number, +} + +export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData): DayData[] { const result = {}; for (const startDay of startDays) { @@ -51,11 +59,11 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) { return Object.values(result); } -let dateFormat; +let dateFormat: Intl.DateTimeFormat; // format a Date object to document's locale, but with 24h format from user's current locale because this // option is a personal preference of the user, not something that the document's locale should dictate. -export function formatDatetime(date) { +export function formatDatetime(date: Date | number): string { if (!dateFormat) { // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), { diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index 470ece31b0..c5a28774a9 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -1,12 +1,12 @@ -export function pathEscapeSegments(s) { +export function pathEscapeSegments(s: string): string { return s.split('/').map(encodeURIComponent).join('/'); } -function stripSlash(url) { +function stripSlash(url: string): string { return url.endsWith('/') ? url.slice(0, -1) : url; } -export function isUrl(url) { +export function isUrl(url: string): boolean { try { return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim(); } catch { diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index 6fb0f5dc8f..68e300f551 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,10 +1,16 @@ window.__webpack_public_path__ = ''; window.config = { + appUrl: 'http://localhost:3000/', + appSubUrl: '', + assetVersionEncoded: '', + assetUrlPrefix: '', + runModeIsProd: true, + customEmojis: {}, csrfToken: 'test-csrf-token-123456', pageData: {}, - i18n: {}, - appSubUrl: '', + notificationSettings: {}, + enableTimeTracking: true, mentionValues: [ {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'}, {key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'}, @@ -14,4 +20,6 @@ window.config = { {key: 'org6 User 6', value: 'org6', name: 'org6', fullname: 'User 6', avatar: 'https://avatar6.com'}, {key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'}, ], + mermaidMaxSourceCharacters: 5000, + i18n: {}, };