Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab01fd0abe | |||
| dfff777d04 | |||
|
|
65d93d819b | ||
|
|
288d1f526a | ||
|
|
7883f6dde9 | ||
|
|
c2dea22926 | ||
|
|
584d8ef75f | ||
|
|
9d96039027 | ||
|
|
072de7d8cd | ||
|
|
e377da989f | ||
|
|
7ad9bf4523 | ||
|
|
7292ae1ed5 |
+1
-1
@@ -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.*"
|
||||
|
||||
@@ -85,6 +85,7 @@ jobs:
|
||||
- "uv.lock"
|
||||
|
||||
docker:
|
||||
- ".github/workflows/pull-docker-dryrun.yml"
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
- "docker/**"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-5
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -102,6 +102,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
continue
|
||||
}
|
||||
comment.Review = re
|
||||
comment.Issue = issue
|
||||
}
|
||||
comments[n] = comment
|
||||
n++
|
||||
|
||||
@@ -43,13 +43,15 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
||||
|
||||
existing := &PackageBlob{}
|
||||
|
||||
has, err := e.Where(builder.Eq{
|
||||
hashCond := builder.Eq{
|
||||
"size": pb.Size,
|
||||
"hash_md5": pb.HashMD5,
|
||||
"hash_sha1": pb.HashSHA1,
|
||||
"hash_sha256": pb.HashSHA256,
|
||||
"hash_sha512": pb.HashSHA512,
|
||||
}).Get(existing)
|
||||
}
|
||||
|
||||
has, err := e.Where(hashCond).Get(existing)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -57,6 +59,11 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
||||
return existing, true, 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 pb, false, nil
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -217,6 +217,7 @@ func LoadSettings() {
|
||||
loadProjectFrom(CfgProvider)
|
||||
loadMimeTypeMapFrom(CfgProvider)
|
||||
loadFederationFrom(CfgProvider)
|
||||
loadSourcegraphFrom(CfgProvider)
|
||||
}
|
||||
|
||||
// LoadSettingsForInstall initializes the settings for install
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Sourcegraph settings for code intelligence integration
|
||||
var Sourcegraph = struct {
|
||||
Enabled bool
|
||||
URL string
|
||||
AccessToken string
|
||||
SkipTLSVerify bool
|
||||
Timeout int // request timeout in seconds
|
||||
CacheTTL int // cache time-to-live in seconds
|
||||
SyncOnPush bool
|
||||
}{
|
||||
Enabled: false,
|
||||
Timeout: 5,
|
||||
CacheTTL: 300,
|
||||
SyncOnPush: true,
|
||||
SkipTLSVerify: false,
|
||||
}
|
||||
|
||||
func loadSourcegraphFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("sourcegraph")
|
||||
Sourcegraph.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
Sourcegraph.URL = sec.Key("URL").MustString("")
|
||||
Sourcegraph.AccessToken = sec.Key("ACCESS_TOKEN").MustString("")
|
||||
Sourcegraph.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool(false)
|
||||
Sourcegraph.Timeout = sec.Key("TIMEOUT").MustInt(5)
|
||||
Sourcegraph.CacheTTL = sec.Key("CACHE_TTL").MustInt(300)
|
||||
Sourcegraph.SyncOnPush = sec.Key("SYNC_ON_PUSH").MustBool(true)
|
||||
|
||||
// Validate: if enabled, URL must be set
|
||||
if Sourcegraph.Enabled && Sourcegraph.URL == "" {
|
||||
Sourcegraph.Enabled = false
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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.use_template": "Úsáid an teimpléad seo",
|
||||
"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_tar": "Íoslódáil TAR.GZ",
|
||||
"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.mostforks": "An líon forcanna is mó",
|
||||
"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_close": "Dún",
|
||||
"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.reject": "athruithe iarrtha %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.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",
|
||||
@@ -1793,6 +1796,7 @@
|
||||
"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.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_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.",
|
||||
@@ -1847,7 +1851,8 @@
|
||||
"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_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_requested": "Riachtanach",
|
||||
"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.show_more": "Taispeáin Tuilleadh",
|
||||
"repo.diff.load": "Difríocht Luchtaigh",
|
||||
"repo.diff.generated": "a ghintear",
|
||||
"repo.diff.vendored": "curtha ar fáil",
|
||||
"repo.diff.generated": "Gineadh",
|
||||
"repo.diff.vendored": "Díoltóir",
|
||||
"repo.diff.comment.add_line_comment": "Cuir trácht líne leis",
|
||||
"repo.diff.comment.placeholder": "Fág trácht",
|
||||
"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.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_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_no_divergence": "Mar an gcéanna le brainse %[1]s",
|
||||
"repo.tag.create_tag": "Cruthaigh clib %s",
|
||||
@@ -3281,8 +3286,6 @@
|
||||
"admin.config.git_gc_args": "Argóintí GC",
|
||||
"admin.config.git_migrate_timeout": "Teorainn Ama Imirce",
|
||||
"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.log_config": "Cumraíocht Logáil",
|
||||
"admin.config.logger_name_fmt": "Logálaí: %s",
|
||||
@@ -3724,8 +3727,8 @@
|
||||
"projects.exit_fullscreen": "Scoir Lánscáileáin",
|
||||
"git.filemode.changed_filemode": "%[1]s → %[2]s",
|
||||
"git.filemode.directory": "Eolaire",
|
||||
"git.filemode.normal_file": "Comhad gnáth",
|
||||
"git.filemode.executable_file": "Comhad infheidhmithe",
|
||||
"git.filemode.normal_file": "Rialta",
|
||||
"git.filemode.executable_file": "Inrite",
|
||||
"git.filemode.symbolic_link": "Nasc siombalach",
|
||||
"git.filemode.submodule": "Fo-mhodúl"
|
||||
}
|
||||
|
||||
+15
-16
@@ -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"
|
||||
|
||||
Generated
+346
-624
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,18 @@ import (
|
||||
|
||||
// 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
|
||||
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)
|
||||
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
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
@@ -67,7 +76,7 @@ func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader
|
||||
return createFileForBlob(ctx, uploadVersion, pb)
|
||||
})
|
||||
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 {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -1446,6 +1443,11 @@ func Routes() *web.Router {
|
||||
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
||||
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
||||
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
|
||||
m.Group("/sourcegraph", func() {
|
||||
m.Get("/hover", repo.SourcegraphHover)
|
||||
m.Get("/definition", repo.SourcegraphDefinition)
|
||||
m.Get("/references", repo.SourcegraphReferences)
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
|
||||
m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
|
||||
m.Group("/avatar", func() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -140,6 +140,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
opts := migrations.MigrateOptions{
|
||||
OriginalURL: form.CloneAddr,
|
||||
CloneAddr: remoteAddr,
|
||||
RepoName: form.RepoName,
|
||||
Description: form.Description,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
sourcegraph_service "code.gitea.io/gitea/services/sourcegraph"
|
||||
)
|
||||
|
||||
// sourcegraphRepoName returns the full repository name for Sourcegraph,
|
||||
// which includes the server domain prefix (e.g., "git.example.com/owner/repo")
|
||||
func sourcegraphRepoName(ctx *context.APIContext) string {
|
||||
return setting.Domain + "/" + ctx.Repo.Repository.FullName()
|
||||
}
|
||||
|
||||
// SourcegraphHover returns hover information at a position
|
||||
func SourcegraphHover(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/sourcegraph/hover repository repoSourcegraphHover
|
||||
// ---
|
||||
// summary: Get code intelligence hover info
|
||||
// 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: path
|
||||
// in: query
|
||||
// description: file path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: line
|
||||
// in: query
|
||||
// description: line number (0-indexed)
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: character
|
||||
// in: query
|
||||
// description: character position (0-indexed)
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: git ref (commit SHA, branch, tag)
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: hover information
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "503":
|
||||
// description: Sourcegraph integration not available
|
||||
|
||||
if !setting.Sourcegraph.Enabled {
|
||||
ctx.APIError(http.StatusServiceUnavailable, "Sourcegraph integration is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
path := ctx.FormString("path")
|
||||
line := ctx.FormInt("line")
|
||||
char := ctx.FormInt("character")
|
||||
ref := ctx.FormString("ref")
|
||||
|
||||
if path == "" || ref == "" {
|
||||
ctx.APIError(http.StatusBadRequest, "path and ref are required")
|
||||
return
|
||||
}
|
||||
|
||||
client := sourcegraph_service.GetClient()
|
||||
if client == nil {
|
||||
ctx.APIError(http.StatusServiceUnavailable, "Sourcegraph client not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := client.Hover(ctx, sourcegraphRepoName(ctx), ref, path, line, char)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadGateway, err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
ctx.JSON(http.StatusOK, map[string]any{})
|
||||
return
|
||||
}
|
||||
|
||||
// Render markdown content to HTML
|
||||
if result.Contents != "" {
|
||||
rendered, err := markdown.RenderString(markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), result.Contents)
|
||||
if err == nil {
|
||||
result.Contents = string(rendered)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// SourcegraphDefinition returns definition locations for a symbol
|
||||
func SourcegraphDefinition(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/sourcegraph/definition repository repoSourcegraphDefinition
|
||||
// ---
|
||||
// summary: Get code intelligence definition locations
|
||||
// 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: path
|
||||
// in: query
|
||||
// description: file path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: line
|
||||
// in: query
|
||||
// description: line number (0-indexed)
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: character
|
||||
// in: query
|
||||
// description: character position (0-indexed)
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: git ref (commit SHA, branch, tag)
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: definition locations
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "503":
|
||||
// description: Sourcegraph integration not available
|
||||
|
||||
if !setting.Sourcegraph.Enabled {
|
||||
ctx.APIError(http.StatusServiceUnavailable, "Sourcegraph integration is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
path := ctx.FormString("path")
|
||||
line := ctx.FormInt("line")
|
||||
char := ctx.FormInt("character")
|
||||
ref := ctx.FormString("ref")
|
||||
|
||||
if path == "" || ref == "" {
|
||||
ctx.APIError(http.StatusBadRequest, "path and ref are required")
|
||||
return
|
||||
}
|
||||
|
||||
client := sourcegraph_service.GetClient()
|
||||
if client == nil {
|
||||
ctx.APIError(http.StatusServiceUnavailable, "Sourcegraph client not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := client.Definition(ctx, sourcegraphRepoName(ctx), ref, path, line, char)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadGateway, err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = []sourcegraph_service.Location{}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// SourcegraphReferences returns reference locations for a symbol
|
||||
func SourcegraphReferences(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/sourcegraph/references repository repoSourcegraphReferences
|
||||
// ---
|
||||
// summary: Get code intelligence reference locations
|
||||
// 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: path
|
||||
// in: query
|
||||
// description: file path
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: line
|
||||
// in: query
|
||||
// description: line number (0-indexed)
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: character
|
||||
// in: query
|
||||
// description: character position (0-indexed)
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: git ref (commit SHA, branch, tag)
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: reference locations
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "503":
|
||||
// description: Sourcegraph integration not available
|
||||
|
||||
if !setting.Sourcegraph.Enabled {
|
||||
ctx.APIError(http.StatusServiceUnavailable, "Sourcegraph integration is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
path := ctx.FormString("path")
|
||||
line := ctx.FormInt("line")
|
||||
char := ctx.FormInt("character")
|
||||
ref := ctx.FormString("ref")
|
||||
|
||||
if path == "" || ref == "" {
|
||||
ctx.APIError(http.StatusBadRequest, "path and ref are required")
|
||||
return
|
||||
}
|
||||
|
||||
client := sourcegraph_service.GetClient()
|
||||
if client == nil {
|
||||
ctx.APIError(http.StatusServiceUnavailable, "Sourcegraph client not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := client.References(ctx, sourcegraphRepoName(ctx), ref, path, line, char)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadGateway, err)
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = []sourcegraph_service.Location{}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/services/repository/archiver"
|
||||
sourcegraph_service "code.gitea.io/gitea/services/sourcegraph"
|
||||
"code.gitea.io/gitea/services/task"
|
||||
"code.gitea.io/gitea/services/uinotification"
|
||||
"code.gitea.io/gitea/services/webhook"
|
||||
@@ -172,6 +173,7 @@ func InitWebInstalled(ctx context.Context) {
|
||||
mustInitCtx(ctx, actions_service.Init)
|
||||
|
||||
mustInit(repo_service.InitLicenseClassifier)
|
||||
mustInit(sourcegraph_service.Init)
|
||||
|
||||
// Finally start up the cron
|
||||
cron.Init(ctx)
|
||||
|
||||
@@ -281,4 +281,14 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
|
||||
ctx.Data["EscapeStatus"] = escapeStatus
|
||||
ctx.Data["BlameRows"] = rows
|
||||
ctx.Data["LexerName"] = lexerName
|
||||
|
||||
// Sourcegraph code intelligence configuration
|
||||
if setting.Sourcegraph.Enabled {
|
||||
ctx.Data["SourcegraphEnabled"] = true
|
||||
ctx.Data["SourcegraphConfig"] = map[string]any{
|
||||
"enabled": true,
|
||||
"repoFullName": ctx.Repo.Repository.FullName(),
|
||||
"commitID": ctx.Repo.CommitID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,16 @@ func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner
|
||||
setPathsCompareContext(ctx, before, head, headOwner, headName)
|
||||
setImageCompareContext(ctx)
|
||||
setCsvCompareContext(ctx)
|
||||
|
||||
// Sourcegraph code intelligence configuration
|
||||
if setting.Sourcegraph.Enabled {
|
||||
ctx.Data["SourcegraphEnabled"] = true
|
||||
ctx.Data["SourcegraphConfig"] = map[string]any{
|
||||
"enabled": true,
|
||||
"repoFullName": headOwner + "/" + headName,
|
||||
"commitID": head.ID.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SourceCommitURL creates a relative URL for a commit in the given repository
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -256,6 +256,16 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
|
||||
default:
|
||||
// unable to render anything, show the "view raw" or let frontend handle it
|
||||
}
|
||||
|
||||
// Sourcegraph code intelligence configuration
|
||||
if setting.Sourcegraph.Enabled {
|
||||
ctx.Data["SourcegraphEnabled"] = true
|
||||
ctx.Data["SourcegraphConfig"] = map[string]any{
|
||||
"enabled": true,
|
||||
"repoFullName": ctx.Repo.Repository.FullName(),
|
||||
"commitID": ctx.Repo.CommitID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareFileViewEditorButtons(ctx *context.Context) bool {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+17
-38
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -63,10 +63,10 @@ func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
|
||||
}
|
||||
|
||||
return &BlobUploader{
|
||||
model,
|
||||
hash,
|
||||
f,
|
||||
false,
|
||||
PackageBlobUpload: model,
|
||||
MultiHasher: hash,
|
||||
file: f,
|
||||
reading: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sourcegraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Client wraps Sourcegraph GraphQL API
|
||||
type Client struct {
|
||||
baseURL string
|
||||
accessToken string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Range represents a range in source code
|
||||
type Range struct {
|
||||
Start Position `json:"start"`
|
||||
End Position `json:"end"`
|
||||
}
|
||||
|
||||
// Position represents a position in source code
|
||||
type Position struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
}
|
||||
|
||||
// HoverResult represents hover documentation response
|
||||
type HoverResult struct {
|
||||
Contents string `json:"contents"` // Markdown content
|
||||
Range *Range `json:"range,omitempty"`
|
||||
}
|
||||
|
||||
// Location represents a code location
|
||||
type Location struct {
|
||||
Repo string `json:"repo"`
|
||||
Path string `json:"path"`
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
var defaultClient *Client
|
||||
|
||||
// GetClient returns the singleton Sourcegraph client
|
||||
func GetClient() *Client {
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
// NewClient creates a new Sourcegraph client with current settings
|
||||
func NewClient() *Client {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: setting.Sourcegraph.SkipTLSVerify, //nolint:gosec
|
||||
},
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: setting.Sourcegraph.URL,
|
||||
accessToken: setting.Sourcegraph.AccessToken,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(setting.Sourcegraph.Timeout) * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// graphqlRequest performs a GraphQL request to Sourcegraph
|
||||
func (c *Client) graphqlRequest(ctx context.Context, query string, variables map[string]any) (json.RawMessage, error) {
|
||||
body := map[string]any{
|
||||
"query": query,
|
||||
"variables": variables,
|
||||
}
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Sourcegraph GraphQL request to %s: variables=%v", c.baseURL+"/.api/graphql", variables)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/.api/graphql", bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.accessToken != "" {
|
||||
req.Header.Set("Authorization", "token "+c.accessToken)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Warn("Sourcegraph GraphQL error status %d: %s", resp.StatusCode, string(respBody))
|
||||
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
log.Debug("Sourcegraph GraphQL response: %s", string(respBody))
|
||||
|
||||
var result struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
Errors []struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if len(result.Errors) > 0 {
|
||||
log.Warn("Sourcegraph GraphQL errors: %v", result.Errors)
|
||||
return nil, fmt.Errorf("graphql error: %s", result.Errors[0].Message)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// cacheKey generates a cache key for code intelligence queries
|
||||
func cacheKey(queryType, repo, rev, path string, line, char int) string {
|
||||
return fmt.Sprintf("sg:%s:%s:%s:%s:%d:%d", queryType, repo, rev, path, line, char)
|
||||
}
|
||||
|
||||
// Hover queries documentation at a position
|
||||
func (c *Client) Hover(ctx context.Context, repo, rev, path string, line, char int) (*HoverResult, error) {
|
||||
key := cacheKey("hover", repo, rev, path, line, char)
|
||||
|
||||
log.Debug("Sourcegraph Hover request: repo=%s rev=%s path=%s line=%d char=%d", repo, rev, path, line, char)
|
||||
|
||||
// Try cache first
|
||||
if cached, hit := cache.GetCache().Get(key); hit {
|
||||
var result HoverResult
|
||||
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
||||
log.Debug("Sourcegraph Hover cache hit")
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.graphqlRequest(ctx, hoverQuery, map[string]any{
|
||||
"repo": repo,
|
||||
"rev": rev,
|
||||
"path": path,
|
||||
"line": line,
|
||||
"char": char,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := parseHoverResponse(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
if result != nil {
|
||||
if resultBytes, err := json.Marshal(result); err == nil {
|
||||
_ = cache.GetCache().Put(key, string(resultBytes), int64(setting.Sourcegraph.CacheTTL))
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Definition queries for symbol definition
|
||||
func (c *Client) Definition(ctx context.Context, repo, rev, path string, line, char int) ([]Location, error) {
|
||||
key := cacheKey("def", repo, rev, path, line, char)
|
||||
|
||||
// Try cache first
|
||||
if cached, hit := cache.GetCache().Get(key); hit {
|
||||
var result []Location
|
||||
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.graphqlRequest(ctx, definitionQuery, map[string]any{
|
||||
"repo": repo,
|
||||
"rev": rev,
|
||||
"path": path,
|
||||
"line": line,
|
||||
"char": char,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := parseLocationsResponse(data, "definitions")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
if resultBytes, err := json.Marshal(result); err == nil {
|
||||
_ = cache.GetCache().Put(key, string(resultBytes), int64(setting.Sourcegraph.CacheTTL))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// References queries for all references to a symbol
|
||||
func (c *Client) References(ctx context.Context, repo, rev, path string, line, char int) ([]Location, error) {
|
||||
key := cacheKey("refs", repo, rev, path, line, char)
|
||||
|
||||
// Try cache first
|
||||
if cached, hit := cache.GetCache().Get(key); hit {
|
||||
var result []Location
|
||||
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.graphqlRequest(ctx, referencesQuery, map[string]any{
|
||||
"repo": repo,
|
||||
"rev": rev,
|
||||
"path": path,
|
||||
"line": line,
|
||||
"char": char,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := parseLocationsResponse(data, "references")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
if resultBytes, err := json.Marshal(result); err == nil {
|
||||
_ = cache.GetCache().Put(key, string(resultBytes), int64(setting.Sourcegraph.CacheTTL))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SyncRepository notifies Sourcegraph to re-sync a repository
|
||||
func (c *Client) SyncRepository(ctx context.Context, repoName string) error {
|
||||
// Use the repository sync mutation
|
||||
_, err := c.graphqlRequest(ctx, syncRepoMutation, map[string]any{
|
||||
"repo": repoName,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("Sourcegraph sync failed for %s: %v", repoName, err)
|
||||
return err
|
||||
}
|
||||
log.Debug("Sourcegraph sync triggered for %s", repoName)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sourcegraph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GraphQL queries for Sourcegraph API
|
||||
|
||||
const hoverQuery = `
|
||||
query Hover($repo: String!, $rev: String!, $path: String!, $line: Int!, $char: Int!) {
|
||||
repository(name: $repo) {
|
||||
commit(rev: $rev) {
|
||||
blob(path: $path) {
|
||||
lsif {
|
||||
hover(line: $line, character: $char) {
|
||||
markdown {
|
||||
text
|
||||
}
|
||||
range {
|
||||
start { line character }
|
||||
end { line character }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const definitionQuery = `
|
||||
query Definition($repo: String!, $rev: String!, $path: String!, $line: Int!, $char: Int!) {
|
||||
repository(name: $repo) {
|
||||
commit(rev: $rev) {
|
||||
blob(path: $path) {
|
||||
lsif {
|
||||
definitions(line: $line, character: $char) {
|
||||
nodes {
|
||||
resource {
|
||||
path
|
||||
repository {
|
||||
name
|
||||
}
|
||||
commit {
|
||||
oid
|
||||
}
|
||||
}
|
||||
range {
|
||||
start { line character }
|
||||
end { line character }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const referencesQuery = `
|
||||
query References($repo: String!, $rev: String!, $path: String!, $line: Int!, $char: Int!) {
|
||||
repository(name: $repo) {
|
||||
commit(rev: $rev) {
|
||||
blob(path: $path) {
|
||||
lsif {
|
||||
references(line: $line, character: $char, first: 100) {
|
||||
nodes {
|
||||
resource {
|
||||
path
|
||||
repository {
|
||||
name
|
||||
}
|
||||
commit {
|
||||
oid
|
||||
}
|
||||
}
|
||||
range {
|
||||
start { line character }
|
||||
end { line character }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const syncRepoMutation = `
|
||||
mutation SyncRepo($repo: String!) {
|
||||
scheduleRepositoryPermissionsSync(repository: $repo) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Response parsing structures
|
||||
|
||||
type hoverResponse struct {
|
||||
Repository *struct {
|
||||
Commit *struct {
|
||||
Blob *struct {
|
||||
LSIF *struct {
|
||||
Hover *struct {
|
||||
Markdown *struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"markdown"`
|
||||
Range *struct {
|
||||
Start struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
} `json:"start"`
|
||||
End struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
} `json:"end"`
|
||||
} `json:"range"`
|
||||
} `json:"hover"`
|
||||
} `json:"lsif"`
|
||||
} `json:"blob"`
|
||||
} `json:"commit"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
type locationsResponse struct {
|
||||
Repository *struct {
|
||||
Commit *struct {
|
||||
Blob *struct {
|
||||
LSIF *struct {
|
||||
Definitions *locationNodes `json:"definitions,omitempty"`
|
||||
References *locationNodes `json:"references,omitempty"`
|
||||
} `json:"lsif"`
|
||||
} `json:"blob"`
|
||||
} `json:"commit"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
type locationNodes struct {
|
||||
Nodes []struct {
|
||||
Resource struct {
|
||||
Path string `json:"path"`
|
||||
Repository struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"repository"`
|
||||
Commit struct {
|
||||
OID string `json:"oid"`
|
||||
} `json:"commit"`
|
||||
} `json:"resource"`
|
||||
Range struct {
|
||||
Start struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
} `json:"start"`
|
||||
End struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
} `json:"end"`
|
||||
} `json:"range"`
|
||||
} `json:"nodes"`
|
||||
}
|
||||
|
||||
func parseHoverResponse(data json.RawMessage) (*HoverResult, error) {
|
||||
var resp hoverResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse hover response: %w", err)
|
||||
}
|
||||
|
||||
if resp.Repository == nil || resp.Repository.Commit == nil ||
|
||||
resp.Repository.Commit.Blob == nil || resp.Repository.Commit.Blob.LSIF == nil ||
|
||||
resp.Repository.Commit.Blob.LSIF.Hover == nil {
|
||||
return nil, nil // No hover info available
|
||||
}
|
||||
|
||||
hover := resp.Repository.Commit.Blob.LSIF.Hover
|
||||
result := &HoverResult{}
|
||||
|
||||
if hover.Markdown != nil {
|
||||
result.Contents = hover.Markdown.Text
|
||||
}
|
||||
|
||||
if hover.Range != nil {
|
||||
result.Range = &Range{
|
||||
Start: Position{
|
||||
Line: hover.Range.Start.Line,
|
||||
Character: hover.Range.Start.Character,
|
||||
},
|
||||
End: Position{
|
||||
Line: hover.Range.End.Line,
|
||||
Character: hover.Range.End.Character,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseLocationsResponse(data json.RawMessage, field string) ([]Location, error) {
|
||||
var resp locationsResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse locations response: %w", err)
|
||||
}
|
||||
|
||||
if resp.Repository == nil || resp.Repository.Commit == nil ||
|
||||
resp.Repository.Commit.Blob == nil || resp.Repository.Commit.Blob.LSIF == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var nodes *locationNodes
|
||||
switch field {
|
||||
case "definitions":
|
||||
nodes = resp.Repository.Commit.Blob.LSIF.Definitions
|
||||
case "references":
|
||||
nodes = resp.Repository.Commit.Blob.LSIF.References
|
||||
}
|
||||
|
||||
if nodes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
locations := make([]Location, 0, len(nodes.Nodes))
|
||||
for _, node := range nodes.Nodes {
|
||||
locations = append(locations, Location{
|
||||
Repo: node.Resource.Repository.Name,
|
||||
Path: node.Resource.Path,
|
||||
Line: node.Range.Start.Line,
|
||||
Character: node.Range.Start.Character,
|
||||
})
|
||||
}
|
||||
|
||||
return locations, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sourcegraph
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
// Init initializes the Sourcegraph service
|
||||
func Init() error {
|
||||
if !setting.Sourcegraph.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
defaultClient = NewClient()
|
||||
notify_service.RegisterNotifier(NewNotifier())
|
||||
|
||||
log.Info("Sourcegraph integration enabled: %s", setting.Sourcegraph.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns whether Sourcegraph integration is enabled
|
||||
func IsEnabled() bool {
|
||||
return setting.Sourcegraph.Enabled && defaultClient != nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sourcegraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
type sourcegraphNotifier struct {
|
||||
notify_service.NullNotifier
|
||||
}
|
||||
|
||||
var _ notify_service.Notifier = &sourcegraphNotifier{}
|
||||
|
||||
// NewNotifier creates a new Sourcegraph notifier
|
||||
func NewNotifier() notify_service.Notifier {
|
||||
return &sourcegraphNotifier{}
|
||||
}
|
||||
|
||||
func (n *sourcegraphNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
||||
if !setting.Sourcegraph.Enabled || !setting.Sourcegraph.SyncOnPush {
|
||||
return
|
||||
}
|
||||
|
||||
client := GetClient()
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger Sourcegraph to re-sync the repository
|
||||
if err := client.SyncRepository(ctx, repo.FullName()); err != nil {
|
||||
log.Warn("Sourcegraph sync failed for %s: %v", repo.FullName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *sourcegraphNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
||||
// Also sync on mirror push
|
||||
n.PushCommits(ctx, pusher, repo, opts, commits)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+5
-3
@@ -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',
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui bottom attached table unstackable segment">
|
||||
<div class="file-view code-view unicode-escaped">
|
||||
<div class="file-view code-view unicode-escaped"
|
||||
{{if .SourcegraphEnabled}}data-global-init="initSourcegraph" data-sourcegraph="{{JsonUtils.EncodeToString .SourcegraphConfig}}" data-sg-path="{{.FileTreePath}}"{{end}}>
|
||||
{{if .IsFileTooLarge}}
|
||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||
{{else if not .FileSize}}
|
||||
|
||||
@@ -81,7 +81,8 @@
|
||||
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
|
||||
{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
|
||||
{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.Repository.IsArchived) $.IsShowingAllCommits}}
|
||||
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
|
||||
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}
|
||||
{{if $.SourcegraphEnabled}}data-global-init="initSourcegraph" data-sourcegraph="{{JsonUtils.EncodeToString $.SourcegraphConfig}}" data-sg-path="{{$file.Name}}"{{end}}>
|
||||
<div class="diff-file-header sticky-2nd-row ui top attached header">
|
||||
<div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap">
|
||||
<div class="flex-text-block">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div {{if .ReadmeInList}}id="readme"{{end}} class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content"
|
||||
data-global-init="initRepoFileView" data-raw-file-link="{{.RawFileLink}}">
|
||||
data-global-init="initRepoFileView{{if .SourcegraphEnabled}} initSourcegraph{{end}}" data-raw-file-link="{{.RawFileLink}}"
|
||||
{{if .SourcegraphEnabled}}data-sourcegraph="{{JsonUtils.EncodeToString .SourcegraphConfig}}" data-sg-path="{{.TreePath}}"{{end}}>
|
||||
|
||||
{{- if .FileError}}
|
||||
<div class="ui error message">
|
||||
|
||||
Generated
+104
-4
@@ -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": [
|
||||
@@ -23536,7 +23636,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": {
|
||||
@@ -24133,7 +24233,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": {
|
||||
@@ -27645,7 +27745,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": {
|
||||
@@ -28389,7 +28489,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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
Vendored
+87
@@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/* Sourcegraph code intelligence styles */
|
||||
|
||||
.sourcegraph-hover {
|
||||
max-width: 600px;
|
||||
max-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sourcegraph-hover .sg-content {
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0; /* Allow shrinking */
|
||||
}
|
||||
|
||||
.sourcegraph-hover .sg-content pre {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
background: var(--color-code-bg);
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: var(--fonts-monospace);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sourcegraph-hover .sg-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
/* References panel */
|
||||
.sg-refs-panel {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
top: 100px;
|
||||
width: 400px;
|
||||
max-height: 60vh;
|
||||
background: var(--color-body);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px var(--color-shadow);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sg-refs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
background: var(--color-box-header);
|
||||
}
|
||||
|
||||
.sg-refs-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sg-refs-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-light);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sg-refs-close:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.sg-refs-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sg-refs-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-family: var(--fonts-monospace);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sg-refs-item:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.sg-refs-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sg-refs-path {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sg-refs-line {
|
||||
color: var(--color-text-light);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Highlight hoverable tokens on hover */
|
||||
.code-view .code-inner span:hover {
|
||||
background: var(--color-hover);
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -44,6 +44,7 @@
|
||||
@import "./features/cropper.css";
|
||||
@import "./features/console.css";
|
||||
@import "./features/captcha.css";
|
||||
@import "./features/sourcegraph.css";
|
||||
|
||||
@import "./markup/content.css";
|
||||
@import "./markup/codecopy.css";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
|
||||
|
||||
type StepContainerElement = HTMLElement & {_stepLogsActiveContainer?: HTMLElement}
|
||||
|
||||
type LogLine = {
|
||||
index: number;
|
||||
timestamp: number;
|
||||
@@ -221,19 +223,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,
|
||||
@@ -395,7 +396,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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'>,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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')!;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import type {Instance as TippyInstance} from 'tippy.js';
|
||||
|
||||
interface SourcegraphConfig {
|
||||
enabled: boolean;
|
||||
repoFullName: string;
|
||||
commitID: string;
|
||||
}
|
||||
|
||||
interface HoverResult {
|
||||
contents?: string;
|
||||
range?: {
|
||||
start: {line: number; character: number};
|
||||
end: {line: number; character: number};
|
||||
};
|
||||
}
|
||||
|
||||
interface Location {
|
||||
repo: string;
|
||||
path: string;
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
let activeTippy: TippyInstance | null = null;
|
||||
let hoverTimeout: number | null = null;
|
||||
let refsPanel: HTMLElement | null = null;
|
||||
|
||||
async function fetchHover(config: SourcegraphConfig, path: string, line: number, char: number): Promise<HoverResult | null> {
|
||||
const params = new URLSearchParams({
|
||||
path,
|
||||
line: String(line),
|
||||
character: String(char),
|
||||
ref: config.commitID,
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await GET(`/api/v1/repos/${config.repoFullName}/sourcegraph/hover?${params}`);
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json();
|
||||
return data.contents ? data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDefinition(config: SourcegraphConfig, path: string, line: number, char: number): Promise<Location[]> {
|
||||
const params = new URLSearchParams({
|
||||
path,
|
||||
line: String(line),
|
||||
character: String(char),
|
||||
ref: config.commitID,
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await GET(`/api/v1/repos/${config.repoFullName}/sourcegraph/definition?${params}`);
|
||||
if (!resp.ok) return [];
|
||||
return await resp.json();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchReferences(config: SourcegraphConfig, path: string, line: number, char: number): Promise<Location[]> {
|
||||
const params = new URLSearchParams({
|
||||
path,
|
||||
line: String(line),
|
||||
character: String(char),
|
||||
ref: config.commitID,
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await GET(`/api/v1/repos/${config.repoFullName}/sourcegraph/references?${params}`);
|
||||
if (!resp.ok) return [];
|
||||
return await resp.json();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function hideActiveTippy(): void {
|
||||
if (activeTippy) {
|
||||
activeTippy.destroy();
|
||||
activeTippy = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hideRefsPanel(): void {
|
||||
if (refsPanel) {
|
||||
refsPanel.remove();
|
||||
refsPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function createHoverContent(
|
||||
contents: string,
|
||||
config: SourcegraphConfig,
|
||||
path: string,
|
||||
line: number,
|
||||
char: number,
|
||||
): Promise<HTMLElement> {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'sourcegraph-hover';
|
||||
|
||||
// Content is pre-rendered as HTML by the backend
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'sg-content markup';
|
||||
contentDiv.innerHTML = contents;
|
||||
el.appendChild(contentDiv);
|
||||
|
||||
// Pre-fetch definitions to know if button should be enabled
|
||||
const definitions = await fetchDefinition(config, path, line, char);
|
||||
|
||||
// Action buttons
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'sg-actions';
|
||||
|
||||
const goToDefBtn = document.createElement('button');
|
||||
goToDefBtn.className = 'ui mini basic button';
|
||||
goToDefBtn.textContent = 'Go to definition';
|
||||
if (definitions.length === 0) {
|
||||
goToDefBtn.disabled = true;
|
||||
goToDefBtn.classList.add('disabled');
|
||||
} else {
|
||||
goToDefBtn.addEventListener('click', () => {
|
||||
hideActiveTippy();
|
||||
if (definitions.length === 1) {
|
||||
navigateToLocation(definitions[0]);
|
||||
} else {
|
||||
showLocationsPanel('Definitions', definitions);
|
||||
}
|
||||
});
|
||||
}
|
||||
actionsDiv.appendChild(goToDefBtn);
|
||||
|
||||
const findRefsBtn = document.createElement('button');
|
||||
findRefsBtn.className = 'ui mini basic button';
|
||||
findRefsBtn.textContent = 'Find references';
|
||||
findRefsBtn.addEventListener('click', async () => {
|
||||
hideActiveTippy();
|
||||
const locations = await fetchReferences(config, path, line, char);
|
||||
if (locations.length > 0) {
|
||||
showLocationsPanel('References', locations);
|
||||
}
|
||||
});
|
||||
actionsDiv.appendChild(findRefsBtn);
|
||||
|
||||
el.appendChild(actionsDiv);
|
||||
return el;
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function normalizeRepoName(repo: string): string {
|
||||
// Sourcegraph returns repo names like "git.example.com/owner/repo"
|
||||
// We need just "owner/repo" for Gitea URLs
|
||||
const parts = repo.split('/');
|
||||
if (parts.length >= 3) {
|
||||
// Has domain prefix - return last two parts (owner/repo)
|
||||
return parts.slice(-2).join('/');
|
||||
}
|
||||
return repo;
|
||||
}
|
||||
|
||||
function navigateToLocation(loc: Location): void {
|
||||
// Build URL to the location
|
||||
// If it's in the same repo, navigate to file
|
||||
// If it's in another repo, we need to construct the full URL
|
||||
const currentPath = window.location.pathname;
|
||||
const repoMatch = currentPath.match(/^\/([^/]+\/[^/]+)/);
|
||||
const currentRepo = repoMatch ? repoMatch[1] : '';
|
||||
|
||||
// Normalize repo name (strip domain prefix if present)
|
||||
const targetRepo = normalizeRepoName(loc.repo || '');
|
||||
|
||||
let url: string;
|
||||
if (targetRepo === currentRepo || !targetRepo) {
|
||||
// Same repo - construct relative URL
|
||||
const basePath = currentPath.replace(/\/src\/.*$/, '');
|
||||
url = `${basePath}/src/branch/main/${loc.path}#L${loc.line + 1}`;
|
||||
} else {
|
||||
// Different repo
|
||||
url = `/${targetRepo}/src/branch/main/${loc.path}#L${loc.line + 1}`;
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function showLocationsPanel(title: string, locations: Location[]): void {
|
||||
hideRefsPanel();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'sg-refs-panel';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'sg-refs-header';
|
||||
header.innerHTML = `
|
||||
<span class="sg-refs-title">${escapeHtml(title)} (${locations.length})</span>
|
||||
<button class="sg-refs-close">×</button>
|
||||
`;
|
||||
panel.appendChild(header);
|
||||
|
||||
header.querySelector('.sg-refs-close')?.addEventListener('click', hideRefsPanel);
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'sg-refs-list';
|
||||
|
||||
for (const loc of locations) {
|
||||
const item = document.createElement('a');
|
||||
item.className = 'sg-refs-item';
|
||||
item.href = '#';
|
||||
item.innerHTML = `
|
||||
<span class="sg-refs-path">${escapeHtml(loc.path)}</span>
|
||||
<span class="sg-refs-line">:${loc.line + 1}</span>
|
||||
`;
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
navigateToLocation(loc);
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
panel.appendChild(list);
|
||||
document.body.appendChild(panel);
|
||||
refsPanel = panel;
|
||||
}
|
||||
|
||||
function getTokenPosition(container: HTMLElement, token: Element, path: string): {path: string; line: number; char: number} | null {
|
||||
const row = token.closest('tr');
|
||||
if (!row) return null;
|
||||
|
||||
// Support both data-line-number (file view, blame) and data-line-num (diff view)
|
||||
let lineEl = row.querySelector('[data-line-number]') as HTMLElement | null;
|
||||
let line: number;
|
||||
|
||||
if (lineEl) {
|
||||
line = parseInt(lineEl.dataset.lineNumber || '0', 10);
|
||||
} else {
|
||||
// For diff views, prefer the "new" line number (right side) for code intelligence
|
||||
lineEl = row.querySelector('.lines-num-new[data-line-num]') as HTMLElement | null;
|
||||
if (!lineEl) {
|
||||
lineEl = row.querySelector('[data-line-num]') as HTMLElement | null;
|
||||
}
|
||||
if (!lineEl) return null;
|
||||
line = parseInt(lineEl.dataset.lineNum || '0', 10);
|
||||
}
|
||||
|
||||
if (isNaN(line) || line <= 0) return null;
|
||||
|
||||
// Calculate character offset within the line
|
||||
const codeCell = row.querySelector('.lines-code .code-inner');
|
||||
if (!codeCell) return null;
|
||||
|
||||
let char = 0;
|
||||
const walker = document.createTreeWalker(codeCell, NodeFilter.SHOW_TEXT);
|
||||
let node: Node | null;
|
||||
|
||||
while ((node = walker.nextNode())) {
|
||||
if (token.contains(node)) {
|
||||
// Found the text node containing or preceding our token
|
||||
break;
|
||||
}
|
||||
char += node.textContent?.length || 0;
|
||||
}
|
||||
|
||||
// Sourcegraph uses 0-indexed lines
|
||||
return {path, line: line - 1, char};
|
||||
}
|
||||
|
||||
function isHoverableToken(el: Element): boolean {
|
||||
// Check if it's a span inside code-inner (syntax highlighted token)
|
||||
if (el.tagName !== 'SPAN') return false;
|
||||
const codeInner = el.closest('.code-inner');
|
||||
return codeInner !== null;
|
||||
}
|
||||
|
||||
export function initSourcegraph(): void {
|
||||
registerGlobalInitFunc('initSourcegraph', (container: HTMLElement) => {
|
||||
const configStr = container.dataset.sourcegraph;
|
||||
if (!configStr) return;
|
||||
|
||||
let config: SourcegraphConfig;
|
||||
try {
|
||||
config = JSON.parse(configStr);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.enabled) return;
|
||||
|
||||
const path = container.dataset.sgPath || '';
|
||||
if (!path) return;
|
||||
|
||||
// Set up hover listener with debouncing
|
||||
container.addEventListener('mouseover', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!isHoverableToken(target)) return;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
}
|
||||
|
||||
hoverTimeout = window.setTimeout(async () => {
|
||||
const pos = getTokenPosition(container, target, path);
|
||||
if (!pos) return;
|
||||
|
||||
const hover = await fetchHover(config, pos.path, pos.line, pos.char);
|
||||
if (!hover?.contents) return;
|
||||
|
||||
// Hide any existing tippy
|
||||
hideActiveTippy();
|
||||
|
||||
// Create and show new tippy
|
||||
const content = await createHoverContent(hover.contents, config, pos.path, pos.line, pos.char);
|
||||
activeTippy = createTippy(target, {
|
||||
content,
|
||||
theme: 'default',
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
maxWidth: 600,
|
||||
onHide: () => {
|
||||
activeTippy = null;
|
||||
},
|
||||
});
|
||||
activeTippy.show();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
container.addEventListener('mouseout', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!isHoverableToken(target)) return;
|
||||
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Close tippy and refs panel when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as Node;
|
||||
|
||||
// Close refs panel if clicking outside it
|
||||
if (refsPanel && !refsPanel.contains(target)) {
|
||||
hideRefsPanel();
|
||||
}
|
||||
|
||||
// Close tippy if clicking outside it
|
||||
if (activeTippy) {
|
||||
const tippyBox = document.querySelector('.tippy-box');
|
||||
if (tippyBox && !tippyBox.contains(target)) {
|
||||
hideActiveTippy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
-30
@@ -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
|
||||
|
||||
@@ -19,6 +19,7 @@ import {initStopwatch} from './features/stopwatch.ts';
|
||||
import {initRepoFileSearch} from './features/repo-findfile.ts';
|
||||
import {initMarkupContent} from './markup/content.ts';
|
||||
import {initRepoFileView} from './features/file-view.ts';
|
||||
import {initSourcegraph} from './features/sourcegraph.ts';
|
||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||
import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
|
||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||
@@ -158,6 +159,7 @@ const initPerformanceTracer = callInitFunctions([
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
|
||||
initRepoFileView,
|
||||
initSourcegraph,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user