0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-07 01:43:24 +02:00

Merge branch 'main' into feature/workflow-graph

This commit is contained in:
Semenets V. Pavel 2026-02-02 00:14:08 +03:00 committed by GitHub
commit b7c87025d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 1027 additions and 956 deletions

2
.github/labeler.yml vendored
View File

@ -84,9 +84,9 @@ docs-update-needed:
topic/code-linting:
- changed-files:
- any-glob-to-any-file:
- ".eslintrc.cjs"
- ".golangci.yml"
- ".markdownlint.yaml"
- ".spectral.yaml"
- ".yamllint.yaml"
- "eslint*.config.*"
- "stylelint.config.*"

View File

@ -85,6 +85,7 @@ jobs:
- "uv.lock"
docker:
- ".github/workflows/pull-docker-dryrun.yml"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"

View File

@ -14,24 +14,25 @@ jobs:
contents: read
container:
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
if: needs.files-changed.outputs.docker == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Build regular container image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: false
tags: gitea/gitea:linux-amd64
- name: Build rootless container image
uses: docker/build-push-action@v6
with:
context: .
push: false
platforms: linux/amd64,linux/arm64,linux/riscv64
file: Dockerfile.rootless
tags: gitea/gitea:linux-amd64

View File

@ -1,7 +1,10 @@
# Instructions for agents
- 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 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

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# 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
@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/.s6-svscan/* \
/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

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# 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
@ -33,7 +33,7 @@ COPY docker/rootless /tmp/local
RUN chmod 755 /tmp/local/usr/local/bin/* \
/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

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
.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 vue-tsc
$(NODE_VARS) pnpm exec knip --no-progress --cache
.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 vue-tsc
$(NODE_VARS) pnpm exec knip --no-progress --cache --fix
.PHONY: lint-css
lint-css: node_modules ## lint css files

View File

@ -15,6 +15,7 @@ import vue from 'eslint-plugin-vue';
import vueScopedCss from 'eslint-plugin-vue-scoped-css';
import wc from 'eslint-plugin-wc';
import {defineConfig, globalIgnores} from 'eslint/config';
import type {ESLint} from 'eslint';
const jsExts = ['js', 'mjs', 'cjs'] as const;
const tsExts = ['ts', 'mts', 'cts'] as const;
@ -62,8 +63,7 @@ export default defineConfig([
'@stylistic': stylistic,
'@typescript-eslint': typescriptPlugin.plugin,
'array-func': arrayFunc,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/203
'import-x': importPlugin,
'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203
regexp,
sonarjs,
unicorn,
@ -156,7 +156,7 @@ export default defineConfig([
'@typescript-eslint/adjacent-overload-signatures': [0],
'@typescript-eslint/array-type': [0],
'@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/class-literal-property-style': [0],
'@typescript-eslint/class-methods-use-this': [0],
@ -924,8 +924,7 @@ export default defineConfig([
},
extends: [
vue.configs['flat/recommended'],
// @ts-expect-error
vueScopedCss.configs['flat/recommended'],
vueScopedCss.configs['flat/recommended'] as any,
],
rules: {
'vue/attributes-order': [0],

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;

View File

@ -1034,6 +1034,20 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
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
type FindCommentsOptions struct {
db.ListOptions

View File

@ -102,6 +102,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
continue
}
comment.Review = re
comment.Issue = issue
}
comments[n] = comment
n++

View File

@ -4,6 +4,7 @@
package user
import (
"strconv"
"strings"
"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
func (u *User) IsGhost() bool {
if u == nil {
@ -41,10 +38,6 @@ const (
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.
func NewActionsUser() *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 {
return u != nil && u.ID == ActionsUserID
}
func GetSystemUserByName(name string) *User {
if IsGhostUserName(name) {
if strings.EqualFold(name, GhostUserName) {
return NewGhostUser()
}
if IsGiteaActionsUserName(name) {
if strings.EqualFold(name, ActionsUserName) {
return NewActionsUser()
}
return nil

View File

@ -16,14 +16,20 @@ func TestSystemUser(t *testing.T) {
assert.Equal(t, "Ghost", u.Name)
assert.Equal(t, "ghost", u.LowerName)
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)
require.NoError(t, err)
assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName)
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)
require.Error(t, err)

View File

@ -4,10 +4,28 @@
package analyze
import (
"path"
"strings"
"github.com/go-enry/go-enry/v2"
)
// IsVendor returns whether or not path is a vendor path.
func IsVendor(path string) bool {
return enry.IsVendor(path)
// IsVendor returns whether the path is a vendor path.
// It uses go-enry's IsVendor function but overrides its detection for certain
// 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
}

View File

@ -14,6 +14,7 @@ func TestIsVendor(t *testing.T) {
path string
want bool
}{
// Original go-enry test cases
{"cache/", true},
{"random/cache/", true},
{"cache", false},
@ -34,6 +35,14 @@ func TestIsVendor(t *testing.T) {
{"a/docs/_build/", true},
{"a/dasdocs/_build-vsdoc.js", true},
{"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 {
t.Run(tt.path, func(t *testing.T) {

View File

@ -72,7 +72,7 @@ type PullReviewComment struct {
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 {
Event ReviewStateType `json:"event"`
Body string `json:"body"`
@ -91,19 +91,19 @@ type CreatePullReviewComment struct {
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 {
Event ReviewStateType `json:"event"`
Body string `json:"body"`
}
// DismissPullReviewOptions are options to dismiss a pull review
// DismissPullReviewOptions are options to dismiss a pull request review
type DismissPullReviewOptions struct {
Message string `json:"message"`
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 {
Reviewers []string `json:"reviewers"`
TeamReviewers []string `json:"team_reviewers"`

View File

@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@10.28.1",
"packageManager": "pnpm@10.28.2",
"engines": {
"node": ">= 22.6.0",
"pnpm": ">= 10.0.0"
@ -28,7 +28,7 @@
"clippie": "4.1.9",
"compare-versions": "6.1.1",
"cropperjs": "1.6.2",
"css-loader": "7.1.2",
"css-loader": "7.1.3",
"dayjs": "1.11.19",
"dropzone": "6.0.0-beta.2",
"easymde": "2.20.0",
@ -37,7 +37,7 @@
"idiomorph": "0.7.4",
"jquery": "4.0.0",
"js-yaml": "4.1.1",
"katex": "0.16.27",
"katex": "0.16.28",
"mermaid": "11.12.2",
"mini-css-extract-plugin": "2.10.0",
"monaco-editor": "0.55.1",
@ -68,7 +68,7 @@
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.6.0",
"@eslint/json": "0.14.0",
"@playwright/test": "1.58.0",
"@playwright/test": "1.58.1",
"@stylistic/eslint-plugin": "5.7.1",
"@stylistic/stylelint-plugin": "5.0.1",
"@types/codemirror": "5.60.17",
@ -82,7 +82,7 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.53.1",
"@typescript-eslint/parser": "8.54.0",
"@vitejs/plugin-vue": "6.0.3",
"@vitest/eslint-plugin": "1.6.6",
"eslint": "9.39.2",
@ -90,34 +90,33 @@
"eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0",
"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-sonarjs": "3.0.5",
"eslint-plugin-sonarjs": "3.0.6",
"eslint-plugin-unicorn": "62.0.0",
"eslint-plugin-vue": "10.7.0",
"eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.0.2",
"globals": "17.1.0",
"happy-dom": "20.3.7",
"globals": "17.2.0",
"happy-dom": "20.4.0",
"jiti": "2.6.1",
"knip": "5.82.1",
"markdownlint-cli": "0.47.0",
"material-icon-theme": "5.31.0",
"nolyfill": "1.0.44",
"postcss-html": "1.8.1",
"spectral-cli-bundle": "1.0.3",
"stylelint": "17.0.0",
"stylelint": "17.1.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-value-no-unknown-custom-properties": "6.1.1",
"svgo": "4.0.0",
"typescript": "5.9.3",
"typescript-eslint": "8.53.1",
"updates": "17.0.8",
"vite-string-plugin": "1.5.0",
"typescript-eslint": "8.54.0",
"updates": "17.0.9",
"vite-string-plugin": "2.0.0",
"vitest": "4.0.18",
"vue-tsc": "3.2.3"
"vue-tsc": "3.2.4"
},
"browserslist": [
"defaults"

970
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -188,8 +188,7 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner
ctx.Repo.Repository = repo
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID {
taskID := ctx.Data["ActionsTaskID"].(int64)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.APIErrorInternal(err)
@ -349,11 +348,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
// Contexter middleware already checks token for user sign in process.
func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// If actions token is present
if true == ctx.Data["IsActionsToken"] {
return
}
// if a real user is signed in, or the user is from a Actions task, we are good
if ctx.IsSigned {
return
}
@ -1353,6 +1348,8 @@ func Routes() *web.Router {
m.Combo("").Get(repo.ListPullRequests).
Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
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.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)

View File

@ -445,7 +445,7 @@ func GetIssueComment(ctx *context.APIContext) {
// "404":
// "$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 issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
@ -455,15 +455,6 @@ func GetIssueComment(ctx *context.APIContext) {
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) {
ctx.APIErrorNotFound()
return
@ -579,7 +570,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
}
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 issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
@ -589,16 +580,6 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
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)) {
ctx.Status(http.StatusForbidden)
return
@ -698,7 +679,7 @@ func DeleteIssueCommentDeprecated(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 issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
@ -708,16 +689,6 @@ func deleteIssueComment(ctx *context.APIContext) {
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)) {
ctx.Status(http.StatusForbidden)
return

View File

@ -140,6 +140,7 @@ func Migrate(ctx *context.APIContext) {
}
opts := migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,

View File

@ -208,6 +208,126 @@ func GetPullReviewComments(ctx *context.APIContext) {
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
func DeletePullReview(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview

View File

@ -22,6 +22,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"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/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
@ -166,7 +167,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
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)
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
@ -197,8 +198,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
accessMode = perm.AccessModeRead
}
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.ServerError("GetActionsUserRepoPermission", err)

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)
if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["LoginMethod"] = ActionTokenMethodName
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.NewActionsUser(), nil
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
if !setting.Service.EnableBasicAuth {

View File

@ -6,6 +6,7 @@ package auth
import (
"context"
"errors"
"net/http"
"strings"
"time"
@ -17,14 +18,12 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/actions"
"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
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
@ -106,18 +105,16 @@ func parseToken(req *http.Request) (string, bool) {
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
// set 'ApiTokenScope' to the scope of the access token
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
// set 'ApiTokenScope' to the scope of the access token (TODO: this behavior should be fixed, don't set ctx.Data)
func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataStore) (*user_model.User, error) {
// Let's see if token is valid.
if strings.Contains(tokenSHA, ".") {
// First attempt to decode an actions JWT, returning the actions user
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
if CheckTaskIsRunning(ctx, taskID) {
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = taskID
return user_model.ActionsUserID
return user_model.NewActionsUserWithTaskID(taskID), nil
}
}
@ -127,33 +124,27 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = accessTokenScope
}
return uid
return user_model.GetUserByID(ctx, uid)
}
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
if err != nil {
if auth_model.IsErrAccessTokenNotExist(err) {
// check task token
task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA)
if err == nil && task != nil {
if task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA); err == nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.ActionsUserID
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err)
}
return 0
return nil, err
}
t.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(ctx, t); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["IsApiToken"] = true
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
@ -173,21 +164,9 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
return nil, nil
}
id := o.userIDFromToken(req.Context(), token, store)
if id <= 0 && id != -2 { // -2 means actions, so we need to allow it.
return nil, user_model.ErrUserNotExist{}
user, err := o.userFromToken(req.Context(), token, store)
if err != nil && !errors.Is(err, util.ErrNotExist) {
log.Error("userFromToken: %v", err) // the callers might ignore the error, so log it here
}
log.Trace("OAuth2 Authorization: Found token for user[%d]", id)
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
return user, err
}

View File

@ -12,23 +12,26 @@ import (
"code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserIDFromToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("Actions JWT", func(t *testing.T) {
const RunningTaskID = 47
const RunningTaskID int64 = 47
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
assert.NoError(t, err)
ds := make(reqctx.ContextData)
o := OAuth2{}
uid := o.userIDFromToken(t.Context(), token, ds)
assert.Equal(t, user_model.ActionsUserID, uid)
assert.Equal(t, true, ds["IsActionsToken"])
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
u, err := o.userFromToken(t.Context(), token, ds)
require.NoError(t, err)
assert.Equal(t, user_model.ActionsUserID, u.ID)
taskID, ok := user_model.GetActionsUserTaskID(u)
assert.True(t, ok)
assert.Equal(t, RunningTaskID, taskID)
})
}

View File

@ -92,34 +92,40 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
for _, lines := range review.CodeComments {
for _, comments := range lines {
for _, comment := range comments {
apiComment := &api.PullReviewComment{
ID: comment.ID,
Body: comment.Content,
Poster: ToUser(ctx, comment.Poster, doer),
Resolver: ToUser(ctx, comment.ResolveDoer, doer),
ReviewID: review.ID,
Created: comment.CreatedUnix.AsTime(),
Updated: comment.UpdatedUnix.AsTime(),
Path: comment.TreePath,
CommitID: comment.CommitSHA,
OrigCommitID: comment.OldRef,
DiffHunk: patch2diff(comment.Patch),
HTMLURL: comment.HTMLURL(ctx),
HTMLPullURL: review.Issue.HTMLURL(ctx),
}
if comment.Line < 0 {
apiComment.OldLineNum = comment.UnsignedLine()
} else {
apiComment.LineNum = comment.UnsignedLine()
}
apiComments = append(apiComments, apiComment)
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{
ID: comment.ID,
Body: comment.Content,
Poster: ToUser(ctx, comment.Poster, doer),
Resolver: ToUser(ctx, comment.ResolveDoer, doer),
ReviewID: comment.ReviewID,
Created: comment.CreatedUnix.AsTime(),
Updated: comment.UpdatedUnix.AsTime(),
Path: comment.TreePath,
CommitID: comment.CommitSHA,
OrigCommitID: comment.OldRef,
DiffHunk: patch2diff(comment.Patch),
HTMLURL: comment.HTMLURL(ctx),
HTMLPullURL: comment.Issue.HTMLURL(ctx),
}
if comment.Line < 0 {
apiComment.OldLineNum = comment.UnsignedLine()
} else {
apiComment.LineNum = comment.UnsignedLine()
}
return apiComment
}
func patch2diff(patch string) string {
split := strings.Split(patch, "\n@@")
if len(split) == 2 {

View File

@ -541,8 +541,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
accessMode = perm_model.AccessModeWrite
}
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID)
if err != nil {
log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err)

View File

@ -131,8 +131,8 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1)
}
if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil {
log.Error("create respotiry notice failed: ", err2)
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 repository notice failed: ", err2)
}
return nil, err
}

View File

@ -202,7 +202,7 @@ func pruneBrokenReferences(ctx context.Context, m *repo_model.Mirror, gitRepo gi
stdoutMessage := util.SanitizeCredentialURLs(stdout)
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 {
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 err != nil {
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 {
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 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)
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 {
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
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)
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 {
log.Error("CreateRepositoryNotice: %v", err)
}

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
defer func() {
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 {
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
}

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)
if err != nil {
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 {
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
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)
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 {
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)
if err := DeleteRepositoryDirectly(ctx, repo.ID); err != nil {
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)
}
}
@ -191,7 +191,7 @@ func ReinitMissingRepositories(ctx context.Context) error {
log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID)
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)
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)
}
}

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
defer func() {
if err != nil {
// we can not use the ctx because it maybe canceled or timeout
cleanupRepository(repo.ID)
// we can not use `ctx` because it may be canceled or timed out
cleanupRepository(repo)
}
}()
@ -461,11 +461,11 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r
return nil
}
func cleanupRepository(repoID int64) {
if errDelete := DeleteRepositoryDirectly(graceful.GetManager().ShutdownContext(), repoID); errDelete != nil {
func cleanupRepository(repo *repo_model.Repository) {
ctx := graceful.GetManager().ShutdownContext()
if errDelete := DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil {
log.Error("cleanupRepository failed: %v", errDelete)
// add system notice
if err := system_model.CreateRepositoryNotice("DeleteRepositoryDirectly failed when cleanup repository: %v", errDelete); err != nil {
if err := system_model.CreateRepositoryNotice("DeleteRepositoryDirectly failed when cleanup repository (%s)", repo.FullName(), errDelete); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
}

View File

@ -309,7 +309,7 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
// Remove repository files.
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 {
log.Error("CreateRepositoryNotice: %v", err)
}
@ -317,7 +317,7 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
// Remove wiki files if it exists.
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
if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)

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
defer func() {
if err != nil {
// we can not use the ctx because it maybe canceled or timeout
cleanupRepository(repo.ID)
// we can not use `ctx` because it may be canceled or timed out
cleanupRepository(repo)
}
}()

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
defer func() {
if err != nil {
// we can not use the ctx because it maybe canceled or timeout
cleanupRepository(generateRepo.ID)
// we can not use `ctx` because it may be canceled or timed out
cleanupRepository(generateRepo)
}
}()

View File

@ -317,7 +317,7 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
color = greenColor
if withSender {
if user_model.IsGiteaActionsUserName(p.Sender.UserName) {
if user_model.GetSystemUserByName(p.Sender.UserName) != nil {
text += " by " + p.Sender.FullName
} else {
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)

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 {
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
if err = system_model.CreateNotice(graceful.GetManager().ShutdownContext(), system_model.NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)

View File

@ -1,3 +1,4 @@
// @ts-check
// TODO: Move to .ts after https://github.com/stylelint/stylelint/issues/8893 is fixed
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)),
];
/** @type {import('stylelint').Config} */
export default {
extends: 'stylelint-config-recommended',
reportUnscopedDisables: true,
@ -57,14 +59,14 @@ export default {
'@stylistic/block-opening-brace-space-before': 'always',
'@stylistic/color-hex-case': 'lower',
'@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-before': null,
'@stylistic/declaration-block-semicolon-space-after': null,
'@stylistic/declaration-block-semicolon-space-before': 'never',
'@stylistic/declaration-block-trailing-semicolon': 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/function-comma-newline-after': null,
'@stylistic/function-comma-newline-before': null,
@ -101,7 +103,7 @@ export default {
'@stylistic/selector-attribute-operator-space-before': null,
'@stylistic/selector-combinator-space-after': 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-before': null,
'@stylistic/selector-list-comma-space-after': 'always-single-line',

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": {
"get": {
"produces": [
@ -23547,7 +23647,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreatePullReviewOptions": {
"description": "CreatePullReviewOptions are options to create a pull review",
"description": "CreatePullReviewOptions are options to create a pull request review",
"type": "object",
"properties": {
"body": {
@ -24144,7 +24244,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"DismissPullReviewOptions": {
"description": "DismissPullReviewOptions are options to dismiss a pull review",
"description": "DismissPullReviewOptions are options to dismiss a pull request review",
"type": "object",
"properties": {
"message": {
@ -27656,7 +27756,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"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",
"properties": {
"reviewers": {
@ -28400,7 +28500,7 @@
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"SubmitPullReviewOptions": {
"description": "SubmitPullReviewOptions are options to submit a pending pull review",
"description": "SubmitPullReviewOptions are options to submit a pending pull request review",
"type": "object",
"properties": {
"body": {

View File

@ -15,9 +15,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@ -362,6 +364,79 @@ func TestAPIPullReviewRequest(t *testing.T) {
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) {
// This test against issue https://github.com/go-gitea/gitea/issues/28542
// where old reviews surface after a review request got dismissed.

View File

@ -13,7 +13,7 @@
"target": "es2020",
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"],
"lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext", "webworker"],
"allowImportingTsExtensions": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,

87
types.d.ts vendored
View File

@ -2,18 +2,105 @@ declare module '@techknowlogick/license-checker-webpack-plugin' {
const plugin: any;
export = plugin;
}
declare module 'eslint-plugin-no-use-extend-native' {
import type {Eslint} from 'eslint';
const plugin: Eslint.Plugin;
export = plugin;
}
declare module 'eslint-plugin-array-func' {
import type {Eslint} from 'eslint';
const plugin: Eslint.Plugin;
export = plugin;
}
declare module 'eslint-plugin-github' {
import type {Eslint} from 'eslint';
const plugin: Eslint.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;
};
});
}
}

View File

@ -5,5 +5,6 @@ export default {
'@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
'tailwindcss', // need to migrate
'@eslint/json', // needs eslint 10
],
} satisfies Config;

View File

@ -46,7 +46,7 @@
}
.repository.wiki .wiki-content-toc ul ul {
border-left: 1px var(--color-secondary);
border-left: 1px var(--color-secondary);
border-left-style: dashed;
}

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
// 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.
for (const e of window._globalHandlerErrors || []) {
for (const e of (window._globalHandlerErrors as Iterable<ErrorEvent & PromiseRejectionEvent>) || []) {
processWindowErrorEvent(e);
}
// then, change _globalHandlerErrors to an object with push method, to process further error
// 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)};
window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)} as any;
}
initGlobalErrorHandler();

View File

@ -14,6 +14,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"
type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
type StepContainerElement = HTMLElement & {_stepLogsActiveContainer?: HTMLElement}
type LogLine = {
index: number;
timestamp: number;
@ -226,19 +228,18 @@ export default defineComponent({
},
// get the job step logs container ('.job-step-logs')
getJobStepLogsContainer(stepIndex: number): HTMLElement {
getJobStepLogsContainer(stepIndex: number): StepContainerElement {
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`
getActiveLogsContainer(stepIndex: number): HTMLElement {
getActiveLogsContainer(stepIndex: number): StepContainerElement {
const el = this.getJobStepLogsContainer(stepIndex);
// @ts-expect-error - _stepLogsActiveContainer is a custom property
return el._stepLogsActiveContainer ?? el;
},
// begin a log group
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'},
this.createLogLine(stepIndex, startTime, {
index: line.index,
@ -400,7 +401,7 @@ export default defineComponent({
}
// 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++) {
if (!autoScrollStepIndexes.get(stepIndex)) continue;
autoScrollJobStepElement = this.getJobStepLogsContainer(stepIndex);

View File

@ -1,5 +1,4 @@
<script lang="ts" setup>
// @ts-expect-error - module exports no types
import {VueBarGraph} from 'vue-bar-graph';
import {computed, onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue';

View File

@ -155,9 +155,8 @@ export default defineComponent({
return -1;
},
getActiveItem() {
const el = this.$refs[`listItem${this.activeItemIndex}`];
// @ts-expect-error - el is unknown type
return (el && el.length) ? el[0] : null;
const el = this.$refs[`listItem${this.activeItemIndex}`] as Array<HTMLDivElement>;
return el?.length ? el[0] : null;
},
keydown(e: KeyboardEvent) {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
@ -174,7 +173,7 @@ export default defineComponent({
return;
}
this.activeItemIndex = nextIndex;
this.getActiveItem().scrollIntoView({block: 'nearest'});
this.getActiveItem()!.scrollIntoView({block: 'nearest'});
} else if (e.key === 'Enter') {
e.preventDefault();
this.getActiveItem()?.click();

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.borderColor = chartJsColors.border;
@ -251,7 +260,7 @@ export default defineComponent({
}
},
getOptions(type: string): ChartOptions<'line'> {
getOptions(type: string): LineOptions {
return {
responsive: true,
maintainAspectRatio: false,
@ -264,7 +273,6 @@ export default defineComponent({
position: 'top',
align: 'center',
},
// @ts-expect-error: bug in chart.js types
customEventListener: {
chartType: type,
instance: this,

View File

@ -8,6 +8,7 @@ import {
TimeScale,
type ChartOptions,
type ChartData,
type ChartDataset,
} from 'chart.js';
import {GET} from '../modules/fetch.ts';
import {Bar} from 'vue-chartjs';
@ -83,13 +84,12 @@ function toGraphData(data: DayData[]): ChartData<'bar'> {
return {
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})),
label: 'Commits',
backgroundColor: chartJsColors['commits'],
borderWidth: 0,
tension: 0.3,
},
} as unknown as ChartDataset<'bar'>,
],
};
}

View File

@ -41,7 +41,6 @@ export async function initCaptcha() {
// * the INPUT_NAME is a "const", it should not be changed.
// * 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';
const instanceURL = captchaEl.getAttribute('data-instance-url')!;

View File

@ -6,13 +6,9 @@ const {pageData} = window.config;
async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
const [{Cite, plugins}] = await Promise.all([
// @ts-expect-error: module exports no types
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'),
// @ts-expect-error: module exports no types
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'),
]);
const {citationFileContent} = pageData;

View File

@ -35,7 +35,7 @@ const baseOptions: MonacoOpts = {
renderLineHighlight: 'all',
renderLineHighlightOnlyWhenFocus: true,
rulers: [],
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6},
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6, alwaysConsumeMouseWheel: false},
scrollBeyondLastLine: false,
automaticLayout: true,
wrappingIndent: 'none',

View File

@ -72,10 +72,9 @@ class Source {
const sourcesByUrl = new Map<string, Source | null>();
const sourcesByPort = new Map<MessagePort, Source | null>();
// @ts-expect-error: typescript bug?
self.addEventListener('connect', (e: MessageEvent) => {
(self as unknown as SharedWorkerGlobalScope).addEventListener('connect', (e: MessageEvent) => {
for (const port of e.ports) {
port.addEventListener('message', (event) => {
port.addEventListener('message', (event: MessageEvent) => {
if (!self.EventSource) {
// 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,

View File

@ -56,8 +56,7 @@ function initRepoDiffConversationForm() {
const idx = newConversationHolder.getAttribute('data-idx');
form.closest('.conversation-holder')!.replaceWith(newConversationHolder);
// @ts-expect-error -- prevent further usage of the form because it should have been replaced
form = null;
(form as any) = null; // prevent further usage of the form because it should have been replaced
if (trLineType) {
// if there is a line-type for the "tr", it means the form is on the diff page

View File

@ -201,7 +201,7 @@ async function pinMoveEnd(e: SortableEvent) {
}
async function initIssuePinSort() {
const pinDiv = document.querySelector('#issue-pins');
const pinDiv = document.querySelector<HTMLElement>('#issue-pins');
if (pinDiv === null) return;

View File

@ -38,7 +38,7 @@ async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<voi
async function initRepoProjectSortable(): Promise<void> {
// 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');
createSortable(mainBoard, {
group: 'project-column',
@ -67,7 +67,7 @@ async function initRepoProjectSortable(): Promise<void> {
});
for (const boardColumn of boardColumns) {
const boardCardList = boardColumn.querySelector('.cards')!;
const boardCardList = boardColumn.querySelector<HTMLElement>('.cards')!;
createSortable(boardCardList, {
group: 'shared',
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises

View File

@ -56,12 +56,11 @@ describe('Repository Branch Settings', () => {
vi.mocked(POST).mockResolvedValue({ok: true} as Response);
// 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) {
options.onEnd(new Event('SortableEvent') as SortableEvent);
}
// @ts-expect-error: mock is incomplete
return {destroy: vi.fn()} as Sortable;
return {destroy: vi.fn()} as unknown as Sortable;
});
initRepoSettingsBranchesDrag();

View File

@ -4,7 +4,7 @@ import {showErrorToast} from '../modules/toast.ts';
import {queryElemChildren} from '../utils/dom.ts';
export function initRepoSettingsBranchesDrag() {
const protectedBranchesList = document.querySelector('#protected-branches-list');
const protectedBranchesList = document.querySelector<HTMLElement>('#protected-branches-list');
if (!protectedBranchesList) return;
createSortable(protectedBranchesList, {

View File

@ -1,51 +1,52 @@
import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
import {html, htmlRaw} from '../utils/html.ts';
type TributeItem = Record<string, any>;
import type {TributeCollection} from 'tributejs';
export async function attachTribute(element: HTMLElement) {
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
const collections = [
{ // emojis
trigger: ':',
requireLeadingSpace: true,
values: (query: string, cb: (matches: Array<string>) => void) => {
const matches = [];
for (const name of emojiKeys) {
if (name.includes(query)) {
matches.push(name);
if (matches.length > 5) break;
}
const emojiCollection: TributeCollection<string> = { // emojis
trigger: ':',
requireLeadingSpace: true,
values: (query: string, cb: (matches: Array<string>) => void) => {
const matches = [];
for (const name of emojiKeys) {
if (name.includes(query)) {
matches.push(name);
if (matches.length > 5) break;
}
cb(matches);
},
lookup: (item: TributeItem) => item,
selectTemplate: (item: TributeItem) => {
if (item === undefined) return null;
return emojiString(item.original);
},
menuItemTemplate: (item: TributeItem) => {
return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`;
},
}, { // mentions
values: window.config.mentionValues,
requireLeadingSpace: true,
menuItemTemplate: (item: TributeItem) => {
const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : '';
return html`
<div class="tribute-item">
<img alt src="${item.original.avatar}" width="21" height="21"/>
<span class="name">${item.original.name}</span>
${htmlRaw(fullNameHtml)}
</div>
`;
},
}
cb(matches);
},
];
lookup: (item) => item,
selectTemplate: (item) => {
if (item === undefined) return '';
return emojiString(item.original) ?? '';
},
menuItemTemplate: (item) => {
return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`;
},
};
// @ts-expect-error TS2351: This expression is not constructable (strange, why)
const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
const mentionCollection: TributeCollection<Record<string, any>> = {
values: window.config.mentionValues,
requireLeadingSpace: true,
menuItemTemplate: (item) => {
const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : '';
return html`
<div class="tribute-item">
<img alt src="${item.original.avatar}" width="21" height="21"/>
<span class="name">${item.original.name}</span>
${htmlRaw(fullNameHtml)}
</div>
`;
},
};
const tribute = new Tribute({
collection: [emojiCollection as TributeCollection<any>, mentionCollection],
noMatchTemplate: () => '',
});
tribute.attach(element);
return tribute;
}

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 {
areYouSure: any, // jquery.are-you-sure
fomanticExt: any; // fomantic extension

View File

@ -3,12 +3,11 @@ import {queryElems} from '../utils/dom.ts';
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
queryElems(elMarkup, '.asciinema-player-container', async (el) => {
const [player] = await Promise.all([
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
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.
// 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',

View File

@ -1,9 +1,9 @@
import type {SortableOptions, SortableEvent} from 'sortablejs';
import type SortableType from 'sortablejs';
export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> {
// @ts-expect-error: wrong type derived by typescript
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> {
// type reassigned because typescript derives the wrong type from this import
const {Sortable} = (await import(/* webpackChunkName: "sortablejs" */'sortablejs') as unknown as {Sortable: typeof SortableType});
return new Sortable(el, {
animation: 150,

View File

@ -4,17 +4,16 @@ try {
new Intl.NumberFormat('en', {style: 'unit', unit: 'minute'}).format(1);
} catch {
const intlNumberFormat = Intl.NumberFormat;
// @ts-expect-error - polyfill is incomplete
Intl.NumberFormat = function(locales: string | string[], options: Intl.NumberFormatOptions) {
if (options.style === 'unit') {
return {
format(value: number | bigint | string) {
return ` ${value} ${options.unit}`;
},
};
} as Intl.NumberFormat;
}
return intlNumberFormat(locales, options);
};
} as unknown as typeof Intl.NumberFormat;
}
export function weakRefClass() {