Compare commits

10 Commits

Author SHA1 Message Date
Noel Jackson
65d93d819b fix(packages/container): data race when uploading container blobs concurrently (#36524)
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-02-04 00:08:20 +08:00
GiteaBot
288d1f526a [skip ci] Updated translations via Crowdin 2026-02-02 00:49:41 +00:00
Copilot
7883f6dde9 Remove and forbid @ts-expect-error (#36513)
Removes `@ts-expect-error` in the code base and forbids it.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: silverwind <115237+silverwind@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-02-02 01:00:34 +08:00
Nicolas
c2dea22926 Add resolve/unresolve review comment API endpoints (#36441)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-02-01 12:28:28 +00:00
Copilot
584d8ef75f Fix incorrect vendored detections (#36508)
Fixes: https://github.com/go-gitea/gitea/issues/22618

`go-enry`'s `IsVendor` function marks git paths (`.gitignore`,
`.gitattributes`, `.gitmodules`), github/gitea paths (`.github/`,
`.gitea/`) as "vendored" for GitHub Linguist language statistics. This
causes these files to incorrectly display the "Vendored" tag in diff
views.

Override `go-enry`'s detection for these specific cases while preserving
its behavior for actual vendor directories.

---------

Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: silverwind <115237+silverwind@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-02-01 10:35:51 +00:00
silverwind
9d96039027 Bump alpine to 3.23, add platforms to docker-dryrun (#36379)
- Bump alpine to 3.23 following
https://github.com/go-gitea/gitea/pull/36185 and
https://github.com/go-gitea/gitea/pull/36202.
- Enable all architectures in `docker-dryrun`.
- Tweak actions conditions to be more precise.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2026-02-01 09:36:43 +00:00
Copilot
072de7d8cd Unify repo names in system notices (#36491)
Fixes: https://github.com/go-gitea/gitea/issues/36211

This PR fixes ensures that all system notices consistently include
repository names in the format `"Action description (owner/repo): error
message"`.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: silverwind <115237+silverwind@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-02-01 17:06:57 +08:00
Lunny Xiao
e377da989f Allow scroll propagation outside code editor (#36502)
Fix #28479

When scrolling inside the editor and the editor has already reached the
end of its scroll area, the browser does not continue scrolling. This is
inconvenient because users must move the cursor out of the editor to
scroll the page further.

This PR enables automatic switching between the editor’s scroll and the
browser’s scroll, allowing seamless continuous scrolling.
2026-02-01 06:03:38 +00:00
wxiaoguang
7ad9bf4523 Refactor ActionsTaskID (#36503) 2026-01-31 22:01:08 -08:00
silverwind
7292ae1ed5 Update JS deps, remove knip, misc tweaks (#36499)
- Update all JS deps
- Enable a few more stylelint stylistic rules and fix issues
- Remove knip, it raised another false-positive, this tool is not worth
it when you have to babysit it like that
- Exclude @eslint/json from updating as it requires unreleased eslint 10
([ref](https://github.com/eslint/json/issues/207))
- Update labeler config for new eslint filenames
- Adjust `make help` output
- Add type checking in `stylelint.config.ts`
2026-01-31 20:58:23 +08:00
70 changed files with 1113 additions and 972 deletions
+1 -1
View File
@@ -84,9 +84,9 @@ docs-update-needed:
topic/code-linting: topic/code-linting:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- ".eslintrc.cjs"
- ".golangci.yml" - ".golangci.yml"
- ".markdownlint.yaml" - ".markdownlint.yaml"
- ".spectral.yaml" - ".spectral.yaml"
- ".yamllint.yaml" - ".yamllint.yaml"
- "eslint*.config.*"
- "stylelint.config.*" - "stylelint.config.*"
+1
View File
@@ -85,6 +85,7 @@ jobs:
- "uv.lock" - "uv.lock"
docker: docker:
- ".github/workflows/pull-docker-dryrun.yml"
- "Dockerfile" - "Dockerfile"
- "Dockerfile.rootless" - "Dockerfile.rootless"
- "docker/**" - "docker/**"
+4 -3
View File
@@ -14,24 +14,25 @@ jobs:
contents: read contents: read
container: container:
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' if: needs.files-changed.outputs.docker == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- name: Build regular container image - name: Build regular container image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: false push: false
tags: gitea/gitea:linux-amd64
- name: Build rootless container image - name: Build rootless container image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: false push: false
platforms: linux/amd64,linux/arm64,linux/riscv64
file: Dockerfile.rootless file: Dockerfile.rootless
tags: gitea/gitea:linux-amd64
+5 -2
View File
@@ -1,7 +1,10 @@
# Instructions for agents # Instructions for agents
- Use `make help` to find available development targets - Use `make help` to find available development targets
- Before committing go code changes, run `make fmt` - Use the latest Golang stable release when working on Go code
- Use the latest Node.js LTS release when working on TypeScript code
- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint
- Before committing `.ts` changes, run `make lint-js` to lint
- Before committing `go.mod` changes, run `make tidy` - Before committing `go.mod` changes, run `make tidy`
- Before committing new `.go` files, add the current year into the copyright header - Before committing new `.go` files, add the current year into the copyright header
- Before committing files, removed any trailing whitespace - Before committing any files, remove all trailing whitespace from source code lines
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build stage # Build stage
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env FROM docker.io/library/golang:1.25-alpine3.23 AS build-env
ARG GOPROXY=direct ARG GOPROXY=direct
@@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/.s6-svscan/* \ /tmp/local/etc/s6/.s6-svscan/* \
/go/src/code.gitea.io/gitea/gitea /go/src/code.gitea.io/gitea/gitea
FROM docker.io/library/alpine:3.22 AS gitea FROM docker.io/library/alpine:3.23 AS gitea
EXPOSE 22 3000 EXPOSE 22 3000
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build stage # Build stage
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env FROM docker.io/library/golang:1.25-alpine3.23 AS build-env
ARG GOPROXY=direct ARG GOPROXY=direct
@@ -33,7 +33,7 @@ COPY docker/rootless /tmp/local
RUN chmod 755 /tmp/local/usr/local/bin/* \ RUN chmod 755 /tmp/local/usr/local/bin/* \
/go/src/code.gitea.io/gitea/gitea /go/src/code.gitea.io/gitea/gitea
FROM docker.io/library/alpine:3.22 AS gitea-rootless FROM docker.io/library/alpine:3.23 AS gitea-rootless
EXPOSE 2222 3000 EXPOSE 2222 3000
+2 -4
View File
@@ -314,16 +314,14 @@ lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
.PHONY: lint-js .PHONY: lint-js
lint-js: node_modules ## lint js files lint-js: node_modules ## lint js and ts files
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES)
$(NODE_VARS) pnpm exec vue-tsc $(NODE_VARS) pnpm exec vue-tsc
$(NODE_VARS) pnpm exec knip --no-progress --cache
.PHONY: lint-js-fix .PHONY: lint-js-fix
lint-js-fix: node_modules ## lint js files and fix issues lint-js-fix: node_modules ## lint js and ts files and fix issues
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix
$(NODE_VARS) pnpm exec vue-tsc $(NODE_VARS) pnpm exec vue-tsc
$(NODE_VARS) pnpm exec knip --no-progress --cache --fix
.PHONY: lint-css .PHONY: lint-css
lint-css: node_modules ## lint css files lint-css: node_modules ## lint css files
+4 -5
View File
@@ -15,6 +15,7 @@ import vue from 'eslint-plugin-vue';
import vueScopedCss from 'eslint-plugin-vue-scoped-css'; import vueScopedCss from 'eslint-plugin-vue-scoped-css';
import wc from 'eslint-plugin-wc'; import wc from 'eslint-plugin-wc';
import {defineConfig, globalIgnores} from 'eslint/config'; import {defineConfig, globalIgnores} from 'eslint/config';
import type {ESLint} from 'eslint';
const jsExts = ['js', 'mjs', 'cjs'] as const; const jsExts = ['js', 'mjs', 'cjs'] as const;
const tsExts = ['ts', 'mts', 'cts'] as const; const tsExts = ['ts', 'mts', 'cts'] as const;
@@ -62,8 +63,7 @@ export default defineConfig([
'@stylistic': stylistic, '@stylistic': stylistic,
'@typescript-eslint': typescriptPlugin.plugin, '@typescript-eslint': typescriptPlugin.plugin,
'array-func': arrayFunc, 'array-func': arrayFunc,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/203 'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203
'import-x': importPlugin,
regexp, regexp,
sonarjs, sonarjs,
unicorn, unicorn,
@@ -156,7 +156,7 @@ export default defineConfig([
'@typescript-eslint/adjacent-overload-signatures': [0], '@typescript-eslint/adjacent-overload-signatures': [0],
'@typescript-eslint/array-type': [0], '@typescript-eslint/array-type': [0],
'@typescript-eslint/await-thenable': [2], '@typescript-eslint/await-thenable': [2],
'@typescript-eslint/ban-ts-comment': [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}], '@typescript-eslint/ban-ts-comment': [2, {'ts-expect-error': true, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}],
'@typescript-eslint/ban-tslint-comment': [0], '@typescript-eslint/ban-tslint-comment': [0],
'@typescript-eslint/class-literal-property-style': [0], '@typescript-eslint/class-literal-property-style': [0],
'@typescript-eslint/class-methods-use-this': [0], '@typescript-eslint/class-methods-use-this': [0],
@@ -924,8 +924,7 @@ export default defineConfig([
}, },
extends: [ extends: [
vue.configs['flat/recommended'], vue.configs['flat/recommended'],
// @ts-expect-error vueScopedCss.configs['flat/recommended'] as any,
vueScopedCss.configs['flat/recommended'],
], ],
rules: { rules: {
'vue/attributes-order': [0], 'vue/attributes-order': [0],
-18
View File
@@ -1,18 +0,0 @@
import type {KnipConfig} from 'knip';
export default {
entry: [
'*.ts',
'tools/**/*.ts',
'tests/e2e/**/*.ts',
],
ignoreDependencies: [
// dependencies used in Makefile or tools
'@primer/octicons',
'markdownlint-cli',
'nolyfill',
'spectral-cli-bundle',
'vue-tsc',
'webpack-cli',
],
} satisfies KnipConfig;
+14
View File
@@ -1034,6 +1034,20 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
return c, nil return c, nil
} }
func GetCommentWithRepoID(ctx context.Context, repoID, commentID int64) (*Comment, error) {
c, err := GetCommentByID(ctx, commentID)
if err != nil {
return nil, err
}
if err := c.LoadIssue(ctx); err != nil {
return nil, err
}
if c.Issue.RepoID != repoID {
return nil, ErrCommentNotExist{commentID, 0}
}
return c, nil
}
// FindCommentsOptions describes the conditions to Find comments // FindCommentsOptions describes the conditions to Find comments
type FindCommentsOptions struct { type FindCommentsOptions struct {
db.ListOptions db.ListOptions
+1
View File
@@ -102,6 +102,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
continue continue
} }
comment.Review = re comment.Review = re
comment.Issue = issue
} }
comments[n] = comment comments[n] = comment
n++ n++
+9 -2
View File
@@ -43,13 +43,15 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
existing := &PackageBlob{} existing := &PackageBlob{}
has, err := e.Where(builder.Eq{ hashCond := builder.Eq{
"size": pb.Size, "size": pb.Size,
"hash_md5": pb.HashMD5, "hash_md5": pb.HashMD5,
"hash_sha1": pb.HashSHA1, "hash_sha1": pb.HashSHA1,
"hash_sha256": pb.HashSHA256, "hash_sha256": pb.HashSHA256,
"hash_sha512": pb.HashSHA512, "hash_sha512": pb.HashSHA512,
}).Get(existing) }
has, err := e.Where(hashCond).Get(existing)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@@ -57,6 +59,11 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
return existing, true, nil return existing, true, nil
} }
if _, err = e.Insert(pb); err != nil { if _, err = e.Insert(pb); err != nil {
// Handle race condition: another request may have inserted the same blob
// between our SELECT and INSERT. Retry the SELECT to get the existing blob.
if has, _ = e.Where(hashCond).Get(existing); has {
return existing, true, nil
}
return nil, false, err return nil, false, err
} }
return pb, false, nil return pb, false, nil
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestGetOrInsertBlobConcurrent(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
testBlob := PackageBlob{
Size: 123,
HashMD5: "md5",
HashSHA1: "sha1",
HashSHA256: "sha256",
HashSHA512: "sha512",
}
const numGoroutines = 3
var wg errgroup.Group
results := make([]*PackageBlob, numGoroutines)
existed := make([]bool, numGoroutines)
for idx := range numGoroutines {
wg.Go(func() error {
blob := testBlob // Create a copy of the test blob for each goroutine
var err error
results[idx], existed[idx], err = GetOrInsertBlob(t.Context(), &blob)
return err
})
}
require.NoError(t, wg.Wait())
// then: all GetOrInsertBlob succeeds with the same blob ID, and only one indicates it did not exist before
existedCount := 0
assert.NotNil(t, results[0])
for i := range numGoroutines {
assert.Equal(t, results[0].ID, results[i].ID)
if existed[i] {
existedCount++
}
}
assert.Equal(t, numGoroutines-1, existedCount)
}
+24 -10
View File
@@ -4,6 +4,7 @@
package user package user
import ( import (
"strconv"
"strings" "strings"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
@@ -23,10 +24,6 @@ func NewGhostUser() *User {
} }
} }
func IsGhostUserName(name string) bool {
return strings.EqualFold(name, GhostUserName)
}
// IsGhost check if user is fake user for a deleted account // IsGhost check if user is fake user for a deleted account
func (u *User) IsGhost() bool { func (u *User) IsGhost() bool {
if u == nil { if u == nil {
@@ -41,10 +38,6 @@ const (
ActionsUserEmail = "teabot@gitea.io" ActionsUserEmail = "teabot@gitea.io"
) )
func IsGiteaActionsUserName(name string) bool {
return strings.EqualFold(name, ActionsUserName)
}
// NewActionsUser creates and returns a fake user for running the actions. // NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User { func NewActionsUser() *User {
return &User{ return &User{
@@ -61,15 +54,36 @@ func NewActionsUser() *User {
} }
} }
func NewActionsUserWithTaskID(id int64) *User {
u := NewActionsUser()
// LoginName is for only internal usage in this case, so it can be moved to other fields in the future
u.LoginSource = -1
u.LoginName = "@" + ActionsUserName + "/" + strconv.FormatInt(id, 10)
return u
}
func GetActionsUserTaskID(u *User) (int64, bool) {
if u == nil || u.ID != ActionsUserID {
return 0, false
}
prefix, payload, _ := strings.Cut(u.LoginName, "/")
if prefix != "@"+ActionsUserName {
return 0, false
} else if taskID, err := strconv.ParseInt(payload, 10, 64); err == nil {
return taskID, true
}
return 0, false
}
func (u *User) IsGiteaActions() bool { func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID return u != nil && u.ID == ActionsUserID
} }
func GetSystemUserByName(name string) *User { func GetSystemUserByName(name string) *User {
if IsGhostUserName(name) { if strings.EqualFold(name, GhostUserName) {
return NewGhostUser() return NewGhostUser()
} }
if IsGiteaActionsUserName(name) { if strings.EqualFold(name, ActionsUserName) {
return NewActionsUser() return NewActionsUser()
} }
return nil return nil
+8 -2
View File
@@ -16,14 +16,20 @@ func TestSystemUser(t *testing.T) {
assert.Equal(t, "Ghost", u.Name) assert.Equal(t, "Ghost", u.Name)
assert.Equal(t, "ghost", u.LowerName) assert.Equal(t, "ghost", u.LowerName)
assert.True(t, u.IsGhost()) assert.True(t, u.IsGhost())
assert.True(t, IsGhostUserName("gHost"))
u = GetSystemUserByName("gHost")
require.NotNil(t, u)
assert.Equal(t, "Ghost", u.Name)
u, err = GetPossibleUserByID(t.Context(), -2) u, err = GetPossibleUserByID(t.Context(), -2)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "gitea-actions", u.Name) assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName) assert.Equal(t, "gitea-actions", u.LowerName)
assert.True(t, u.IsGiteaActions()) assert.True(t, u.IsGiteaActions())
assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
u = GetSystemUserByName("Gitea-actionS")
require.NotNil(t, u)
assert.Equal(t, "Gitea Actions", u.FullName)
_, err = GetPossibleUserByID(t.Context(), -3) _, err = GetPossibleUserByID(t.Context(), -3)
require.Error(t, err) require.Error(t, err)
+21 -3
View File
@@ -4,10 +4,28 @@
package analyze package analyze
import ( import (
"path"
"strings"
"github.com/go-enry/go-enry/v2" "github.com/go-enry/go-enry/v2"
) )
// IsVendor returns whether or not path is a vendor path. // IsVendor returns whether the path is a vendor path.
func IsVendor(path string) bool { // It uses go-enry's IsVendor function but overrides its detection for certain
return enry.IsVendor(path) // special cases that shouldn't be marked as vendored in the diff view.
func IsVendor(treePath string) bool {
if !enry.IsVendor(treePath) {
return false
}
// Override detection for single files
basename := path.Base(treePath)
switch basename {
case ".gitignore", ".gitattributes", ".gitmodules":
return false
}
if strings.HasPrefix(treePath, ".github/") || strings.HasPrefix(treePath, ".gitea/") {
return false
}
return true
} }
+9
View File
@@ -14,6 +14,7 @@ func TestIsVendor(t *testing.T) {
path string path string
want bool want bool
}{ }{
// Original go-enry test cases
{"cache/", true}, {"cache/", true},
{"random/cache/", true}, {"random/cache/", true},
{"cache", false}, {"cache", false},
@@ -34,6 +35,14 @@ func TestIsVendor(t *testing.T) {
{"a/docs/_build/", true}, {"a/docs/_build/", true},
{"a/dasdocs/_build-vsdoc.js", true}, {"a/dasdocs/_build-vsdoc.js", true},
{"a/dasdocs/_build-vsdoc.j", false}, {"a/dasdocs/_build-vsdoc.j", false},
// Override: Git/GitHub/Gitea-related paths should NOT be detected as vendored
{".gitignore", false},
{".gitattributes", false},
{".gitmodules", false},
{"src/.gitignore", false},
{".github/workflows/ci.yml", false},
{".gitea/workflows/ci.yml", false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) { t.Run(tt.path, func(t *testing.T) {
+4 -4
View File
@@ -72,7 +72,7 @@ type PullReviewComment struct {
HTMLPullURL string `json:"pull_request_url"` HTMLPullURL string `json:"pull_request_url"`
} }
// CreatePullReviewOptions are options to create a pull review // CreatePullReviewOptions are options to create a pull request review
type CreatePullReviewOptions struct { type CreatePullReviewOptions struct {
Event ReviewStateType `json:"event"` Event ReviewStateType `json:"event"`
Body string `json:"body"` Body string `json:"body"`
@@ -91,19 +91,19 @@ type CreatePullReviewComment struct {
NewLineNum int64 `json:"new_position"` NewLineNum int64 `json:"new_position"`
} }
// SubmitPullReviewOptions are options to submit a pending pull review // SubmitPullReviewOptions are options to submit a pending pull request review
type SubmitPullReviewOptions struct { type SubmitPullReviewOptions struct {
Event ReviewStateType `json:"event"` Event ReviewStateType `json:"event"`
Body string `json:"body"` Body string `json:"body"`
} }
// DismissPullReviewOptions are options to dismiss a pull review // DismissPullReviewOptions are options to dismiss a pull request review
type DismissPullReviewOptions struct { type DismissPullReviewOptions struct {
Message string `json:"message"` Message string `json:"message"`
Priors bool `json:"priors"` Priors bool `json:"priors"`
} }
// PullReviewRequestOptions are options to add or remove pull review requests // PullReviewRequestOptions are options to add or remove pull request review requests
type PullReviewRequestOptions struct { type PullReviewRequestOptions struct {
Reviewers []string `json:"reviewers"` Reviewers []string `json:"reviewers"`
TeamReviewers []string `json:"team_reviewers"` TeamReviewers []string `json:"team_reviewers"`
+11 -8
View File
@@ -977,6 +977,7 @@
"repo.fork.blocked_user": "Ní féidir an stór a fhorcáil toisc go bhfuil úinéir an stórais bac ort.", "repo.fork.blocked_user": "Ní féidir an stór a fhorcáil toisc go bhfuil úinéir an stórais bac ort.",
"repo.use_template": "Úsáid an teimpléad seo", "repo.use_template": "Úsáid an teimpléad seo",
"repo.open_with_editor": "Oscail le %s", "repo.open_with_editor": "Oscail le %s",
"repo.download_directory_as": "Íoslódáil an eolaire mar %s",
"repo.download_zip": "Íoslódáil ZIP", "repo.download_zip": "Íoslódáil ZIP",
"repo.download_tar": "Íoslódáil TAR.GZ", "repo.download_tar": "Íoslódáil TAR.GZ",
"repo.download_bundle": "Íoslódáil BUNDLE", "repo.download_bundle": "Íoslódáil BUNDLE",
@@ -1489,6 +1490,7 @@
"repo.issues.filter_sort.feweststars": "An líon réaltaí is lú", "repo.issues.filter_sort.feweststars": "An líon réaltaí is lú",
"repo.issues.filter_sort.mostforks": "An líon forcanna is mó", "repo.issues.filter_sort.mostforks": "An líon forcanna is mó",
"repo.issues.filter_sort.fewestforks": "An líon forcanna is lú", "repo.issues.filter_sort.fewestforks": "An líon forcanna is lú",
"repo.issues.quick_goto": "Téigh go dtí an cheist",
"repo.issues.action_open": "Oscailte", "repo.issues.action_open": "Oscailte",
"repo.issues.action_close": "Dún", "repo.issues.action_close": "Dún",
"repo.issues.action_label": "Lipéad", "repo.issues.action_label": "Lipéad",
@@ -1701,6 +1703,7 @@
"repo.issues.review.content.empty": "Ní mór duit trácht a fhágáil a léiríonn an t-athrú (í) iarrtha.", "repo.issues.review.content.empty": "Ní mór duit trácht a fhágáil a léiríonn an t-athrú (í) iarrtha.",
"repo.issues.review.reject": "athruithe iarrtha %s", "repo.issues.review.reject": "athruithe iarrtha %s",
"repo.issues.review.wait": "iarradh athbhreithniú %s", "repo.issues.review.wait": "iarradh athbhreithniú %s",
"repo.issues.review.codeowners_rules": "Rialacha CÓDÚINÉIRÍ",
"repo.issues.review.add_review_request": "athbhreithniú iarrtha ó %s %s", "repo.issues.review.add_review_request": "athbhreithniú iarrtha ó %s %s",
"repo.issues.review.remove_review_request": "iarratas athbhreithnithe bainte le haghaidh %s %s", "repo.issues.review.remove_review_request": "iarratas athbhreithnithe bainte le haghaidh %s %s",
"repo.issues.review.remove_review_request_self": "dhiúltaigh athbhreithniú a dhéanamh ar %s", "repo.issues.review.remove_review_request_self": "dhiúltaigh athbhreithniú a dhéanamh ar %s",
@@ -1793,6 +1796,7 @@
"repo.pulls.remove_prefix": "Bain an réimír <strong>%s</strong>", "repo.pulls.remove_prefix": "Bain an réimír <strong>%s</strong>",
"repo.pulls.data_broken": "Tá an t-iarratas tarraingthe seo briste mar gheall ar fhaisnéis forc a bheith in easnamh.", "repo.pulls.data_broken": "Tá an t-iarratas tarraingthe seo briste mar gheall ar fhaisnéis forc a bheith in easnamh.",
"repo.pulls.files_conflicted": "Tá athruithe ag an iarratas tarraingthe seo atá contrártha leis an spriocbhrainse.", "repo.pulls.files_conflicted": "Tá athruithe ag an iarratas tarraingthe seo atá contrártha leis an spriocbhrainse.",
"repo.pulls.files_conflicted_no_listed_files": "(Níl aon chomhaid choinbhleacha liostaithe)",
"repo.pulls.is_checking": "Ag seiceáil le haghaidh coinbhleachtaí cumaisc…", "repo.pulls.is_checking": "Ag seiceáil le haghaidh coinbhleachtaí cumaisc…",
"repo.pulls.is_ancestor": "Tá an brainse seo san áireamh cheana féin sa spriocbhrainse. Níl aon rud le cumasc.", "repo.pulls.is_ancestor": "Tá an brainse seo san áireamh cheana féin sa spriocbhrainse. Níl aon rud le cumasc.",
"repo.pulls.is_empty": "Tá na hathruithe ar an mbrainse seo ar an spriocbhrainse cheana féin. Is tiomantas folamh é seo.", "repo.pulls.is_empty": "Tá na hathruithe ar an mbrainse seo ar an spriocbhrainse cheana féin. Is tiomantas folamh é seo.",
@@ -1847,7 +1851,8 @@
"repo.pulls.status_checking": "Tá roinnt seiceála ar feitheamh", "repo.pulls.status_checking": "Tá roinnt seiceála ar feitheamh",
"repo.pulls.status_checks_success": "D'éirigh le gach seiceáil", "repo.pulls.status_checks_success": "D'éirigh le gach seiceáil",
"repo.pulls.status_checks_warning": "Thuairiscigh roinnt seiceálacha rabhaidh", "repo.pulls.status_checks_warning": "Thuairiscigh roinnt seiceálacha rabhaidh",
"repo.pulls.status_checks_failure": "Theip ar roinnt seiceálacha", "repo.pulls.status_checks_failure_required": "Theip ar roinnt seiceálacha riachtanacha",
"repo.pulls.status_checks_failure_optional": "Theip ar roinnt seiceálacha roghnacha",
"repo.pulls.status_checks_error": "Thug roinnt seiceálacha earráidí", "repo.pulls.status_checks_error": "Thug roinnt seiceálacha earráidí",
"repo.pulls.status_checks_requested": "Riachtanach", "repo.pulls.status_checks_requested": "Riachtanach",
"repo.pulls.status_checks_details": "Sonraí", "repo.pulls.status_checks_details": "Sonraí",
@@ -2542,8 +2547,8 @@
"repo.diff.too_many_files": "Níor taispeánadh roinnt comhad mar go bhfuil an iomarca comhad athraithe sa difríocht seo", "repo.diff.too_many_files": "Níor taispeánadh roinnt comhad mar go bhfuil an iomarca comhad athraithe sa difríocht seo",
"repo.diff.show_more": "Taispeáin Tuilleadh", "repo.diff.show_more": "Taispeáin Tuilleadh",
"repo.diff.load": "Difríocht Luchtaigh", "repo.diff.load": "Difríocht Luchtaigh",
"repo.diff.generated": "a ghintear", "repo.diff.generated": "Gineadh",
"repo.diff.vendored": "curtha ar fáil", "repo.diff.vendored": "Díoltóir",
"repo.diff.comment.add_line_comment": "Cuir trácht líne leis", "repo.diff.comment.add_line_comment": "Cuir trácht líne leis",
"repo.diff.comment.placeholder": "Fág trácht", "repo.diff.comment.placeholder": "Fág trácht",
"repo.diff.comment.add_single_comment": "Cuir trácht aonair leis", "repo.diff.comment.add_single_comment": "Cuir trácht aonair leis",
@@ -2660,7 +2665,7 @@
"repo.branch.new_branch_from": "Cruthaigh brainse nua ó \"%s\"", "repo.branch.new_branch_from": "Cruthaigh brainse nua ó \"%s\"",
"repo.branch.renamed": "Ainmníodh brainse %s go %s.", "repo.branch.renamed": "Ainmníodh brainse %s go %s.",
"repo.branch.rename_default_or_protected_branch_error": "Ní féidir ach le riarthóirí brainsí réamhshocraithe nó cosanta a athainmniú.", "repo.branch.rename_default_or_protected_branch_error": "Ní féidir ach le riarthóirí brainsí réamhshocraithe nó cosanta a athainmniú.",
"repo.branch.rename_protected_branch_failed": "Tá an brainse seo faoi chosaint ag rialacha cosanta domhanda.", "repo.branch.rename_protected_branch_failed": "Theip ar athainmniú na brainse mar gheall ar rialacha cosanta brainse.",
"repo.branch.commits_divergence_from": "Difríocht tiomantais: %[1]d taobh thiar agus %[2]d chun tosaigh ar %[3]s", "repo.branch.commits_divergence_from": "Difríocht tiomantais: %[1]d taobh thiar agus %[2]d chun tosaigh ar %[3]s",
"repo.branch.commits_no_divergence": "Mar an gcéanna le brainse %[1]s", "repo.branch.commits_no_divergence": "Mar an gcéanna le brainse %[1]s",
"repo.tag.create_tag": "Cruthaigh clib %s", "repo.tag.create_tag": "Cruthaigh clib %s",
@@ -3281,8 +3286,6 @@
"admin.config.git_gc_args": "Argóintí GC", "admin.config.git_gc_args": "Argóintí GC",
"admin.config.git_migrate_timeout": "Teorainn Ama Imirce", "admin.config.git_migrate_timeout": "Teorainn Ama Imirce",
"admin.config.git_mirror_timeout": "Teorainn Ama Nuashonraithe Scátháin", "admin.config.git_mirror_timeout": "Teorainn Ama Nuashonraithe Scátháin",
"admin.config.git_clone_timeout": "Teorainn Ama Oibríochta Clón",
"admin.config.git_pull_timeout": "Tarraing Am Oibríochta",
"admin.config.git_gc_timeout": "Teorainn Ama Oibriúcháin GC", "admin.config.git_gc_timeout": "Teorainn Ama Oibriúcháin GC",
"admin.config.log_config": "Cumraíocht Logáil", "admin.config.log_config": "Cumraíocht Logáil",
"admin.config.logger_name_fmt": "Logálaí: %s", "admin.config.logger_name_fmt": "Logálaí: %s",
@@ -3724,8 +3727,8 @@
"projects.exit_fullscreen": "Scoir Lánscáileáin", "projects.exit_fullscreen": "Scoir Lánscáileáin",
"git.filemode.changed_filemode": "%[1]s → %[2]s", "git.filemode.changed_filemode": "%[1]s → %[2]s",
"git.filemode.directory": "Eolaire", "git.filemode.directory": "Eolaire",
"git.filemode.normal_file": "Comhad gnáth", "git.filemode.normal_file": "Rialta",
"git.filemode.executable_file": "Comhad infheidhmithe", "git.filemode.executable_file": "Inrite",
"git.filemode.symbolic_link": "Nasc siombalach", "git.filemode.symbolic_link": "Nasc siombalach",
"git.filemode.submodule": "Fo-mhodúl" "git.filemode.submodule": "Fo-mhodúl"
} }
+15 -16
View File
@@ -1,6 +1,6 @@
{ {
"type": "module", "type": "module",
"packageManager": "pnpm@10.28.1", "packageManager": "pnpm@10.28.2",
"engines": { "engines": {
"node": ">= 22.6.0", "node": ">= 22.6.0",
"pnpm": ">= 10.0.0" "pnpm": ">= 10.0.0"
@@ -28,7 +28,7 @@
"clippie": "4.1.9", "clippie": "4.1.9",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"css-loader": "7.1.2", "css-loader": "7.1.3",
"dayjs": "1.11.19", "dayjs": "1.11.19",
"dropzone": "6.0.0-beta.2", "dropzone": "6.0.0-beta.2",
"easymde": "2.20.0", "easymde": "2.20.0",
@@ -37,7 +37,7 @@
"idiomorph": "0.7.4", "idiomorph": "0.7.4",
"jquery": "4.0.0", "jquery": "4.0.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"katex": "0.16.27", "katex": "0.16.28",
"mermaid": "11.12.2", "mermaid": "11.12.2",
"mini-css-extract-plugin": "2.10.0", "mini-css-extract-plugin": "2.10.0",
"monaco-editor": "0.55.1", "monaco-editor": "0.55.1",
@@ -68,7 +68,7 @@
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.6.0", "@eslint-community/eslint-plugin-eslint-comments": "4.6.0",
"@eslint/json": "0.14.0", "@eslint/json": "0.14.0",
"@playwright/test": "1.58.0", "@playwright/test": "1.58.1",
"@stylistic/eslint-plugin": "5.7.1", "@stylistic/eslint-plugin": "5.7.1",
"@stylistic/stylelint-plugin": "5.0.1", "@stylistic/stylelint-plugin": "5.0.1",
"@types/codemirror": "5.60.17", "@types/codemirror": "5.60.17",
@@ -82,7 +82,7 @@
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4", "@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.53.1", "@typescript-eslint/parser": "8.54.0",
"@vitejs/plugin-vue": "6.0.3", "@vitejs/plugin-vue": "6.0.3",
"@vitest/eslint-plugin": "1.6.6", "@vitest/eslint-plugin": "1.6.6",
"eslint": "9.39.2", "eslint": "9.39.2",
@@ -90,34 +90,33 @@
"eslint-plugin-array-func": "5.1.0", "eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0", "eslint-plugin-github": "6.0.0",
"eslint-plugin-import-x": "4.16.1", "eslint-plugin-import-x": "4.16.1",
"eslint-plugin-playwright": "2.5.0", "eslint-plugin-playwright": "2.5.1",
"eslint-plugin-regexp": "3.0.0", "eslint-plugin-regexp": "3.0.0",
"eslint-plugin-sonarjs": "3.0.5", "eslint-plugin-sonarjs": "3.0.6",
"eslint-plugin-unicorn": "62.0.0", "eslint-plugin-unicorn": "62.0.0",
"eslint-plugin-vue": "10.7.0", "eslint-plugin-vue": "10.7.0",
"eslint-plugin-vue-scoped-css": "2.12.0", "eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.0.2", "eslint-plugin-wc": "3.0.2",
"globals": "17.1.0", "globals": "17.2.0",
"happy-dom": "20.3.7", "happy-dom": "20.4.0",
"jiti": "2.6.1", "jiti": "2.6.1",
"knip": "5.82.1",
"markdownlint-cli": "0.47.0", "markdownlint-cli": "0.47.0",
"material-icon-theme": "5.31.0", "material-icon-theme": "5.31.0",
"nolyfill": "1.0.44", "nolyfill": "1.0.44",
"postcss-html": "1.8.1", "postcss-html": "1.8.1",
"spectral-cli-bundle": "1.0.3", "spectral-cli-bundle": "1.0.3",
"stylelint": "17.0.0", "stylelint": "17.1.0",
"stylelint-config-recommended": "18.0.0", "stylelint-config-recommended": "18.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0",
"stylelint-declaration-strict-value": "1.10.11", "stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.1.1", "stylelint-value-no-unknown-custom-properties": "6.1.1",
"svgo": "4.0.0", "svgo": "4.0.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.53.1", "typescript-eslint": "8.54.0",
"updates": "17.0.8", "updates": "17.0.9",
"vite-string-plugin": "1.5.0", "vite-string-plugin": "2.0.0",
"vitest": "4.0.18", "vitest": "4.0.18",
"vue-tsc": "3.2.3" "vue-tsc": "3.2.4"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"
+346 -624
View File
File diff suppressed because it is too large Load Diff
+11 -2
View File
@@ -26,9 +26,18 @@ import (
// saveAsPackageBlob creates a package blob from an upload // saveAsPackageBlob creates a package blob from an upload
// The uploaded blob gets stored in a special upload version to link them to the package/image // The uploaded blob gets stored in a special upload version to link them to the package/image
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used // There will be concurrent uploading for the same blob, so it needs a global lock per blob hash
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam //returned PackageBlob is never used
pb := packages_service.NewPackageBlob(hsr) pb := packages_service.NewPackageBlob(hsr)
err := globallock.LockAndDo(ctx, "container-blob:"+pb.HashSHA256, func(ctx context.Context) error {
var err error
pb, err = saveAsPackageBlobInternal(ctx, hsr, pci, pb)
return err
})
return pb, err
}
func saveAsPackageBlobInternal(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo, pb *packages_model.PackageBlob) (*packages_model.PackageBlob, error) {
exists := false exists := false
contentStore := packages_module.NewContentStore() contentStore := packages_module.NewContentStore()
@@ -67,7 +76,7 @@ func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader
return createFileForBlob(ctx, uploadVersion, pb) return createFileForBlob(ctx, uploadVersion, pb)
}) })
if err != nil { if err != nil {
if !exists { if !exists && pb != nil { // pb can be nil if GetOrInsertBlob failed
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
log.Error("Error deleting package blob from content store: %v", err) log.Error("Error deleting package blob from content store: %v", err)
} }
+4 -7
View File
@@ -188,8 +188,7 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner repo.Owner = owner
ctx.Repo.Repository = repo ctx.Repo.Repository = repo
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
taskID := ctx.Data["ActionsTaskID"].(int64)
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -349,11 +348,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
// Contexter middleware already checks token for user sign in process. // Contexter middleware already checks token for user sign in process.
func reqToken() func(ctx *context.APIContext) { func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
// If actions token is present // if a real user is signed in, or the user is from a Actions task, we are good
if true == ctx.Data["IsActionsToken"] {
return
}
if ctx.IsSigned { if ctx.IsSigned {
return return
} }
@@ -1353,6 +1348,8 @@ func Routes() *web.Router {
m.Combo("").Get(repo.ListPullRequests). m.Combo("").Get(repo.ListPullRequests).
Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest) Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
m.Get("/pinned", repo.ListPinnedPullRequests) m.Get("/pinned", repo.ListPinnedPullRequests)
m.Post("/comments/{id}/resolve", reqToken(), mustNotBeArchived, repo.ResolvePullReviewComment)
m.Post("/comments/{id}/unresolve", reqToken(), mustNotBeArchived, repo.UnresolvePullReviewComment)
m.Group("/{index}", func() { m.Group("/{index}", func() {
m.Combo("").Get(repo.GetPullRequest). m.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest) Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
+3 -32
View File
@@ -445,7 +445,7 @@ func GetIssueComment(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil { if err != nil {
if issues_model.IsErrCommentNotExist(err) { if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
@@ -455,15 +455,6 @@ func GetIssueComment(ctx *context.APIContext) {
return return
} }
if err = comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound() ctx.APIErrorNotFound()
return return
@@ -579,7 +570,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
} }
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil { if err != nil {
if issues_model.IsErrCommentNotExist(err) { if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
@@ -589,16 +580,6 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return return
} }
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden) ctx.Status(http.StatusForbidden)
return return
@@ -698,7 +679,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
} }
func deleteIssueComment(ctx *context.APIContext) { func deleteIssueComment(ctx *context.APIContext) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil { if err != nil {
if issues_model.IsErrCommentNotExist(err) { if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
@@ -708,16 +689,6 @@ func deleteIssueComment(ctx *context.APIContext) {
return return
} }
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden) ctx.Status(http.StatusForbidden)
return return
+1
View File
@@ -140,6 +140,7 @@ func Migrate(ctx *context.APIContext) {
} }
opts := migrations.MigrateOptions{ opts := migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
CloneAddr: remoteAddr, CloneAddr: remoteAddr,
RepoName: form.RepoName, RepoName: form.RepoName,
Description: form.Description, Description: form.Description,
+120
View File
@@ -208,6 +208,126 @@ func GetPullReviewComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiComments) ctx.JSON(http.StatusOK, apiComments)
} }
// ResolvePullReviewComment resolves a review comment in a pull request
func ResolvePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/resolve repository repoResolvePullReviewComment
// ---
// summary: Resolve a pull request review comment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the review comment
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/validationError"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
updatePullReviewCommentResolve(ctx, true)
}
// UnresolvePullReviewComment unresolves a review comment in a pull request
func UnresolvePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/unresolve repository repoUnresolvePullReviewComment
// ---
// summary: Unresolve a pull request review comment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the review comment
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/validationError"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
updatePullReviewCommentResolve(ctx, false)
}
func updatePullReviewCommentResolve(ctx *context.APIContext, isResolve bool) {
comment := getPullReviewCommentToResolve(ctx)
if comment == nil {
return
}
canMarkConv, err := issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !canMarkConv {
ctx.APIError(http.StatusForbidden, "user should have permission to resolve comment")
return
}
if err = issues_model.MarkConversation(ctx, comment, ctx.Doer, isResolve); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func getPullReviewCommentToResolve(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound("GetCommentByID", err)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
if !comment.Issue.IsPull {
ctx.APIError(http.StatusBadRequest, "comment does not belong to a pull request")
return nil
}
if comment.Type != issues_model.CommentTypeCode {
ctx.APIError(http.StatusBadRequest, "comment is not a review comment")
return nil
}
return comment
}
// DeletePullReview delete a specific review from a pull request // DeletePullReview delete a specific review from a pull request
func DeletePullReview(ctx *context.APIContext) { func DeletePullReview(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
+3 -3
View File
@@ -22,6 +22,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
@@ -166,7 +167,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil return nil
} }
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true { if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() {
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID) _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil { if err == nil {
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
@@ -197,8 +198,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
accessMode = perm.AccessModeRead accessMode = perm.AccessModeRead
} }
if ctx.Data["IsActionsToken"] == true { if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
taskID := ctx.Data["ActionsTaskID"].(int64)
p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil { if err != nil {
ctx.ServerError("GetActionsUserRepoPermission", err) ctx.ServerError("GetActionsUserRepoPermission", err)
+1 -5
View File
@@ -117,12 +117,8 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken) task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil { if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["LoginMethod"] = ActionTokenMethodName store.GetData()["LoginMethod"] = ActionTokenMethodName
store.GetData()["IsActionsToken"] = true return user_model.NewActionsUserWithTaskID(task.ID), nil
store.GetData()["ActionsTaskID"] = task.ID
return user_model.NewActionsUser(), nil
} }
if !setting.Service.EnableBasicAuth { if !setting.Service.EnableBasicAuth {
+19 -40
View File
@@ -6,6 +6,7 @@ package auth
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -17,14 +18,12 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/oauth2_provider" "code.gitea.io/gitea/services/oauth2_provider"
) )
// Ensure the struct implements the interface. var _ Method = &OAuth2{}
var (
_ Method = &OAuth2{}
)
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id // GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) { func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
@@ -106,18 +105,16 @@ func parseToken(req *http.Request) (string, bool) {
return "", false return "", false
} }
// userIDFromToken returns the user id corresponding to the OAuth token. // userFromToken returns the user corresponding to the OAuth token.
// It will set 'IsApiToken' to true if the token is an API token and // It will set 'IsApiToken' to true if the token is an API token and
// set 'ApiTokenScope' to the scope of the access token // set 'ApiTokenScope' to the scope of the access token (TODO: this behavior should be fixed, don't set ctx.Data)
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 { func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataStore) (*user_model.User, error) {
// Let's see if token is valid. // Let's see if token is valid.
if strings.Contains(tokenSHA, ".") { if strings.Contains(tokenSHA, ".") {
// First attempt to decode an actions JWT, returning the actions user // First attempt to decode an actions JWT, returning the actions user
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil { if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
if CheckTaskIsRunning(ctx, taskID) { if CheckTaskIsRunning(ctx, taskID) {
store.GetData()["IsActionsToken"] = true return user_model.NewActionsUserWithTaskID(taskID), nil
store.GetData()["ActionsTaskID"] = taskID
return user_model.ActionsUserID
} }
} }
@@ -127,33 +124,27 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
store.GetData()["IsApiToken"] = true store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = accessTokenScope store.GetData()["ApiTokenScope"] = accessTokenScope
} }
return uid return user_model.GetUserByID(ctx, uid)
} }
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA) t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
if err != nil { if err != nil {
if auth_model.IsErrAccessTokenNotExist(err) { if auth_model.IsErrAccessTokenNotExist(err) {
// check task token // check task token
task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA) if task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA); err == nil {
if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
}
return nil, err
}
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.ActionsUserID
}
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err)
}
return 0
}
t.UpdatedUnix = timeutil.TimeStampNow() t.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(ctx, t); err != nil { if err = auth_model.UpdateAccessToken(ctx, t); err != nil {
log.Error("UpdateAccessToken: %v", err) log.Error("UpdateAccessToken: %v", err)
} }
store.GetData()["IsApiToken"] = true store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = t.Scope store.GetData()["ApiTokenScope"] = t.Scope
return t.UID return user_model.GetUserByID(ctx, t.UID)
} }
// Verify extracts the user ID from the OAuth token in the query parameters // Verify extracts the user ID from the OAuth token in the query parameters
@@ -173,21 +164,9 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
return nil, nil return nil, nil
} }
id := o.userIDFromToken(req.Context(), token, store) user, err := o.userFromToken(req.Context(), token, store)
if err != nil && !errors.Is(err, util.ErrNotExist) {
if id <= 0 && id != -2 { // -2 means actions, so we need to allow it. log.Error("userFromToken: %v", err) // the callers might ignore the error, so log it here
return nil, user_model.ErrUserNotExist{}
} }
log.Trace("OAuth2 Authorization: Found token for user[%d]", id) return user, err
user, err := user_model.GetPossibleUserByID(req.Context(), id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByName: %v", err)
}
return nil, err
}
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
return user, nil
} }
+8 -5
View File
@@ -12,23 +12,26 @@ import (
"code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestUserIDFromToken(t *testing.T) { func TestUserIDFromToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("Actions JWT", func(t *testing.T) { t.Run("Actions JWT", func(t *testing.T) {
const RunningTaskID = 47 const RunningTaskID int64 = 47
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
assert.NoError(t, err) assert.NoError(t, err)
ds := make(reqctx.ContextData) ds := make(reqctx.ContextData)
o := OAuth2{} o := OAuth2{}
uid := o.userIDFromToken(t.Context(), token, ds) u, err := o.userFromToken(t.Context(), token, ds)
assert.Equal(t, user_model.ActionsUserID, uid) require.NoError(t, err)
assert.Equal(t, true, ds["IsActionsToken"]) assert.Equal(t, user_model.ActionsUserID, u.ID)
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) taskID, ok := user_model.GetActionsUserTaskID(u)
assert.True(t, ok)
assert.Equal(t, RunningTaskID, taskID)
}) })
} }
+13 -7
View File
@@ -92,12 +92,21 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
for _, lines := range review.CodeComments { for _, lines := range review.CodeComments {
for _, comments := range lines { for _, comments := range lines {
for _, comment := range comments { for _, comment := range comments {
apiComments = append(apiComments, ToPullReviewComment(ctx, comment, doer))
}
}
}
return apiComments, nil
}
// ToPullReviewComment convert a single code review comment to api format
func ToPullReviewComment(ctx context.Context, comment *issues_model.Comment, doer *user_model.User) *api.PullReviewComment {
apiComment := &api.PullReviewComment{ apiComment := &api.PullReviewComment{
ID: comment.ID, ID: comment.ID,
Body: comment.Content, Body: comment.Content,
Poster: ToUser(ctx, comment.Poster, doer), Poster: ToUser(ctx, comment.Poster, doer),
Resolver: ToUser(ctx, comment.ResolveDoer, doer), Resolver: ToUser(ctx, comment.ResolveDoer, doer),
ReviewID: review.ID, ReviewID: comment.ReviewID,
Created: comment.CreatedUnix.AsTime(), Created: comment.CreatedUnix.AsTime(),
Updated: comment.UpdatedUnix.AsTime(), Updated: comment.UpdatedUnix.AsTime(),
Path: comment.TreePath, Path: comment.TreePath,
@@ -105,7 +114,7 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
OrigCommitID: comment.OldRef, OrigCommitID: comment.OldRef,
DiffHunk: patch2diff(comment.Patch), DiffHunk: patch2diff(comment.Patch),
HTMLURL: comment.HTMLURL(ctx), HTMLURL: comment.HTMLURL(ctx),
HTMLPullURL: review.Issue.HTMLURL(ctx), HTMLPullURL: comment.Issue.HTMLURL(ctx),
} }
if comment.Line < 0 { if comment.Line < 0 {
@@ -113,11 +122,8 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
} else { } else {
apiComment.LineNum = comment.UnsignedLine() apiComment.LineNum = comment.UnsignedLine()
} }
apiComments = append(apiComments, apiComment)
} return apiComment
}
}
return apiComments, nil
} }
func patch2diff(patch string) string { func patch2diff(patch string) string {
+1 -2
View File
@@ -541,8 +541,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
accessMode = perm_model.AccessModeWrite accessMode = perm_model.AccessModeWrite
} }
if ctx.Data["IsActionsToken"] == true { if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
taskID := ctx.Data["ActionsTaskID"].(int64)
perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID) perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID)
if err != nil { if err != nil {
log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err) log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err)
+2 -2
View File
@@ -131,8 +131,8 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
if err1 := uploader.Rollback(); err1 != nil { if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1) log.Error("rollback failed: %v", err1)
} }
if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository (%s/%s) from %s failed: %v", ownerName, opts.RepoName, opts.OriginalURL, err)); err2 != nil {
log.Error("create respotiry notice failed: ", err2) log.Error("create repository notice failed: ", err2)
} }
return nil, err return nil, err
} }
+4 -4
View File
@@ -202,7 +202,7 @@ func pruneBrokenReferences(ctx context.Context, m *repo_model.Mirror, gitRepo gi
stdoutMessage := util.SanitizeCredentialURLs(stdout) stdoutMessage := util.SanitizeCredentialURLs(stdout)
log.Error("Failed to prune mirror repository %s references:\nStdout: %s\nStderr: %s\nErr: %v", gitRepo.RelativePath(), stdoutMessage, stderrMessage, pruneErr) log.Error("Failed to prune mirror repository %s references:\nStdout: %s\nStderr: %s\nErr: %v", gitRepo.RelativePath(), stdoutMessage, stderrMessage, pruneErr)
desc := fmt.Sprintf("Failed to prune mirror repository %s references: %s", gitRepo.RelativePath(), stderrMessage) desc := fmt.Sprintf("Failed to prune mirror repository (%s) references: %s", m.Repo.FullName(), stderrMessage)
if err := system_model.CreateRepositoryNotice(desc); err != nil { if err := system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
@@ -277,7 +277,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
// If there is still an error (or there always was an error) // If there is still an error (or there always was an error)
if err != nil { if err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", m.Repo.RelativePath(), stderrMessage) desc := fmt.Sprintf("Failed to update mirror repository (%s): %s", m.Repo.FullName(), stderrMessage)
if err := system_model.CreateRepositoryNotice(desc); err != nil { if err := system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
@@ -355,7 +355,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
// If there is still an error (or there always was an error) // If there is still an error (or there always was an error)
if err != nil { if err != nil {
log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", m.Repo.WikiStorageRepo().RelativePath(), stderrMessage) desc := fmt.Sprintf("Failed to update mirror repository wiki (%s): %s", m.Repo.FullName(), stderrMessage)
if err := system_model.CreateRepositoryNotice(desc); err != nil { if err := system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
@@ -595,7 +595,7 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, re
// Update the is empty and default_branch columns // Update the is empty and default_branch columns
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, m.Repo, "default_branch", "is_empty"); err != nil { if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, m.Repo, "default_branch", "is_empty"); err != nil {
log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err) log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err)
desc := fmt.Sprintf("Failed to update default branch of repository '%s': %v", m.Repo.RelativePath(), err) desc := fmt.Sprintf("Failed to update default branch of repository (%s): %v", m.Repo.FullName(), err)
if err = system_model.CreateRepositoryNotice(desc); err != nil { if err = system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
+4 -4
View File
@@ -63,10 +63,10 @@ func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
} }
return &BlobUploader{ return &BlobUploader{
model, PackageBlobUpload: model,
hash, MultiHasher: hash,
f, file: f,
false, reading: false,
}, nil }, nil
} }
+1 -1
View File
@@ -74,7 +74,7 @@ func AdoptRepository(ctx context.Context, doer, owner *user_model.User, opts Cre
// WARNING: Don't override all later err with local variables // WARNING: Don't override all later err with local variables
defer func() { defer func() {
if err != nil { if err != nil {
// we can not use the ctx because it maybe canceled or timeout // we can not use `ctx` because it may be canceled or timed out
if errDel := deleteFailedAdoptRepository(repo.ID); errDel != nil { if errDel := deleteFailedAdoptRepository(repo.ID); errDel != nil {
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel) log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
} }
+4 -4
View File
@@ -91,7 +91,7 @@ func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Du
stdout, _, err = gitrepo.RunCmdString(ctx, repo, command) stdout, _, err = gitrepo.RunCmdString(ctx, repo, command)
if err != nil { if err != nil {
log.Error("Repository garbage collection failed for %-v. Stdout: %s\nError: %v", repo, stdout, err) log.Error("Repository garbage collection failed for %-v. Stdout: %s\nError: %v", repo, stdout, err)
desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RelativePath(), stdout, err) desc := fmt.Sprintf("Repository garbage collection failed (%s). Stdout: %s\nError: %v", repo.FullName(), stdout, err)
if err := system_model.CreateRepositoryNotice(desc); err != nil { if err := system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
@@ -101,7 +101,7 @@ func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Du
// Now update the size of the repository // Now update the size of the repository
if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
log.Error("Updating size as part of garbage collection failed for %-v. Stdout: %s\nError: %v", repo, stdout, err) log.Error("Updating size as part of garbage collection failed for %-v. Stdout: %s\nError: %v", repo, stdout, err)
desc := fmt.Sprintf("Updating size as part of garbage collection failed for %s. Stdout: %s\nError: %v", repo.RelativePath(), stdout, err) desc := fmt.Sprintf("Updating size as part of garbage collection failed (%s). Stdout: %s\nError: %v", repo.FullName(), stdout, err)
if err := system_model.CreateRepositoryNotice(desc); err != nil { if err := system_model.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
@@ -163,7 +163,7 @@ func DeleteMissingRepositories(ctx context.Context, doer *user_model.User) error
log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID)
if err := DeleteRepositoryDirectly(ctx, repo.ID); err != nil { if err := DeleteRepositoryDirectly(ctx, repo.ID); err != nil {
log.Error("Failed to DeleteRepository %-v: Error: %v", repo, err) log.Error("Failed to DeleteRepository %-v: Error: %v", repo, err)
if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository (%s) [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
} }
@@ -191,7 +191,7 @@ func ReinitMissingRepositories(ctx context.Context) error {
log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID)
if err := gitrepo.InitRepository(ctx, repo, repo.ObjectFormatName); err != nil { if err := gitrepo.InitRepository(ctx, repo, repo.ObjectFormatName); err != nil {
log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RelativePath(), err) log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RelativePath(), err)
if err2 := system_model.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { if err2 := system_model.CreateRepositoryNotice("InitRepository (%s) [%d]: %v", repo.FullName(), repo.ID, err); err2 != nil {
log.Error("CreateRepositoryNotice: %v", err2) log.Error("CreateRepositoryNotice: %v", err2)
} }
} }
+6 -6
View File
@@ -265,8 +265,8 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
// WARNING: Don't override all later err with local variables // WARNING: Don't override all later err with local variables
defer func() { defer func() {
if err != nil { if err != nil {
// we can not use the ctx because it maybe canceled or timeout // we can not use `ctx` because it may be canceled or timed out
cleanupRepository(repo.ID) cleanupRepository(repo)
} }
}() }()
@@ -461,11 +461,11 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r
return nil return nil
} }
func cleanupRepository(repoID int64) { func cleanupRepository(repo *repo_model.Repository) {
if errDelete := DeleteRepositoryDirectly(graceful.GetManager().ShutdownContext(), repoID); errDelete != nil { ctx := graceful.GetManager().ShutdownContext()
if errDelete := DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil {
log.Error("cleanupRepository failed: %v", errDelete) log.Error("cleanupRepository failed: %v", errDelete)
// add system notice if err := system_model.CreateRepositoryNotice("DeleteRepositoryDirectly failed when cleanup repository (%s)", repo.FullName(), errDelete); err != nil {
if err := system_model.CreateRepositoryNotice("DeleteRepositoryDirectly failed when cleanup repository: %v", errDelete); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
} }
+2 -2
View File
@@ -309,7 +309,7 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
// Remove repository files. // Remove repository files.
if err := gitrepo.DeleteRepository(ctx, repo); err != nil { if err := gitrepo.DeleteRepository(ctx, repo); err != nil {
desc := fmt.Sprintf("Delete repository files [%s]: %v", repo.FullName(), err) desc := fmt.Sprintf("Delete repository files (%s): %v", repo.FullName(), err)
if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil { if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
} }
@@ -317,7 +317,7 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
// Remove wiki files if it exists. // Remove wiki files if it exists.
if err := gitrepo.DeleteRepository(ctx, repo.WikiStorageRepo()); err != nil { if err := gitrepo.DeleteRepository(ctx, repo.WikiStorageRepo()); err != nil {
desc := fmt.Sprintf("Delete wiki repository files [%s]: %v", repo.FullName(), err) desc := fmt.Sprintf("Delete wiki repository files (%s): %v", repo.FullName(), err)
// Note we use the db.DefaultContext here rather than passing in a context as the context may be cancelled // Note we use the db.DefaultContext here rather than passing in a context as the context may be cancelled
if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil { if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
+2 -2
View File
@@ -123,8 +123,8 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
// WARNING: Don't override all later err with local variables // WARNING: Don't override all later err with local variables
defer func() { defer func() {
if err != nil { if err != nil {
// we can not use the ctx because it maybe canceled or timeout // we can not use `ctx` because it may be canceled or timed out
cleanupRepository(repo.ID) cleanupRepository(repo)
} }
}() }()
+2 -2
View File
@@ -100,8 +100,8 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
// last - clean up the repository if something goes wrong // last - clean up the repository if something goes wrong
defer func() { defer func() {
if err != nil { if err != nil {
// we can not use the ctx because it maybe canceled or timeout // we can not use `ctx` because it may be canceled or timed out
cleanupRepository(generateRepo.ID) cleanupRepository(generateRepo)
} }
}() }()
+1 -1
View File
@@ -317,7 +317,7 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description) text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
color = greenColor color = greenColor
if withSender { if withSender {
if user_model.IsGiteaActionsUserName(p.Sender.UserName) { if user_model.GetSystemUserByName(p.Sender.UserName) != nil {
text += " by " + p.Sender.FullName text += " by " + p.Sender.FullName
} else { } else {
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+1 -1
View File
@@ -369,7 +369,7 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
} }
if err := gitrepo.DeleteRepository(ctx, repo.WikiStorageRepo()); err != nil { if err := gitrepo.DeleteRepository(ctx, repo.WikiStorageRepo()); err != nil {
desc := fmt.Sprintf("Delete wiki repository files [%s]: %v", repo.FullName(), err) desc := fmt.Sprintf("Delete wiki repository files (%s): %v", repo.FullName(), err)
// Note we use the db.DefaultContext here rather than passing in a context as the context may be cancelled // Note we use the db.DefaultContext here rather than passing in a context as the context may be cancelled
if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil { if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err) log.Error("CreateRepositoryNotice: %v", err)
+5 -3
View File
@@ -1,3 +1,4 @@
// @ts-check
// TODO: Move to .ts after https://github.com/stylelint/stylelint/issues/8893 is fixed // TODO: Move to .ts after https://github.com/stylelint/stylelint/issues/8893 is fixed
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
@@ -7,6 +8,7 @@ const cssVarFiles = [
fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)), fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
]; ];
/** @type {import('stylelint').Config} */
export default { export default {
extends: 'stylelint-config-recommended', extends: 'stylelint-config-recommended',
reportUnscopedDisables: true, reportUnscopedDisables: true,
@@ -57,14 +59,14 @@ export default {
'@stylistic/block-opening-brace-space-before': 'always', '@stylistic/block-opening-brace-space-before': 'always',
'@stylistic/color-hex-case': 'lower', '@stylistic/color-hex-case': 'lower',
'@stylistic/declaration-bang-space-after': 'never', '@stylistic/declaration-bang-space-after': 'never',
'@stylistic/declaration-bang-space-before': null, '@stylistic/declaration-bang-space-before': 'always',
'@stylistic/declaration-block-semicolon-newline-after': null, '@stylistic/declaration-block-semicolon-newline-after': null,
'@stylistic/declaration-block-semicolon-newline-before': null, '@stylistic/declaration-block-semicolon-newline-before': null,
'@stylistic/declaration-block-semicolon-space-after': null, '@stylistic/declaration-block-semicolon-space-after': null,
'@stylistic/declaration-block-semicolon-space-before': 'never', '@stylistic/declaration-block-semicolon-space-before': 'never',
'@stylistic/declaration-block-trailing-semicolon': null, '@stylistic/declaration-block-trailing-semicolon': null,
'@stylistic/declaration-colon-newline-after': null, '@stylistic/declaration-colon-newline-after': null,
'@stylistic/declaration-colon-space-after': null, '@stylistic/declaration-colon-space-after': 'always',
'@stylistic/declaration-colon-space-before': 'never', '@stylistic/declaration-colon-space-before': 'never',
'@stylistic/function-comma-newline-after': null, '@stylistic/function-comma-newline-after': null,
'@stylistic/function-comma-newline-before': null, '@stylistic/function-comma-newline-before': null,
@@ -101,7 +103,7 @@ export default {
'@stylistic/selector-attribute-operator-space-before': null, '@stylistic/selector-attribute-operator-space-before': null,
'@stylistic/selector-combinator-space-after': null, '@stylistic/selector-combinator-space-after': null,
'@stylistic/selector-combinator-space-before': null, '@stylistic/selector-combinator-space-before': null,
'@stylistic/selector-descendant-combinator-no-non-space': null, '@stylistic/selector-descendant-combinator-no-non-space': true,
'@stylistic/selector-list-comma-newline-after': null, '@stylistic/selector-list-comma-newline-after': null,
'@stylistic/selector-list-comma-newline-before': null, '@stylistic/selector-list-comma-newline-before': null,
'@stylistic/selector-list-comma-space-after': 'always-single-line', '@stylistic/selector-list-comma-space-after': 'always-single-line',
+104 -4
View File
@@ -13707,6 +13707,106 @@
} }
} }
}, },
"/repos/{owner}/{repo}/pulls/comments/{id}/resolve": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Resolve a pull request review comment",
"operationId": "repoResolvePullReviewComment",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"format": "int64",
"description": "id of the review comment",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"400": {
"$ref": "#/responses/validationError"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/pulls/comments/{id}/unresolve": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Unresolve a pull request review comment",
"operationId": "repoUnresolvePullReviewComment",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"format": "int64",
"description": "id of the review comment",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"400": {
"$ref": "#/responses/validationError"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/pulls/pinned": { "/repos/{owner}/{repo}/pulls/pinned": {
"get": { "get": {
"produces": [ "produces": [
@@ -23536,7 +23636,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"CreatePullReviewOptions": { "CreatePullReviewOptions": {
"description": "CreatePullReviewOptions are options to create a pull review", "description": "CreatePullReviewOptions are options to create a pull request review",
"type": "object", "type": "object",
"properties": { "properties": {
"body": { "body": {
@@ -24133,7 +24233,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"DismissPullReviewOptions": { "DismissPullReviewOptions": {
"description": "DismissPullReviewOptions are options to dismiss a pull review", "description": "DismissPullReviewOptions are options to dismiss a pull request review",
"type": "object", "type": "object",
"properties": { "properties": {
"message": { "message": {
@@ -27645,7 +27745,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"PullReviewRequestOptions": { "PullReviewRequestOptions": {
"description": "PullReviewRequestOptions are options to add or remove pull review requests", "description": "PullReviewRequestOptions are options to add or remove pull request review requests",
"type": "object", "type": "object",
"properties": { "properties": {
"reviewers": { "reviewers": {
@@ -28389,7 +28489,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"SubmitPullReviewOptions": { "SubmitPullReviewOptions": {
"description": "SubmitPullReviewOptions are options to submit a pending pull review", "description": "SubmitPullReviewOptions are options to submit a pending pull request review",
"type": "object", "type": "object",
"properties": { "properties": {
"body": { "body": {
+75
View File
@@ -15,9 +15,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -362,6 +364,79 @@ func TestAPIPullReviewRequest(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent) MakeRequest(t, req, http.StatusNoContent)
} }
func TestAPIPullReviewCommentResolveEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := t.Context()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
require.NoError(t, pullIssue.LoadAttributes(ctx))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: pullIssue.PosterID})
require.NoError(t, pullIssue.LoadPullRequest(ctx))
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
require.NoError(t, err)
defer gitRepo.Close()
latestCommitID, err := gitRepo.GetRefCommitID(pullIssue.PullRequest.GetGitHeadRefName())
require.NoError(t, err)
codeComment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, pullIssue, 1, "resolve comment", "README.md", false, 0, latestCommitID, nil)
require.NoError(t, err)
require.NotNil(t, codeComment)
session := loginUser(t, doer.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, codeComment.ID)
unresolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/unresolve", repo.OwnerName, repo.Name, codeComment.ID)
req := NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is resolved
updatedComment, err := issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.NotZero(t, updatedComment.ResolveDoerID)
assert.Equal(t, doer.ID, updatedComment.ResolveDoerID)
// Resolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, http.MethodPost, unresolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is unresolved
updatedComment, err = issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.Zero(t, updatedComment.ResolveDoerID)
// Unresolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
// Non-existing comment ID
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/999999/resolve", repo.OwnerName, repo.Name)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// Non-code-comment
plainComment, err := issue_service.CreateIssueComment(ctx, doer, repo, pullIssue, "not a review comment", nil)
require.NoError(t, err)
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
// Test permission check: use a user without write access for target repo to test 403 response
unauthorizedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
require.NotEqual(t, pullIssue.PosterID, unauthorizedUser.ID)
unauthorizedSession := loginUser(t, unauthorizedUser.Name)
unauthorizedToken := getTokenForLoggedInUser(t, unauthorizedSession, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusForbidden)
}
func TestAPIPullReviewStayDismissed(t *testing.T) { func TestAPIPullReviewStayDismissed(t *testing.T) {
// This test against issue https://github.com/go-gitea/gitea/issues/28542 // This test against issue https://github.com/go-gitea/gitea/issues/28542
// where old reviews surface after a review request got dismissed. // where old reviews surface after a review request got dismissed.
+1 -1
View File
@@ -13,7 +13,7 @@
"target": "es2020", "target": "es2020",
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext", "webworker"],
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"allowJs": true, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
Vendored
+87
View File
@@ -2,18 +2,105 @@ declare module '@techknowlogick/license-checker-webpack-plugin' {
const plugin: any; const plugin: any;
export = plugin; export = plugin;
} }
declare module 'eslint-plugin-no-use-extend-native' { declare module 'eslint-plugin-no-use-extend-native' {
import type {Eslint} from 'eslint'; import type {Eslint} from 'eslint';
const plugin: Eslint.Plugin; const plugin: Eslint.Plugin;
export = plugin; export = plugin;
} }
declare module 'eslint-plugin-array-func' { declare module 'eslint-plugin-array-func' {
import type {Eslint} from 'eslint'; import type {Eslint} from 'eslint';
const plugin: Eslint.Plugin; const plugin: Eslint.Plugin;
export = plugin; export = plugin;
} }
declare module 'eslint-plugin-github' { declare module 'eslint-plugin-github' {
import type {Eslint} from 'eslint'; import type {Eslint} from 'eslint';
const plugin: Eslint.Plugin; const plugin: Eslint.Plugin;
export = plugin; export = plugin;
} }
declare module '*.svg' {
const value: string;
export default value;
}
declare module '*.css' {
const value: string;
export default value;
}
declare module '*.vue' {
import type {DefineComponent} from 'vue';
const component: DefineComponent<unknown, unknown, any>;
export default component;
// Here we declare all exports from vue files so `tsc` or `tsgo` can work for
// non-vue files. To lint .vue files, `vue-tsc` must be used.
export function initDashboardRepoList(): void;
export function initRepositoryActionView(): void;
}
declare module 'htmx.org/dist/htmx.esm.js' {
const value = await import('htmx.org');
export default value;
}
declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
const value = await import('swagger-ui-dist');
export default value.SwaggerUIBundle;
}
declare module 'asciinema-player' {
interface AsciinemaPlayer {
create(src: string, element: HTMLElement, options?: Record<string, unknown>): void;
}
const exports: AsciinemaPlayer;
export = exports;
}
declare module '@citation-js/core' {
export class Cite {
constructor(data: string);
format(format: string, options?: Record<string, any>): string;
}
export const plugins: {
config: {
get(name: string): any;
};
};
}
declare module '@citation-js/plugin-software-formats' {}
declare module '@citation-js/plugin-bibtex' {}
declare module '@citation-js/plugin-csl' {}
declare module 'vue-bar-graph' {
import type {DefineComponent} from 'vue';
interface BarGraphPoint {
value: number;
label: string;
}
export const VueBarGraph: DefineComponent<{
points?: Array<BarGraphPoint>;
barColor?: string;
textColor?: string;
textAltColor?: string;
height?: number;
labelHeight?: number;
}>;
}
declare module '@mcaptcha/vanilla-glue' {
export let INPUT_NAME: string;
export default class Widget {
constructor(options: {
siteKey: {
instanceUrl: URL;
key: string;
};
});
}
}
+1
View File
@@ -5,5 +5,6 @@ export default {
'@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
'cropperjs', // need to migrate to v2 but v2 is not compatible with v1 'cropperjs', // need to migrate to v2 but v2 is not compatible with v1
'tailwindcss', // need to migrate 'tailwindcss', // need to migrate
'@eslint/json', // needs eslint 10
], ],
} satisfies Config; } satisfies Config;
+2 -3
View File
@@ -80,13 +80,12 @@ function initGlobalErrorHandler() {
// we added an event handler for window error at the very beginning of <script> of page head the // we added an event handler for window error at the very beginning of <script> of page head the
// handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before // handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before
// this init then in this init, we can collect all error events and show them. // this init then in this init, we can collect all error events and show them.
for (const e of window._globalHandlerErrors || []) { for (const e of (window._globalHandlerErrors as Iterable<ErrorEvent & PromiseRejectionEvent>) || []) {
processWindowErrorEvent(e); processWindowErrorEvent(e);
} }
// then, change _globalHandlerErrors to an object with push method, to process further error // then, change _globalHandlerErrors to an object with push method, to process further error
// events directly // events directly
// @ts-expect-error -- this should be refactored to not use a fake array window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)} as any;
window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)};
} }
initGlobalErrorHandler(); initGlobalErrorHandler();
+6 -5
View File
@@ -13,6 +13,8 @@ import {localUserSettings} from '../modules/user-settings.ts';
// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
type StepContainerElement = HTMLElement & {_stepLogsActiveContainer?: HTMLElement}
type LogLine = { type LogLine = {
index: number; index: number;
timestamp: number; timestamp: number;
@@ -221,19 +223,18 @@ export default defineComponent({
}, },
// get the job step logs container ('.job-step-logs') // get the job step logs container ('.job-step-logs')
getJobStepLogsContainer(stepIndex: number): HTMLElement { getJobStepLogsContainer(stepIndex: number): StepContainerElement {
return (this.$refs.logs as any)[stepIndex]; return (this.$refs.logs as any)[stepIndex];
}, },
// get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group` // get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
getActiveLogsContainer(stepIndex: number): HTMLElement { getActiveLogsContainer(stepIndex: number): StepContainerElement {
const el = this.getJobStepLogsContainer(stepIndex); const el = this.getJobStepLogsContainer(stepIndex);
// @ts-expect-error - _stepLogsActiveContainer is a custom property
return el._stepLogsActiveContainer ?? el; return el._stepLogsActiveContainer ?? el;
}, },
// begin a log group // begin a log group
beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) {
const el = (this.$refs.logs as any)[stepIndex]; const el = (this.$refs.logs as any)[stepIndex] as StepContainerElement;
const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'}, const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'},
this.createLogLine(stepIndex, startTime, { this.createLogLine(stepIndex, startTime, {
index: line.index, index: line.index,
@@ -395,7 +396,7 @@ export default defineComponent({
} }
// auto-scroll to the last log line of the last step // auto-scroll to the last log line of the last step
let autoScrollJobStepElement: HTMLElement | undefined; let autoScrollJobStepElement: StepContainerElement | undefined;
for (let stepIndex = 0; stepIndex < this.currentJob.steps.length; stepIndex++) { for (let stepIndex = 0; stepIndex < this.currentJob.steps.length; stepIndex++) {
if (!autoScrollStepIndexes.get(stepIndex)) continue; if (!autoScrollStepIndexes.get(stepIndex)) continue;
autoScrollJobStepElement = this.getJobStepLogsContainer(stepIndex); autoScrollJobStepElement = this.getJobStepLogsContainer(stepIndex);
@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
// @ts-expect-error - module exports no types
import {VueBarGraph} from 'vue-bar-graph'; import {VueBarGraph} from 'vue-bar-graph';
import {computed, onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue'; import {computed, onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
@@ -155,9 +155,8 @@ export default defineComponent({
return -1; return -1;
}, },
getActiveItem() { getActiveItem() {
const el = this.$refs[`listItem${this.activeItemIndex}`]; const el = this.$refs[`listItem${this.activeItemIndex}`] as Array<HTMLDivElement>;
// @ts-expect-error - el is unknown type return el?.length ? el[0] : null;
return (el && el.length) ? el[0] : null;
}, },
keydown(e: KeyboardEvent) { keydown(e: KeyboardEvent) {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
@@ -174,7 +173,7 @@ export default defineComponent({
return; return;
} }
this.activeItemIndex = nextIndex; this.activeItemIndex = nextIndex;
this.getActiveItem().scrollIntoView({block: 'nearest'}); this.getActiveItem()!.scrollIntoView({block: 'nearest'});
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
this.getActiveItem()?.click(); this.getActiveItem()?.click();
+10 -2
View File
@@ -41,6 +41,15 @@ const customEventListener: Plugin = {
}, },
}; };
type LineOptions = ChartOptions<'line'> & {
plugins?: {
customEventListener?: {
chartType: string;
instance: unknown;
};
};
}
Chart.defaults.color = chartJsColors.text; Chart.defaults.color = chartJsColors.text;
Chart.defaults.borderColor = chartJsColors.border; Chart.defaults.borderColor = chartJsColors.border;
@@ -251,7 +260,7 @@ export default defineComponent({
} }
}, },
getOptions(type: string): ChartOptions<'line'> { getOptions(type: string): LineOptions {
return { return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
@@ -264,7 +273,6 @@ export default defineComponent({
position: 'top', position: 'top',
align: 'center', align: 'center',
}, },
// @ts-expect-error: bug in chart.js types
customEventListener: { customEventListener: {
chartType: type, chartType: type,
instance: this, instance: this,
+2 -2
View File
@@ -8,6 +8,7 @@ import {
TimeScale, TimeScale,
type ChartOptions, type ChartOptions,
type ChartData, type ChartData,
type ChartDataset,
} from 'chart.js'; } from 'chart.js';
import {GET} from '../modules/fetch.ts'; import {GET} from '../modules/fetch.ts';
import {Bar} from 'vue-chartjs'; import {Bar} from 'vue-chartjs';
@@ -83,13 +84,12 @@ function toGraphData(data: DayData[]): ChartData<'bar'> {
return { return {
datasets: [ datasets: [
{ {
// @ts-expect-error -- bar chart expects one-dimensional data, but apparently x/y still works
data: data.map((i) => ({x: i.week, y: i.commits})), data: data.map((i) => ({x: i.week, y: i.commits})),
label: 'Commits', label: 'Commits',
backgroundColor: chartJsColors['commits'], backgroundColor: chartJsColors['commits'],
borderWidth: 0, borderWidth: 0,
tension: 0.3, tension: 0.3,
}, } as unknown as ChartDataset<'bar'>,
], ],
}; };
} }
-1
View File
@@ -41,7 +41,6 @@ export async function initCaptcha() {
// * the INPUT_NAME is a "const", it should not be changed. // * the INPUT_NAME is a "const", it should not be changed.
// * the "mCaptcha.default" is actually the "Widget". // * the "mCaptcha.default" is actually the "Widget".
// @ts-expect-error TS2540: Cannot assign to 'INPUT_NAME' because it is a read-only property.
mCaptcha.INPUT_NAME = 'm-captcha-response'; mCaptcha.INPUT_NAME = 'm-captcha-response';
const instanceURL = captchaEl.getAttribute('data-instance-url')!; const instanceURL = captchaEl.getAttribute('data-instance-url')!;
-4
View File
@@ -6,13 +6,9 @@ const {pageData} = window.config;
async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
const [{Cite, plugins}] = await Promise.all([ const [{Cite, plugins}] = await Promise.all([
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'), import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'),
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'),
]); ]);
const {citationFileContent} = pageData; const {citationFileContent} = pageData;
+1 -1
View File
@@ -35,7 +35,7 @@ const baseOptions: MonacoOpts = {
renderLineHighlight: 'all', renderLineHighlight: 'all',
renderLineHighlightOnlyWhenFocus: true, renderLineHighlightOnlyWhenFocus: true,
rulers: [], rulers: [],
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6, alwaysConsumeMouseWheel: false},
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
automaticLayout: true, automaticLayout: true,
wrappingIndent: 'none', wrappingIndent: 'none',
@@ -72,10 +72,9 @@ class Source {
const sourcesByUrl = new Map<string, Source | null>(); const sourcesByUrl = new Map<string, Source | null>();
const sourcesByPort = new Map<MessagePort, Source | null>(); const sourcesByPort = new Map<MessagePort, Source | null>();
// @ts-expect-error: typescript bug? (self as unknown as SharedWorkerGlobalScope).addEventListener('connect', (e: MessageEvent) => {
self.addEventListener('connect', (e: MessageEvent) => {
for (const port of e.ports) { for (const port of e.ports) {
port.addEventListener('message', (event) => { port.addEventListener('message', (event: MessageEvent) => {
if (!self.EventSource) { if (!self.EventSource) {
// some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope. // some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope.
// this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller, // this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller,
+1 -2
View File
@@ -56,8 +56,7 @@ function initRepoDiffConversationForm() {
const idx = newConversationHolder.getAttribute('data-idx'); const idx = newConversationHolder.getAttribute('data-idx');
form.closest('.conversation-holder')!.replaceWith(newConversationHolder); form.closest('.conversation-holder')!.replaceWith(newConversationHolder);
// @ts-expect-error -- prevent further usage of the form because it should have been replaced (form as any) = null; // prevent further usage of the form because it should have been replaced
form = null;
if (trLineType) { if (trLineType) {
// if there is a line-type for the "tr", it means the form is on the diff page // if there is a line-type for the "tr", it means the form is on the diff page
+1 -1
View File
@@ -201,7 +201,7 @@ async function pinMoveEnd(e: SortableEvent) {
} }
async function initIssuePinSort() { async function initIssuePinSort() {
const pinDiv = document.querySelector('#issue-pins'); const pinDiv = document.querySelector<HTMLElement>('#issue-pins');
if (pinDiv === null) return; if (pinDiv === null) return;
+2 -2
View File
@@ -38,7 +38,7 @@ async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<voi
async function initRepoProjectSortable(): Promise<void> { async function initRepoProjectSortable(): Promise<void> {
// the HTML layout is: #project-board.board > .project-column .cards > .issue-card // the HTML layout is: #project-board.board > .project-column .cards > .issue-card
const mainBoard = document.querySelector('#project-board')!; const mainBoard = document.querySelector<HTMLElement>('#project-board')!;
let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column'); let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
createSortable(mainBoard, { createSortable(mainBoard, {
group: 'project-column', group: 'project-column',
@@ -67,7 +67,7 @@ async function initRepoProjectSortable(): Promise<void> {
}); });
for (const boardColumn of boardColumns) { for (const boardColumn of boardColumns) {
const boardCardList = boardColumn.querySelector('.cards')!; const boardCardList = boardColumn.querySelector<HTMLElement>('.cards')!;
createSortable(boardCardList, { createSortable(boardCardList, {
group: 'shared', group: 'shared',
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
@@ -56,12 +56,11 @@ describe('Repository Branch Settings', () => {
vi.mocked(POST).mockResolvedValue({ok: true} as Response); vi.mocked(POST).mockResolvedValue({ok: true} as Response);
// Mock createSortable to capture and execute the onEnd callback // Mock createSortable to capture and execute the onEnd callback
vi.mocked(createSortable).mockImplementation(async (_el: Element, options: SortableOptions | undefined) => { vi.mocked(createSortable).mockImplementation(async (_el: HTMLElement, options: SortableOptions | undefined) => {
if (options?.onEnd) { if (options?.onEnd) {
options.onEnd(new Event('SortableEvent') as SortableEvent); options.onEnd(new Event('SortableEvent') as SortableEvent);
} }
// @ts-expect-error: mock is incomplete return {destroy: vi.fn()} as unknown as Sortable;
return {destroy: vi.fn()} as Sortable;
}); });
initRepoSettingsBranchesDrag(); initRepoSettingsBranchesDrag();
@@ -4,7 +4,7 @@ import {showErrorToast} from '../modules/toast.ts';
import {queryElemChildren} from '../utils/dom.ts'; import {queryElemChildren} from '../utils/dom.ts';
export function initRepoSettingsBranchesDrag() { export function initRepoSettingsBranchesDrag() {
const protectedBranchesList = document.querySelector('#protected-branches-list'); const protectedBranchesList = document.querySelector<HTMLElement>('#protected-branches-list');
if (!protectedBranchesList) return; if (!protectedBranchesList) return;
createSortable(protectedBranchesList, { createSortable(protectedBranchesList, {
+16 -15
View File
@@ -1,13 +1,11 @@
import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
import {html, htmlRaw} from '../utils/html.ts'; import {html, htmlRaw} from '../utils/html.ts';
import type {TributeCollection} from 'tributejs';
type TributeItem = Record<string, any>;
export async function attachTribute(element: HTMLElement) { export async function attachTribute(element: HTMLElement) {
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
const collections = [ const emojiCollection: TributeCollection<string> = { // emojis
{ // emojis
trigger: ':', trigger: ':',
requireLeadingSpace: true, requireLeadingSpace: true,
values: (query: string, cb: (matches: Array<string>) => void) => { values: (query: string, cb: (matches: Array<string>) => void) => {
@@ -20,18 +18,20 @@ export async function attachTribute(element: HTMLElement) {
} }
cb(matches); cb(matches);
}, },
lookup: (item: TributeItem) => item, lookup: (item) => item,
selectTemplate: (item: TributeItem) => { selectTemplate: (item) => {
if (item === undefined) return null; if (item === undefined) return '';
return emojiString(item.original); return emojiString(item.original) ?? '';
}, },
menuItemTemplate: (item: TributeItem) => { menuItemTemplate: (item) => {
return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`; return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`;
}, },
}, { // mentions };
const mentionCollection: TributeCollection<Record<string, any>> = {
values: window.config.mentionValues, values: window.config.mentionValues,
requireLeadingSpace: true, requireLeadingSpace: true,
menuItemTemplate: (item: TributeItem) => { menuItemTemplate: (item) => {
const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : ''; const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : '';
return html` return html`
<div class="tribute-item"> <div class="tribute-item">
@@ -41,11 +41,12 @@ export async function attachTribute(element: HTMLElement) {
</div> </div>
`; `;
}, },
}, };
];
// @ts-expect-error TS2351: This expression is not constructable (strange, why) const tribute = new Tribute({
const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); collection: [emojiCollection as TributeCollection<any>, mentionCollection],
noMatchTemplate: () => '',
});
tribute.attach(element); tribute.attach(element);
return tribute; return tribute;
} }
-30
View File
@@ -1,33 +1,3 @@
declare module '*.svg' {
const value: string;
export default value;
}
declare module '*.css' {
const value: string;
export default value;
}
declare module '*.vue' {
import type {DefineComponent} from 'vue';
const component: DefineComponent<unknown, unknown, any>;
export default component;
// Here we declare all exports from vue files so `tsc` or `tsgo` can work for
// non-vue files. To lint .vue files, `vue-tsc` must be used.
export function initDashboardRepoList(): void;
export function initRepositoryActionView(): void;
}
declare module 'htmx.org/dist/htmx.esm.js' {
const value = await import('htmx.org');
export default value;
}
declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
const value = await import('swagger-ui-dist');
export default value.SwaggerUIBundle;
}
interface JQuery { interface JQuery {
areYouSure: any, // jquery.are-you-sure areYouSure: any, // jquery.are-you-sure
fomanticExt: any; // fomantic extension fomanticExt: any; // fomantic extension
+1 -2
View File
@@ -3,12 +3,11 @@ import {queryElems} from '../utils/dom.ts';
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> { export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
queryElems(elMarkup, '.asciinema-player-container', async (el) => { queryElems(elMarkup, '.asciinema-player-container', async (el) => {
const [player] = await Promise.all([ const [player] = await Promise.all([
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'),
]); ]);
player.create(el.getAttribute('data-asciinema-player-src'), el, { player.create(el.getAttribute('data-asciinema-player-src')!, el, {
// poster (a preview frame) to display until the playback is started. // poster (a preview frame) to display until the playback is started.
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
poster: 'npt:1:0:0', poster: 'npt:1:0:0',
+3 -3
View File
@@ -1,9 +1,9 @@
import type {SortableOptions, SortableEvent} from 'sortablejs'; import type {SortableOptions, SortableEvent} from 'sortablejs';
import type SortableType from 'sortablejs'; import type SortableType from 'sortablejs';
export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> { export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> {
// @ts-expect-error: wrong type derived by typescript // type reassigned because typescript derives the wrong type from this import
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs'); const {Sortable} = (await import(/* webpackChunkName: "sortablejs" */'sortablejs') as unknown as {Sortable: typeof SortableType});
return new Sortable(el, { return new Sortable(el, {
animation: 150, animation: 150,
+2 -3
View File
@@ -4,17 +4,16 @@ try {
new Intl.NumberFormat('en', {style: 'unit', unit: 'minute'}).format(1); new Intl.NumberFormat('en', {style: 'unit', unit: 'minute'}).format(1);
} catch { } catch {
const intlNumberFormat = Intl.NumberFormat; const intlNumberFormat = Intl.NumberFormat;
// @ts-expect-error - polyfill is incomplete
Intl.NumberFormat = function(locales: string | string[], options: Intl.NumberFormatOptions) { Intl.NumberFormat = function(locales: string | string[], options: Intl.NumberFormatOptions) {
if (options.style === 'unit') { if (options.style === 'unit') {
return { return {
format(value: number | bigint | string) { format(value: number | bigint | string) {
return ` ${value} ${options.unit}`; return ` ${value} ${options.unit}`;
}, },
}; } as Intl.NumberFormat;
} }
return intlNumberFormat(locales, options); return intlNumberFormat(locales, options);
}; } as unknown as typeof Intl.NumberFormat;
} }
export function weakRefClass() { export function weakRefClass() {