Compare commits

...

12 Commits

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

---------

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

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

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

---------

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

---------

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

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

---------

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

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

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