Merge branch 'main' into lunny/add_pr_files_selection

pull/36014/head
Lunny Xiao 2025-12-11 14:13:27 +07:00
commit a5738ef605
263 changed files with 3439 additions and 2736 deletions

@ -4,7 +4,7 @@ tmp_dir = ".air"
[build]
pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs
cmd = "make --no-print-directory backend"
bin = "gitea"
entrypoint = ["./gitea"]
delay = 2000
include_ext = ["go", "tmpl"]
include_file = ["main.go"]

@ -1,35 +0,0 @@
name: e2e-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-e2e:
# the "test-e2e" won't pass, and it seems that there is no useful test, so skip
# if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
if: false
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 24
- run: make deps-frontend frontend deps-backend
- run: pnpm exec playwright install --with-deps
- run: make test-e2e-sqlite
timeout-minutes: 40
env:
USE_REPO_TEST_DIR: 1

@ -114,6 +114,10 @@ linters:
- stringsbuilder
perfsprint:
concat-loop: false
govet:
enable:
- nilness
- unusedwrite
exclusions:
generated: lax
presets:

@ -32,15 +32,14 @@ XGO_VERSION := go-1.25.x
AIR_PACKAGE ?= github.com/air-verse/air@v1
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.0
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest
@ -333,7 +332,7 @@ lint-frontend: lint-js lint-css ## lint frontend files
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
.PHONY: lint-backend
lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files
lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files
.PHONY: lint-backend-fix
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
@ -396,11 +395,6 @@ lint-go-gitea-vet: ## lint go files with gitea-vet
@echo "Running gitea-vet..."
@$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./...
.PHONY: lint-go-gopls
lint-go-gopls: ## lint go files with gopls
@echo "Running gopls check..."
@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES)
.PHONY: lint-editorconfig
lint-editorconfig:
@echo "Running editorconfig check..."
@ -844,7 +838,6 @@ deps-tools: ## install tool dependencies
$(GO) install $(GO_LICENSES_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
$(GO) install $(GOPLS_PACKAGE) & \
wait
node_modules: pnpm-lock.yaml

@ -3,7 +3,6 @@ import comments from '@eslint-community/eslint-plugin-eslint-comments';
import github from 'eslint-plugin-github';
import globals from 'globals';
import importPlugin from 'eslint-plugin-import-x';
import noUseExtendNative from 'eslint-plugin-no-use-extend-native';
import playwright from 'eslint-plugin-playwright';
import regexp from 'eslint-plugin-regexp';
import sonarjs from 'eslint-plugin-sonarjs';
@ -58,7 +57,6 @@ export default defineConfig([
'array-func': arrayFunc,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/203
'import-x': importPlugin,
'no-use-extend-native': noUseExtendNative,
regexp,
sonarjs,
unicorn,
@ -155,7 +153,7 @@ export default defineConfig([
'@typescript-eslint/ban-tslint-comment': [0],
'@typescript-eslint/class-literal-property-style': [0],
'@typescript-eslint/class-methods-use-this': [0],
'@typescript-eslint/consistent-generic-constructors': [0],
'@typescript-eslint/consistent-generic-constructors': [2, 'constructor'],
'@typescript-eslint/consistent-indexed-object-style': [0],
'@typescript-eslint/consistent-return': [0],
'@typescript-eslint/consistent-type-assertions': [2, {assertionStyle: 'as', objectLiteralTypeAssertions: 'allow'}],
@ -231,6 +229,7 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-return': [0],
'@typescript-eslint/no-unsafe-unary-minus': [2],
'@typescript-eslint/no-unused-expressions': [0],
'@typescript-eslint/no-unused-private-class-members': [2],
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}],
'@typescript-eslint/no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}],
'@typescript-eslint/no-useless-constructor': [0],
@ -587,10 +586,9 @@ export default defineConfig([
'no-unsafe-negation': [2],
'no-unused-expressions': [2],
'no-unused-labels': [2],
'no-unused-private-class-members': [2],
'no-unused-private-class-members': [0], // handled by @typescript-eslint/no-unused-private-class-members
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define
'no-use-extend-native/no-use-extend-native': [2],
'no-useless-assignment': [2],
'no-useless-backreference': [2],
'no-useless-call': [2],

@ -2,7 +2,7 @@ module code.gitea.io/gitea
go 1.25.0
toolchain go1.25.4
toolchain go1.25.5
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:
@ -17,7 +17,7 @@ require (
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/httpsig v1.2.3

@ -41,8 +41,8 @@ gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15 h1:qFYmz05u/s9664o7+XEgrlHXSPQ4uHO8/ccZGUb1uxA=
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e h1:4bugwPyGMLvblEm3pZ8fZProSPVxE4l0UXF2Kv6IJoY=
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=

@ -276,8 +276,14 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate {
// The task repo can access the current repo only if the task repo is private and
// the owner of the task repo is a collaborative owner of the current repo.
// FIXME allow public repo read access if tokenless pull is enabled
// FIXME should owner's visibility also be considered here?
// check permission like simple user but limit to read-only
perm, err = GetUserRepoPermission(ctx, repo, user_model.NewActionsUser())
if err != nil {
return perm, err
}
perm.AccessMode = min(perm.AccessMode, perm_model.AccessModeRead)
return perm, nil
}
accessMode = perm_model.AccessModeRead

@ -73,18 +73,18 @@ func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string)
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
// The given map of files with their viewed state will be merged with the previous review, if present
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) error {
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) (*ReviewState, error) {
log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles)
review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA)
if err != nil {
return err
return nil, err
}
if exists {
review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles)
} else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil {
return err
return nil, err
// Overwrite the viewed files of the previous review if present
} else if previousReview != nil {
@ -98,11 +98,11 @@ func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA stri
if !exists {
log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles)
_, err := engine.Insert(review)
return err
return nil, err
}
log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles)
_, err = engine.ID(review.ID).Update(&ReviewState{UpdatedFiles: review.UpdatedFiles})
return err
_, err = engine.ID(review.ID).Cols("updated_files").Update(review)
return review, err
}
// mergeFiles merges the given maps of files with their viewing state into one map.

@ -869,16 +869,6 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
}
// IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
has, err := IsRepositoryModelExist(ctx, u, repoName)
if err != nil {
return false, err
}
isDir, err := util.IsDir(RepoPath(u.Name, repoName))
return has || isDir, err
}
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
return db.GetEngine(ctx).Get(&Repository{
OwnerID: u.ID,

@ -9,8 +9,6 @@ import (
"time"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
@ -106,35 +104,6 @@ func (err ErrRepoFilesAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// CheckCreateRepository check if doer could create a repository in new owner
func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepoIn(owner) {
return ErrReachLimitOfRepo{owner.MaxRepoCreation}
}
if err := IsUsableRepoName(name); err != nil {
return err
}
has, err := IsRepositoryModelOrDirExist(ctx, owner, name)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has {
return ErrRepoAlreadyExist{owner.Name, name}
}
repoPath := RepoPath(owner.Name, name)
isExist, err := util.IsExist(repoPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
return err
}
if !overwriteOrAdopt && isExist {
return ErrRepoFilesAlreadyExist{owner.Name, name}
}
return nil
}
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error {
_, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{

@ -22,7 +22,7 @@ var WebAuthn *webauthn.WebAuthn
// Init initializes the WebAuthn instance from the config.
func Init() {
gob.Register(&webauthn.SessionData{})
gob.Register(&webauthn.SessionData{}) // TODO: CHI-SESSION-GOB-REGISTER.
appURL, _ := protocol.FullyQualifiedOrigin(setting.AppURL)

@ -76,7 +76,7 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
if p.IconSVGs[svgID] == "" {
p.IconSVGs[svgID] = svgHTML
}
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
return template.HTML(`<svg ` + svgCommonAttrs + `><use href="#` + svgID + `"></use></svg>`)
}
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {

@ -25,7 +25,7 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
return ""
}
sb := &strings.Builder{}
sb.WriteString(`<div class=tw-hidden>`)
sb.WriteString(`<div class="svg-icon-container">`)
for _, icon := range p.IconSVGs {
sb.WriteString(string(icon))
}

@ -96,8 +96,8 @@ func (attrs *Attributes) GetGitlabLanguage() optional.Option[string] {
// gitlab-language may have additional parameters after the language
// ignore them and just use the main language
// https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type
if idx := strings.IndexByte(raw, '?'); idx >= 0 {
return optional.Some(raw[:idx])
if before, _, ok := strings.Cut(raw, "?"); ok {
return optional.Some(before)
}
}
return attrStr

@ -5,17 +5,13 @@
package git
import (
"bufio"
"bytes"
"context"
"errors"
"io"
"os/exec"
"strconv"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
@ -130,65 +126,6 @@ func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptio
return err
}
// AllCommitsCount returns count of all commits in repository
func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, files ...string) (int64, error) {
cmd := gitcmd.NewCommand("rev-list")
if hidePRRefs {
cmd.AddArguments("--exclude=" + PullPrefix + "*")
}
cmd.AddArguments("--all", "--count")
if len(files) > 0 {
cmd.AddDashesAndList(files...)
}
stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
}
// CommitsCountOptions the options when counting commits
type CommitsCountOptions struct {
RepoPath string
Not string
Revision []string
RelPath []string
Since string
Until string
}
// CommitsCount returns number of total commits of until given revision.
func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) {
cmd := gitcmd.NewCommand("rev-list", "--count")
cmd.AddDynamicArguments(opts.Revision...)
if opts.Not != "" {
cmd.AddOptionValues("--not", opts.Not)
}
if len(opts.RelPath) > 0 {
cmd.AddDashesAndList(opts.RelPath...)
}
stdout, _, err := cmd.WithDir(opts.RepoPath).RunStdString(ctx)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
}
// CommitsCount returns number of total commits of until current revision.
func (c *Commit) CommitsCount() (int64, error) {
return CommitsCount(c.repo.Ctx, CommitsCountOptions{
RepoPath: c.repo.Path,
Revision: []string{c.ID.String()},
})
}
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
@ -371,85 +308,6 @@ func (c *Commit) GetBranchName() (string, error) {
return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil
}
// CommitFileStatus represents status of files in a commit.
type CommitFileStatus struct {
Added []string
Removed []string
Modified []string
}
// NewCommitFileStatus creates a CommitFileStatus
func NewCommitFileStatus() *CommitFileStatus {
return &CommitFileStatus{
[]string{}, []string{}, []string{},
}
}
func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) {
rd := bufio.NewReader(stdout)
peek, err := rd.Peek(1)
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
if peek[0] == '\n' || peek[0] == '\x00' {
_, _ = rd.Discard(1)
}
for {
modifier, err := rd.ReadString('\x00')
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
file, err := rd.ReadString('\x00')
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
file = file[:len(file)-1]
switch modifier[0] {
case 'A':
fileStatus.Added = append(fileStatus.Added, file)
case 'D':
fileStatus.Removed = append(fileStatus.Removed, file)
case 'M':
fileStatus.Modified = append(fileStatus.Modified, file)
}
}
}
// GetCommitFileStatus returns file status of commit in given repository.
func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*CommitFileStatus, error) {
stdout, w := io.Pipe()
done := make(chan struct{})
fileStatus := NewCommitFileStatus()
go func() {
parseCommitFileStatus(fileStatus, stdout)
close(done)
}()
stderr := new(bytes.Buffer)
err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").
AddDynamicArguments(commitID).
WithDir(repoPath).
WithStdout(w).
WithStderr(stderr).
Run(ctx)
w.Close() // Close writer to exit parsing goroutine
if err != nil {
return nil, gitcmd.ConcatenateError(err, stderr.String())
}
<-done
return fileStatus, nil
}
// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) {
commitID, _, err := gitcmd.NewCommand("rev-parse").

@ -14,33 +14,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestCommitsCountSha256(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
commitsCount, err := CommitsCount(t.Context(),
CommitsCountOptions{
RepoPath: bareRepo1Path,
Revision: []string{"f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc"},
})
assert.NoError(t, err)
assert.Equal(t, int64(3), commitsCount)
}
func TestCommitsCountWithoutBaseSha256(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
commitsCount, err := CommitsCount(t.Context(),
CommitsCountOptions{
RepoPath: bareRepo1Path,
Not: "main",
Revision: []string{"branch1"},
})
assert.NoError(t, err)
assert.Equal(t, int64(2), commitsCount)
}
func TestGetFullCommitIDSha256(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
@ -157,39 +130,3 @@ func TestHasPreviousCommitSha256(t *testing.T) {
assert.NoError(t, err)
assert.False(t, selfNot)
}
func TestGetCommitFileStatusMergesSha256(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo6_merge_sha256")
commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo1Path, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1")
assert.NoError(t, err)
expected := CommitFileStatus{
[]string{
"add_file.txt",
},
[]string{},
[]string{
"to_modify.txt",
},
}
assert.Equal(t, expected.Added, commitFileStatus.Added)
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
expected = CommitFileStatus{
[]string{},
[]string{
"to_remove.txt",
},
[]string{},
}
commitFileStatus, err = GetCommitFileStatus(t.Context(), bareRepo1Path, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172")
assert.NoError(t, err)
assert.Equal(t, expected.Added, commitFileStatus.Added)
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
}

@ -13,33 +13,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestCommitsCount(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
commitsCount, err := CommitsCount(t.Context(),
CommitsCountOptions{
RepoPath: bareRepo1Path,
Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"},
})
assert.NoError(t, err)
assert.Equal(t, int64(3), commitsCount)
}
func TestCommitsCountWithoutBase(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
commitsCount, err := CommitsCount(t.Context(),
CommitsCountOptions{
RepoPath: bareRepo1Path,
Not: "master",
Revision: []string{"branch1"},
})
assert.NoError(t, err)
assert.Equal(t, int64(2), commitsCount)
}
func TestGetFullCommitID(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
@ -212,134 +185,6 @@ func TestHasPreviousCommit(t *testing.T) {
assert.False(t, selfNot)
}
func TestParseCommitFileStatus(t *testing.T) {
type testcase struct {
output string
added []string
removed []string
modified []string
}
kases := []testcase{
{
// Merge commit
output: "MM\x00options/locale/locale_en-US.ini\x00",
modified: []string{
"options/locale/locale_en-US.ini",
},
added: []string{},
removed: []string{},
},
{
// Spaces commit
output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00",
removed: []string{
"b",
"b b/b",
},
modified: []string{},
added: []string{
"b b/b b/b b/b",
"b b/b b/b b/b b/b",
},
},
{
// larger commit
output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00",
modified: []string{
"go.mod",
"go.sum",
"modules/ssh/ssh.go",
"vendor/github.com/gliderlabs/ssh/circle.yml",
"vendor/github.com/gliderlabs/ssh/context.go",
"vendor/github.com/gliderlabs/ssh/server.go",
"vendor/github.com/gliderlabs/ssh/session.go",
"vendor/github.com/gliderlabs/ssh/ssh.go",
"vendor/golang.org/x/sys/unix/mkerrors.sh",
"vendor/golang.org/x/sys/unix/syscall_darwin.go",
"vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go",
"vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go",
"vendor/golang.org/x/sys/unix/zerrors_linux.go",
"vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go",
"vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go",
"vendor/modules.txt",
},
added: []string{
"vendor/github.com/gliderlabs/ssh/go.mod",
"vendor/github.com/gliderlabs/ssh/go.sum",
},
removed: []string{},
},
{
// git 1.7.2 adds an unnecessary \x00 on merge commit
output: "\x00MM\x00options/locale/locale_en-US.ini\x00",
modified: []string{
"options/locale/locale_en-US.ini",
},
added: []string{},
removed: []string{},
},
{
// git 1.7.2 adds an unnecessary \n on normal commit
output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00",
removed: []string{
"b",
"b b/b",
},
modified: []string{},
added: []string{
"b b/b b/b b/b",
"b b/b b/b b/b b/b",
},
},
}
for _, kase := range kases {
fileStatus := NewCommitFileStatus()
parseCommitFileStatus(fileStatus, strings.NewReader(kase.output))
assert.Equal(t, kase.added, fileStatus.Added)
assert.Equal(t, kase.removed, fileStatus.Removed)
assert.Equal(t, kase.modified, fileStatus.Modified)
}
}
func TestGetCommitFileStatusMerges(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo6_merge")
commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo1Path, "022f4ce6214973e018f02bf363bf8a2e3691f699")
assert.NoError(t, err)
expected := CommitFileStatus{
[]string{
"add_file.txt",
},
[]string{
"to_remove.txt",
},
[]string{
"to_modify.txt",
},
}
assert.Equal(t, expected.Added, commitFileStatus.Added)
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
}
func Test_GetCommitBranchStart(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
repo, err := OpenRepository(t.Context(), bareRepo1Path)

@ -113,10 +113,10 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) {
var fieldKey string
var fieldVal string
firstSpace := strings.Index(field, " ")
if firstSpace > 0 {
fieldKey = field[:firstSpace]
fieldVal = field[firstSpace+1:]
before, after, ok := strings.Cut(field, " ")
if ok {
fieldKey = before
fieldVal = after
} else {
// could be the case if the requested field had no value
fieldKey = field

@ -27,15 +27,15 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
// <mode> <type> <sha>\t<filename>
var err error
posTab := bytes.IndexByte(line, '\t')
if posTab == -1 {
before, after, ok := bytes.Cut(line, []byte{'\t'})
if !ok {
return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
}
entry := new(LsTreeEntry)
entryAttrs := line[:posTab]
entryName := line[posTab+1:]
entryAttrs := before
entryName := after
entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type

@ -32,11 +32,6 @@ type GPGSettings struct {
const prettyLogFormat = `--pretty=format:%H`
// GetAllCommitsCount returns count of all commits in repository
func (repo *Repository) GetAllCommitsCount() (int64, error) {
return AllCommitsCount(repo.Ctx, repo.Path, false)
}
func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) {
// avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]'
logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat).
@ -191,18 +186,21 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
// PushOptions options when push to remote
type PushOptions struct {
Remote string
Branch string
Force bool
Mirror bool
Env []string
Timeout time.Duration
Remote string
Branch string
Force bool
ForceWithLease string
Mirror bool
Env []string
Timeout time.Duration
}
// Push pushs local commits to given remote branch.
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
cmd := gitcmd.NewCommand("push")
if opts.Force {
if opts.ForceWithLease != "" {
cmd.AddOptionFormat("--force-with-lease=%s", opts.ForceWithLease)
} else if opts.Force {
cmd.AddArguments("-f")
}
if opts.Mirror {

@ -11,7 +11,6 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
)
@ -216,16 +215,6 @@ func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bo
return len(strings.TrimSpace(string(stdout))) > 0, nil
}
// FileCommitsCount return the number of files at a revision
func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
return CommitsCount(repo.Ctx,
CommitsCountOptions{
RepoPath: repo.Path,
Revision: []string{revision},
RelPath: []string{file},
})
}
type CommitsByFileAndRangeOptions struct {
Revision string
File string
@ -433,25 +422,6 @@ func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error
return repo.CommitsBetween(lastCommit, beforeCommit)
}
// CommitsCountBetween return numbers of commits between two commits
func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) {
count, err := CommitsCount(repo.Ctx, CommitsCountOptions{
RepoPath: repo.Path,
Revision: []string{start + ".." + end},
})
if err != nil && strings.Contains(err.Error(), "no merge base") {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list before last so let's try that...
return CommitsCount(repo.Ctx, CommitsCountOptions{
RepoPath: repo.Path,
Revision: []string{start, end},
})
}
return count, err
}
// commitsBefore the limit is depth, not total number of returned commits.
func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) {
cmd := gitcmd.NewCommand("log", prettyLogFormat)
@ -564,23 +534,6 @@ func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err e
return len(stdout) > 0, err
}
func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error {
if repo.LastCommitCache == nil {
commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) {
commit, err := repo.GetCommit(sha)
if err != nil {
return 0, err
}
return commit.CommitsCount()
})
if err != nil {
return err
}
repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache())
}
return nil
}
// GetCommitBranchStart returns the commit where the branch diverged
func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
cmd := gitcmd.NewCommand("log", prettyLogFormat)

@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git"
)
func NewBatch(ctx context.Context, repo Repository) (*git.Batch, error) {
return git.NewBatch(ctx, repoPath(repo))
}

@ -0,0 +1,96 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"strconv"
"strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// CommitsCountOptions the options when counting commits
type CommitsCountOptions struct {
Not string
Revision []string
RelPath []string
Since string
Until string
}
// CommitsCount returns number of total commits of until given revision.
func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions) (int64, error) {
cmd := gitcmd.NewCommand("rev-list", "--count")
cmd.AddDynamicArguments(opts.Revision...)
if opts.Not != "" {
cmd.AddOptionValues("--not", opts.Not)
}
if len(opts.RelPath) > 0 {
cmd.AddDashesAndList(opts.RelPath...)
}
stdout, _, err := cmd.WithDir(repoPath(repo)).RunStdString(ctx)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
}
// CommitsCountBetween return numbers of commits between two commits
func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) {
count, err := CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{start + ".." + end},
})
if err != nil && strings.Contains(err.Error(), "no merge base") {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list before last so let's try that...
return CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{start, end},
})
}
return count, err
}
// FileCommitsCount return the number of files at a revision
func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) {
return CommitsCount(ctx, repo,
CommitsCountOptions{
Revision: []string{revision},
RelPath: []string{file},
})
}
// CommitsCountOfCommit returns number of total commits of until current revision.
func CommitsCountOfCommit(ctx context.Context, repo Repository, commitID string) (int64, error) {
return CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{commitID},
})
}
// AllCommitsCount returns count of all commits in repository
func AllCommitsCount(ctx context.Context, repo Repository, hidePRRefs bool, files ...string) (int64, error) {
cmd := gitcmd.NewCommand("rev-list")
if hidePRRefs {
cmd.AddArguments("--exclude=" + git.PullPrefix + "*")
}
cmd.AddArguments("--all", "--count")
if len(files) > 0 {
cmd.AddDashesAndList(files...)
}
stdout, err := RunCmdString(ctx, repo, cmd)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
}

@ -0,0 +1,93 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"bufio"
"bytes"
"context"
"io"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
)
// CommitFileStatus represents status of files in a commit.
type CommitFileStatus struct {
Added []string
Removed []string
Modified []string
}
// NewCommitFileStatus creates a CommitFileStatus
func NewCommitFileStatus() *CommitFileStatus {
return &CommitFileStatus{
[]string{}, []string{}, []string{},
}
}
func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) {
rd := bufio.NewReader(stdout)
peek, err := rd.Peek(1)
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
if peek[0] == '\n' || peek[0] == '\x00' {
_, _ = rd.Discard(1)
}
for {
modifier, err := rd.ReadString('\x00')
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
file, err := rd.ReadString('\x00')
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
file = file[:len(file)-1]
switch modifier[0] {
case 'A':
fileStatus.Added = append(fileStatus.Added, file)
case 'D':
fileStatus.Removed = append(fileStatus.Removed, file)
case 'M':
fileStatus.Modified = append(fileStatus.Modified, file)
}
}
}
// GetCommitFileStatus returns file status of commit in given repository.
func GetCommitFileStatus(ctx context.Context, repo Repository, commitID string) (*CommitFileStatus, error) {
stdout, w := io.Pipe()
done := make(chan struct{})
fileStatus := NewCommitFileStatus()
go func() {
parseCommitFileStatus(fileStatus, stdout)
close(done)
}()
stderr := new(bytes.Buffer)
err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").
AddDynamicArguments(commitID).
WithDir(repoPath(repo)).
WithStdout(w).
WithStderr(stderr).
Run(ctx)
w.Close() // Close writer to exit parsing goroutine
if err != nil {
return nil, gitcmd.ConcatenateError(err, stderr.String())
}
<-done
return fileStatus, nil
}

@ -0,0 +1,175 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseCommitFileStatus(t *testing.T) {
type testcase struct {
output string
added []string
removed []string
modified []string
}
kases := []testcase{
{
// Merge commit
output: "MM\x00options/locale/locale_en-US.ini\x00",
modified: []string{
"options/locale/locale_en-US.ini",
},
added: []string{},
removed: []string{},
},
{
// Spaces commit
output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00",
removed: []string{
"b",
"b b/b",
},
modified: []string{},
added: []string{
"b b/b b/b b/b",
"b b/b b/b b/b b/b",
},
},
{
// larger commit
output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00",
modified: []string{
"go.mod",
"go.sum",
"modules/ssh/ssh.go",
"vendor/github.com/gliderlabs/ssh/circle.yml",
"vendor/github.com/gliderlabs/ssh/context.go",
"vendor/github.com/gliderlabs/ssh/server.go",
"vendor/github.com/gliderlabs/ssh/session.go",
"vendor/github.com/gliderlabs/ssh/ssh.go",
"vendor/golang.org/x/sys/unix/mkerrors.sh",
"vendor/golang.org/x/sys/unix/syscall_darwin.go",
"vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go",
"vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go",
"vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go",
"vendor/golang.org/x/sys/unix/zerrors_linux.go",
"vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go",
"vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go",
"vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go",
"vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go",
"vendor/modules.txt",
},
added: []string{
"vendor/github.com/gliderlabs/ssh/go.mod",
"vendor/github.com/gliderlabs/ssh/go.sum",
},
removed: []string{},
},
{
// git 1.7.2 adds an unnecessary \x00 on merge commit
output: "\x00MM\x00options/locale/locale_en-US.ini\x00",
modified: []string{
"options/locale/locale_en-US.ini",
},
added: []string{},
removed: []string{},
},
{
// git 1.7.2 adds an unnecessary \n on normal commit
output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00",
removed: []string{
"b",
"b b/b",
},
modified: []string{},
added: []string{
"b b/b b/b b/b",
"b b/b b/b b/b b/b",
},
},
}
for _, kase := range kases {
fileStatus := NewCommitFileStatus()
parseCommitFileStatus(fileStatus, strings.NewReader(kase.output))
assert.Equal(t, kase.added, fileStatus.Added)
assert.Equal(t, kase.removed, fileStatus.Removed)
assert.Equal(t, kase.modified, fileStatus.Modified)
}
}
func TestGetCommitFileStatusMerges(t *testing.T) {
bareRepo6 := &mockRepository{path: "repo6_merge"}
commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6, "022f4ce6214973e018f02bf363bf8a2e3691f699")
assert.NoError(t, err)
expected := CommitFileStatus{
[]string{
"add_file.txt",
},
[]string{
"to_remove.txt",
},
[]string{
"to_modify.txt",
},
}
assert.Equal(t, expected.Added, commitFileStatus.Added)
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
}
func TestGetCommitFileStatusMergesSha256(t *testing.T) {
bareRepo6Sha256 := &mockRepository{path: "repo6_merge_sha256"}
commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6Sha256, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1")
assert.NoError(t, err)
expected := CommitFileStatus{
[]string{
"add_file.txt",
},
[]string{},
[]string{
"to_modify.txt",
},
}
assert.Equal(t, expected.Added, commitFileStatus.Added)
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
expected = CommitFileStatus{
[]string{},
[]string{
"to_remove.txt",
},
[]string{},
}
commitFileStatus, err = GetCommitFileStatus(t.Context(), bareRepo6Sha256, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172")
assert.NoError(t, err)
assert.Equal(t, expected.Added, commitFileStatus.Added)
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
}

@ -0,0 +1,35 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommitsCount(t *testing.T) {
bareRepo1 := &mockRepository{path: "repo1_bare"}
commitsCount, err := CommitsCount(t.Context(), bareRepo1,
CommitsCountOptions{
Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"},
})
assert.NoError(t, err)
assert.Equal(t, int64(3), commitsCount)
}
func TestCommitsCountWithoutBase(t *testing.T) {
bareRepo1 := &mockRepository{path: "repo1_bare"}
commitsCount, err := CommitsCount(t.Context(), bareRepo1,
CommitsCountOptions{
Not: "master",
Revision: []string{"branch1"},
})
assert.NoError(t, err)
assert.Equal(t, int64(2), commitsCount)
}

@ -9,6 +9,19 @@ import (
"code.gitea.io/gitea/modules/git"
)
func Push(ctx context.Context, repo Repository, opts git.PushOptions) error {
// PushToExternal pushes a managed repository to an external remote.
func PushToExternal(ctx context.Context, repo Repository, opts git.PushOptions) error {
return git.Push(ctx, repoPath(repo), opts)
}
// Push pushes from one managed repository to another managed repository.
func Push(ctx context.Context, fromRepo, toRepo Repository, opts git.PushOptions) error {
opts.Remote = repoPath(toRepo)
return git.Push(ctx, repoPath(fromRepo), opts)
}
// PushFromLocal pushes from a local path to a managed repository.
func PushFromLocal(ctx context.Context, fromLocalPath string, toRepo Repository, opts git.PushOptions) error {
opts.Remote = repoPath(toRepo)
return git.Push(ctx, fromLocalPath, opts)
}

@ -77,8 +77,8 @@ func Code(fileName, language, code string) (output template.HTML, lexerName stri
if lexer == nil {
// Attempt stripping off the '?'
if idx := strings.IndexByte(language, '?'); idx > 0 {
lexer = lexers.Get(language[:idx])
if before, _, ok := strings.Cut(language, "?"); ok {
lexer = lexers.Get(before)
}
}
}

@ -218,7 +218,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error {
batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize)
if len(changes.Updates) > 0 {
gitBatch, err := git.NewBatch(ctx, repo.RepoPath())
gitBatch, err := gitrepo.NewBatch(ctx, repo)
if err != nil {
return err
}

@ -210,7 +210,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elasti
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error {
reqs := make([]elastic.BulkableRequest, 0)
if len(changes.Updates) > 0 {
batch, err := git.NewBatch(ctx, repo.RepoPath())
batch, err := gitrepo.NewBatch(ctx, repo)
if err != nil {
return err
}

@ -17,20 +17,20 @@ func FilenameIndexerID(repoID int64, filename string) string {
}
func ParseIndexerID(indexerID string) (int64, string) {
index := strings.IndexByte(indexerID, '_')
if index == -1 {
before, after, ok := strings.Cut(indexerID, "_")
if !ok {
log.Error("Unexpected ID in repo indexer: %s", indexerID)
}
repoID, _ := internal.ParseBase36(indexerID[:index])
return repoID, indexerID[index+1:]
repoID, _ := internal.ParseBase36(before)
return repoID, after
}
func FilenameOfIndexerID(indexerID string) string {
index := strings.IndexByte(indexerID, '_')
if index == -1 {
_, after, ok := strings.Cut(indexerID, "_")
if !ok {
log.Error("Unexpected ID in repo indexer: %s", indexerID)
}
return indexerID[index+1:]
return after
}
// FilenameMatchIndexPos returns the boundaries of its first seven lines.

@ -33,7 +33,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
// Of text and link contents
sl := strings.SplitSeq(content, "|")
for v := range sl {
if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
if found := strings.Contains(v, "="); !found {
// There is no equal in this argument; this is a mandatory arg
if props["name"] == "" {
if IsFullURLString(v) {
@ -55,8 +55,8 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
} else {
// There is an equal; optional argument.
sep := strings.IndexByte(v, '=')
key, val := v[:sep], html.UnescapeString(v[sep+1:])
before, after, _ := strings.Cut(v, "=")
key, val := before, html.UnescapeString(after)
// When parsing HTML, x/net/html will change all quotes which are
// not used for syntax into UTF-8 quotes. So checking val[0] won't

@ -30,6 +30,10 @@ func TestMathRender(t *testing.T) {
"$ a $",
`<p><code class="language-math">a</code></p>` + nl,
},
{
"$a$$b$",
`<p><code class="language-math">a</code><code class="language-math">b</code></p>` + nl,
},
{
"$a$ $b$",
`<p><code class="language-math">a</code> <code class="language-math">b</code></p>` + nl,
@ -59,7 +63,7 @@ func TestMathRender(t *testing.T) {
`<p>a$b $a a$b b$</p>` + nl,
},
{
"a$x$",
"a$x$", // Pattern: "word$other$" The real world example is: "Price is between US$1 and US$2.", so don't parse this.
`<p>a$x$</p>` + nl,
},
{
@ -70,6 +74,10 @@ func TestMathRender(t *testing.T) {
"$a$ ($b$) [$c$] {$d$}",
`<p><code class="language-math">a</code> (<code class="language-math">b</code>) [$c$] {$d$}</p>` + nl,
},
{
"[$a$](link)",
`<p><a href="/link" rel="nofollow"><code class="language-math">a</code></a></p>` + nl,
},
{
"$$a$$",
`<p><code class="language-math">a</code></p>` + nl,

@ -54,6 +54,10 @@ func isAlphanumeric(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
}
func isInMarkdownLinkText(block text.Reader, lineAfter []byte) bool {
return block.PrecendingCharacter() == '[' && bytes.HasPrefix(lineAfter, []byte("]("))
}
// Parse parses the current line and returns a result of parsing.
func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()
@ -115,7 +119,9 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
}
// check valid ending character
isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) ||
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 ||
succeedingCharacter == '$' ||
isInMarkdownLinkText(block, line[i+len(stopMark):])
if checkSurrounding && !isValidEndingChar {
break
}

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/cachegroup"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@ -72,7 +73,7 @@ func ToAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.U
committerUsername = committer.Name
}
fileStatus, err := git.GetCommitFileStatus(ctx, repo.RepoPath(), commit.Sha1)
fileStatus, err := gitrepo.GetCommitFileStatus(ctx, repo, commit.Sha1)
if err != nil {
return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %w", commit.Sha1, err)
}

@ -51,10 +51,10 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
for _, unescapeIdx := range escapeStringIndices {
preceding := encoded[last:unescapeIdx[0]]
if !inKey {
if splitter := strings.Index(preceding, "__"); splitter > -1 {
section += preceding[:splitter]
if before, after, cutOk := strings.Cut(preceding, "__"); cutOk {
section += before
inKey = true
key += preceding[splitter+2:]
key += after
} else {
section += preceding
}
@ -77,9 +77,9 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
}
remaining := encoded[last:]
if !inKey {
if splitter := strings.Index(remaining, "__"); splitter > -1 {
section += remaining[:splitter]
key += remaining[splitter+2:]
if before, after, cutOk := strings.Cut(remaining, "__"); cutOk {
section += before
key += after
} else {
section += remaining
}
@ -111,21 +111,21 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect
func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
for _, kv := range envs {
idx := strings.IndexByte(kv, '=')
if idx < 0 {
before, after, ok := strings.Cut(kv, "=")
if !ok {
continue
}
// parse the environment variable to config section name and key name
envKey := kv[:idx]
envValue := kv[idx+1:]
envKey := before
envValue := after
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
if !ok {
continue
}
// use environment value as config value, or read the file content as value if the key indicates a file
keyValue := envValue
keyValue := envValue //nolint:staticcheck // false positive
if useFileValue {
fileContent, err := os.ReadFile(envValue)
if err != nil {

@ -337,14 +337,14 @@ func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
}
}
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
}
}

@ -370,6 +370,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
}
}
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
// But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))

@ -292,6 +292,21 @@ type RenameBranchRepoOption struct {
Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"`
}
// UpdateBranchRepoOption options when updating a branch reference in a repository
// swagger:model
type UpdateBranchRepoOption struct {
// New commit SHA (or any ref) the branch should point to
//
// required: true
NewCommitID string `json:"new_commit_id" binding:"Required"`
// Expected old commit SHA of the branch; if provided it must match the current tip
OldCommitID string `json:"old_commit_id"`
// Force update even if the change is not a fast-forward
Force bool `json:"force"`
}
// TransferRepoOption options when transfer a repository's ownership
// swagger:model
type TransferRepoOption struct {

@ -215,8 +215,8 @@ func addValidGroupTeamMapRule() {
}
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
_, after, ok := strings.Cut(hostport, ":")
if !ok {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
@ -225,7 +225,7 @@ func portOnly(hostport string) string {
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
return after
}
func validPort(p string) bool {

@ -46,11 +46,15 @@ func RouterMockPoint(pointName string) func(next http.Handler) http.Handler {
//
// Then the mock function will be executed as a middleware at the mock point.
// It only takes effect in testing mode (setting.IsInTesting == true).
func RouteMock(pointName string, h any) {
func RouteMock(pointName string, h any) func() {
if _, ok := routeMockPoints[pointName]; !ok {
panic("route mock point not found: " + pointName)
}
old := routeMockPoints[pointName]
routeMockPoints[pointName] = toHandlerProvider(h)
return func() {
routeMockPoints[pointName] = old
}
}
// RouteMockReset resets all mock points (no mock anymore)

@ -55,7 +55,7 @@ func NewRouter() *Router {
// Use supports two middlewares
func (r *Router) Use(middlewares ...any) {
for _, m := range middlewares {
if m != nil {
if !isNilOrFuncNil(m) {
r.chiRouter.Use(toHandlerProvider(m))
}
}

@ -3219,6 +3219,22 @@
".favicons": "folder-favicon",
"_favicons": "folder-favicon",
"__favicons__": "folder-favicon",
"feature": "folder-features",
".feature": "folder-features",
"_feature": "folder-features",
"__feature__": "folder-features",
"features": "folder-features",
".features": "folder-features",
"_features": "folder-features",
"__features__": "folder-features",
"feat": "folder-features",
".feat": "folder-features",
"_feat": "folder-features",
"__feat__": "folder-features",
"feats": "folder-features",
".feats": "folder-features",
"_feats": "folder-features",
"__feats__": "folder-features",
"lefthook": "folder-lefthook",
".lefthook": "folder-lefthook",
"_lefthook": "folder-lefthook",
@ -3474,7 +3490,15 @@
"cues": "folder-cue",
".cues": "folder-cue",
"_cues": "folder-cue",
"__cues__": "folder-cue"
"__cues__": "folder-cue",
"license": "folder-license",
".license": "folder-license",
"_license": "folder-license",
"__license__": "folder-license",
"licenses": "folder-license",
".licenses": "folder-license",
"_licenses": "folder-license",
"__licenses__": "folder-license"
},
"folderNamesExpanded": {
"rust": "folder-rust-open",
@ -6696,6 +6720,22 @@
".favicons": "folder-favicon-open",
"_favicons": "folder-favicon-open",
"__favicons__": "folder-favicon-open",
"feature": "folder-features-open",
".feature": "folder-features-open",
"_feature": "folder-features-open",
"__feature__": "folder-features-open",
"features": "folder-features-open",
".features": "folder-features-open",
"_features": "folder-features-open",
"__features__": "folder-features-open",
"feat": "folder-features-open",
".feat": "folder-features-open",
"_feat": "folder-features-open",
"__feat__": "folder-features-open",
"feats": "folder-features-open",
".feats": "folder-features-open",
"_feats": "folder-features-open",
"__feats__": "folder-features-open",
"lefthook": "folder-lefthook-open",
".lefthook": "folder-lefthook-open",
"_lefthook": "folder-lefthook-open",
@ -6951,7 +6991,15 @@
"cues": "folder-cue-open",
".cues": "folder-cue-open",
"_cues": "folder-cue-open",
"__cues__": "folder-cue-open"
"__cues__": "folder-cue-open",
"license": "folder-license-open",
".license": "folder-license-open",
"_license": "folder-license-open",
"__license__": "folder-license-open",
"licenses": "folder-license-open",
".licenses": "folder-license-open",
"_licenses": "folder-license-open",
"__licenses__": "folder-license-open"
},
"rootFolderNames": {},
"rootFolderNamesExpanded": {},
@ -7102,6 +7150,8 @@
"srf": "image",
"srw": "image",
"x3f": "image",
"ktx": "image",
"ktx2": "image",
"pal": "palette",
"gpl": "palette",
"act": "palette",
@ -7287,6 +7337,7 @@
"cp": "cpp",
"mii": "cpp",
"ii": "cpp",
"cppm": "cpp",
"hh": "hpp",
"hpp": "hpp",
"hxx": "hpp",
@ -7373,6 +7424,11 @@
"fsi": "fsharp",
"fsproj": "fsharp",
"swift": "swift",
"xcplayground": "swift",
"swiftdeps": "swift",
"swiftdoc": "swift",
"swiftmodule": "swift",
"swiftsourceinfo": "swift",
"ino": "arduino",
"dockerignore": "docker",
"dockerfile": "docker",
@ -8105,10 +8161,10 @@
"css.jsx": "vanilla-extract",
"toc": "toc",
"cue": "cue",
"lean": "lean",
"cljx": "clojure",
"clojure": "clojure",
"edn": "clojure",
"cppm": "cpp",
"ccm": "cpp",
"cxxm": "cpp",
"c++m": "cpp",
@ -8412,36 +8468,36 @@
"gradlew": "gradle",
"gradle-wrapper.properties": "gradle",
"gradlew.bat": "gradle",
"copying": "certificate",
"copying.md": "certificate",
"copying.rst": "certificate",
"copying.txt": "certificate",
"copyright": "certificate",
"copyright.md": "certificate",
"copyright.rst": "certificate",
"copyright.txt": "certificate",
"license": "certificate",
"license-agpl": "certificate",
"license-apache": "certificate",
"license-bsd": "certificate",
"license-mit": "certificate",
"license-gpl": "certificate",
"license-lgpl": "certificate",
"license.md": "certificate",
"license.rst": "certificate",
"license.txt": "certificate",
"licence": "certificate",
"licence-agpl": "certificate",
"licence-apache": "certificate",
"licence-bsd": "certificate",
"licence-mit": "certificate",
"licence-gpl": "certificate",
"licence-lgpl": "certificate",
"licence.md": "certificate",
"licence.rst": "certificate",
"licence.txt": "certificate",
"unlicense": "certificate",
"unlicense.txt": "certificate",
"copying": "license",
"copying.md": "license",
"copying.rst": "license",
"copying.txt": "license",
"copyright": "license",
"copyright.md": "license",
"copyright.rst": "license",
"copyright.txt": "license",
"license": "license",
"license-agpl": "license",
"license-apache": "license",
"license-bsd": "license",
"license-mit": "license",
"license-gpl": "license",
"license-lgpl": "license",
"license.md": "license",
"license.rst": "license",
"license.txt": "license",
"licence": "license",
"licence-agpl": "license",
"licence-apache": "license",
"licence-bsd": "license",
"licence-mit": "license",
"licence-gpl": "license",
"licence-lgpl": "license",
"licence.md": "license",
"licence.rst": "license",
"licence.txt": "license",
"unlicense": "unlicense",
"unlicense.txt": "unlicense",
".htpasswd": "key",
"sha256sums": "key",
".secrets": "key",
@ -8457,6 +8513,7 @@
".rspec": "rspec",
".swift-format": "swift",
".swift-version": "swift",
".swiftformat": "swift",
"dockerfile": "docker",
"dockerfile.prod": "docker",
"dockerfile.production": "docker",
@ -8993,6 +9050,12 @@
"rspress.config.ts": "rstack",
"rslint.json": "rstack",
"rslint.jsonc": "rstack",
"lynx.config.js": "lynx",
"lynx.config.mjs": "lynx",
"lynx.config.cjs": "lynx",
"lynx.config.ts": "lynx",
"lynx.config.mts": "lynx",
"lynx.config.cts": "lynx",
"ionic.config.json": "ionic",
".io-config.json": "ionic",
"gulpfile.js": "gulp",
@ -9722,6 +9785,18 @@
"vitest.config.ts": "vitest",
"vitest.config.mts": "vitest",
"vitest.config.cts": "vitest",
"vitest.unit.config.js": "vitest",
"vitest.unit.config.mjs": "vitest",
"vitest.unit.config.cjs": "vitest",
"vitest.unit.config.ts": "vitest",
"vitest.unit.config.mts": "vitest",
"vitest.unit.config.cts": "vitest",
"vitest.e2e.config.js": "vitest",
"vitest.e2e.config.mjs": "vitest",
"vitest.e2e.config.cjs": "vitest",
"vitest.e2e.config.ts": "vitest",
"vitest.e2e.config.mts": "vitest",
"vitest.e2e.config.cts": "vitest",
"velite.config.js": "velite",
"velite.config.mjs": "velite",
"velite.config.cjs": "velite",
@ -10169,6 +10244,7 @@
".coderabbit.yml": "coderabbit-ai",
".coderabbit.yaml": "coderabbit-ai",
".aiexclude": "gemini-ai",
"gemini.md": "gemini-ai",
"taze.config.js": "taze",
"taze.config.mjs": "taze",
"taze.config.cjs": "taze",
@ -10273,7 +10349,10 @@
"hadolint.yaml": "hadolint",
"hadolint.yml": "hadolint",
"tsdoc.json": "tsdoc",
".oxlintrc.json": "oxlint",
".oxlintrc.json": "oxc",
".oxlintrc.jsonc": "oxc",
".oxfmtrc.json": "oxc",
".oxfmtrc.jsonc": "oxc",
"claude.md": "claude",
"claude.local.md": "claude",
".cursorignore": "cursor",
@ -10296,6 +10375,7 @@
"googleservice-info.plist": "google",
".shellcheckrc": "shellcheck",
"shellcheckrc": "shellcheck",
"warp.md": "warp",
"language-configuration.json": "jsonc",
"icon-theme.json": "jsonc",
"color-theme.json": "jsonc",
@ -10322,6 +10402,7 @@
"toml": "toml",
"diff": "diff",
"json": "json",
"jsonl": "json",
"jsonc": "json",
"json5": "json",
"blink": "blink",
@ -10497,7 +10578,8 @@
"gnuplot": "gnuplot",
"helm": "helm",
"nginx": "nginx",
"cue": "cue"
"cue": "cue",
"lean": "lean"
},
"light": {
"fileExtensions": {
@ -10705,7 +10787,8 @@
"src/bashly-strings.yaml": "bashly-strings_light",
"src/bashly-strings.yml": "bashly-strings_light",
".shellcheckrc": "shellcheck_light",
"shellcheckrc": "shellcheck_light"
"shellcheckrc": "shellcheck_light",
"warp.md": "warp_light"
},
"languageIds": {
"toml": "toml_light",

@ -92,7 +92,7 @@
"capnp": "<svg viewBox='0 0 24 24'><path fill='#c62828' d='M17 3V2h4v8h-4c-.085-2.088-.445-4.042-3-4-2.917 0-5 2.51-5 5 0 3 .495 6.981 4.67 6.981 2.906-.26 2.99-2.705 3.33-4.981h4c0 5.806-3.314 9.052-9 9-6.154-.073-8.915-4.685-9-10-.128-6.14 4.568-9.2 10.414-9.65 1.301-.028 2.466 0 3.586.65'/></svg>",
"cbx": "<svg viewBox='0 0 1024 1024'><path fill='#1565c0' d='M128 704v128c0 70.692 57.308 128 128 128h608c17.728 0 32-14.272 32-32V704z'/><path fill='#ffe082' d='M704 704v192h128V704z'/><path fill='#fff8e1' d='M192 704v96c0 53.184 42.816 96 96 96h544a96 96 0 0 1-96-96 96 96 0 0 1 96-96z'/><path fill='#ff1744' d='M320 832h192v192l-96-96-96 96z'/><path fill='#2196f3' d='M256 64c-70.692 0-128 57.308-128 128v640c0 11.088 1.557 21.787 4.207 32.047C146.767 807.565 197.672 768.07 256 768h608c17.728 0 32-14.272 32-32V96c0-17.728-14.272-32-32-32z'/><path fill='#e3f2fd' d='M384 192c-70.692 0-128 57.308-128 128 .171 67.295 52.422 122.965 119.57 127.396L256 640h80l156.748-252.488h-.146A128 128 0 0 0 512 320c0-70.692-57.308-128-128-128m320 0c-70.692 0-128 57.308-128 128 .171 67.295 52.422 122.965 119.57 127.396L576 640h80l156.748-252.488h-.146A128 128 0 0 0 832 320c0-70.692-57.308-128-128-128'/></svg>",
"cds": "<svg viewBox='0 0 16 16'><g fill='#0288d1'><rect width='4' height='1' x='7' y='9' ry='.5'/><rect width='3' height='1' x='8' y='11' ry='.5'/><rect width='4' height='1' x='7' y='13' ry='.5'/><path d='m5 9-1 1 1.5 1.5L4 13l1 1 2.5-2.5z'/><path d='M6 2a3 3 0 0 0-2.598 1.5 3 3 0 0 0-.187 2.607 3 3 0 0 0-1.514.965 3 3 0 0 0-.42 3.196A3 3 0 0 0 4 12v-1a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 .515.076l.159-.591A2 2 0 0 1 4 5a2 2 0 0 1 2-2 2 2 0 0 1 2 2l.594.594A2 2 0 0 1 10 5a2 2 0 0 1 2 2 2 2 0 0 1 2 2 2 2 0 0 1-2 2v1a3 3 0 0 0 2.898-2.223A3 3 0 0 0 13.5 6.402a3 3 0 0 0-.63-.267 3 3 0 0 0-1.722-1.906 3 3 0 0 0-2.252-.014 3 3 0 0 0-2.119-2.113A3 3 0 0 0 6 2'/></g></svg>",
"certificate": "<svg viewBox='0 0 32 32'><path fill='#ff5722' d='M4 6v14a2 2 0 0 0 2 2h12v6l3-2 3 2v-6h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2m2 0h8v2H6Zm0 4h6v2H6Zm0 4h8v2H6Zm10 6H6v-2h10Zm8-6v4l-3-2-3 2v-4l-4-2 4-2V6l3 2 3-2v4.2l4 1.8Z'/></svg>",
"certificate": "<svg viewBox='0 0 16 16'><path fill='#ff5722' d='M2 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h6v3l2-1.25L12 14v-3h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1Zm0 1h4v1H2Zm6 0 2 1.25L12 3v2.5l2 1-2 1V10l-2-1.25L8 10V7.5l-2-1 2-1zM2 5h3v1H2Zm0 2h3v1H2Zm0 2h4v1H2Z'/></svg>",
"changelog": "<svg fill='none' viewBox='0 0 24 24'><path d='M0 0h24v24H0z'/><path fill='#8bc34a' d='M13 3a9 9 0 0 0-9 9H1l4 4 4-4H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.95 8.95 0 0 0 13 21a9 9 0 0 0 0-18m-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z'/></svg>",
"chess": "<svg viewBox='0 0 32 32'><path fill='#cfd8dc' d='M6 26h20v4H6zm16.5-13a5.49 5.49 0 0 0-4.5 2.344V10h4V6h-4V2h-4v4h-4v4h4v5.344a5.498 5.498 0 1 0-5 8.63V24h14v-.025A5.499 5.499 0 0 0 22.5 13'/></svg>",
"chess_light": "<svg viewBox='0 0 32 32'><path fill='#455a64' d='M6 26h20v4H6zm16.5-13a5.49 5.49 0 0 0-4.5 2.344V10h4V6h-4V2h-4v4h-4v4h4v5.344a5.498 5.498 0 1 0-5 8.63V24h14v-.025A5.499 5.499 0 0 0 22.5 13'/></svg>",
@ -359,6 +359,8 @@
"folder-fastlane": "<svg viewBox='0 0 32 32'><path fill='#1e88e5' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#e3f2fd' d='m15.508 21.618 1.272 3.936a1.456 1.456 0 0 0-.06 1.922 1.35 1.35 0 0 0 .937.486l.1.003a1.33 1.33 0 0 0 .894-.346 1.4 1.4 0 0 0 .207-.231 1.446 1.446 0 0 0-.307-1.972 1.36 1.36 0 0 0-.597-.256l-1.586-5.037a.16.16 0 0 0-.092-.101.15.15 0 0 0-.134.007 1.92 1.92 0 0 1-2.059-.113 1.995 1.995 0 0 1-.423-2.72 1.84 1.84 0 0 1 2.088-.697.16.16 0 0 0 .199-.102l.325-.95a.17.17 0 0 0-.007-.128.16.16 0 0 0-.093-.084 3 3 0 0 0-.978-.167h-.039A3.215 3.215 0 0 0 12 18.296a3.3 3.3 0 0 0 1.321 2.698 3.08 3.08 0 0 0 2.187.624'/><path fill='#e3f2fd' d='M15.802 17.146a1.35 1.35 0 0 0-1.787.535 1.45 1.45 0 0 0-.157 1.074 1.4 1.4 0 0 0 .622.875 1.33 1.33 0 0 0 .706.203 1.36 1.36 0 0 0 1.176-.685 1.47 1.47 0 0 0 .177-.971l4.14-3.149a.17.17 0 0 0 .065-.122.17.17 0 0 0-.05-.13 2.09 2.09 0 0 1-.556-2.063 1.883 1.883 0 0 1 2.383-1.262 1.95 1.95 0 0 1 1.31 1.823.16.16 0 0 0 .155.161l.983.02a.2.2 0 0 0 .114-.047.17.17 0 0 0 .048-.117 3.34 3.34 0 0 0-.945-2.333A3.12 3.12 0 0 0 21.939 10a4 4 0 0 0-.293.014 3.14 3.14 0 0 0-2.162 1.182 3.4 3.4 0 0 0-.457 3.457Zm10.842 10.755a1.4 1.4 0 0 0 .736-.77 1.45 1.45 0 0 0-.005-1.083 1.38 1.38 0 0 0-.744-.763 1.3 1.3 0 0 0-1.047.005 1.4 1.4 0 0 0-.693.672l-5.12.014a.16.16 0 0 0-.123.06.17.17 0 0 0-.033.135 2.08 2.08 0 0 1-.724 2 1.82 1.82 0 0 1-1.4.355 1.86 1.86 0 0 1-1.233-.773 2 2 0 0 1-.016-2.286.17.17 0 0 0-.033-.226l-.778-.613a.15.15 0 0 0-.12-.032.16.16 0 0 0-.105.065 3.36 3.36 0 0 0-.575 2.45 3.3 3.3 0 0 0 1.266 2.153 3.1 3.1 0 0 0 1.874.634 3.15 3.15 0 0 0 2.572-1.349 3.4 3.4 0 0 0 .545-1.264l4.01-.04a1.35 1.35 0 0 0 1.746.656'/><path fill='#e3f2fd' d='m27.718 23.882 1.228-3.945a1.34 1.34 0 0 0 .824-.473 1.44 1.44 0 0 0 .328-1.026 1.366 1.366 0 0 0-2.729-.013 1.5 1.5 0 0 0 .06.553 1.4 1.4 0 0 0 .336.564l-1.603 5.027a.17.17 0 0 0 .016.14.16.16 0 0 0 .115.075 1.952 1.952 0 0 1 .385 3.782 1.84 1.84 0 0 1-2.097-.692.156.156 0 0 0-.217-.037l-.811.572a.16.16 0 0 0-.067.108.17.17 0 0 0 .028.124A3.15 3.15 0 0 0 26.097 30a3.1 3.1 0 0 0 1.871-.63 3.36 3.36 0 0 0 1.165-3.667 3.23 3.23 0 0 0-1.415-1.82Z'/><path fill='#e3f2fd' d='M31.845 17.545a3.15 3.15 0 0 0-5.177-1.459l-3.28-2.432a1.46 1.46 0 0 0-.193-.969 1.37 1.37 0 0 0-.857-.637 1.3 1.3 0 0 0-.308-.038h-.004a1.425 1.425 0 0 0-.003 2.847h.005a1.3 1.3 0 0 0 .631-.16l4.165 3.137a.16.16 0 0 0 .133.027.16.16 0 0 0 .104-.089 1.98 1.98 0 0 1 1.735-1.178h.001a1.933 1.933 0 0 1 1.897 1.963 1.96 1.96 0 0 1-1.296 1.863.166.166 0 0 0-.102.203l.28.98a.16.16 0 0 0 .079.098.15.15 0 0 0 .123.011 3.2 3.2 0 0 0 1.867-1.64 3.4 3.4 0 0 0 .2-2.527'/></svg>",
"folder-favicon-open": "<svg viewBox='0 0 32 32'><path fill='#fbc02d' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124c-.468 0-.921-.164-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#fffde7' d='m24 24 6 4-2-6 4-4h-6l-1.999-6L22 18h-6l4 4-2 6z'/></svg>",
"folder-favicon": "<svg viewBox='0 0 32 32'><path fill='#fbc02d' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#fffde7' d='m24 24 6 4-2-6 4-4h-6l-1.999-6L22 18h-6l4 4-2 6z'/></svg>",
"folder-features-open": "<svg fill='none' viewBox='0 0 32 32'><path fill='#689f38' d='M28.067 12H9.194a1.9 1.9 0 0 0-1.131.377c-.33.246-.576.593-.704.991L3.933 24V10h23.2c0-.53-.204-1.04-.567-1.414A1.9 1.9 0 0 0 25.199 8H14.686a1.9 1.9 0 0 1-1.237-.464l-1.245-1.072A1.9 1.9 0 0 0 10.966 6H3.933a1.9 1.9 0 0 0-1.367.586A2.04 2.04 0 0 0 2 8v16c0 .53.204 1.04.566 1.414.363.375.855.586 1.367.586H25.2l4.645-11.212a2.06 2.06 0 0 0-.163-1.889 1.96 1.96 0 0 0-.698-.66 1.9 1.9 0 0 0-.916-.239'/><path fill='#dcedc8' d='m27.587 13.584 2.132.509-6.03 6.03-2.7-2.7-1.456 1.454 4.156 4.155 6.485-6.485-.06.691L32 19.495l-1.885 2.259.262 2.988-2.79.666-1.46 2.583-2.627-1.184L20.873 28l-1.46-2.583-2.79-.667.262-2.996L15 19.495l1.885-2.265-.262-2.989 2.79-.657L20.873 11l2.627 1.185L26.127 11zM32 14.722l-1.826 1.825.203-2.297-.658-.157.827-.826z'/></svg>",
"folder-features": "<svg fill='none' viewBox='0 0 32 32'><path fill='#689f38' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#dcedc8' d='m27.587 13.583 2.132.51-6.03 6.03-2.7-2.701-1.456 1.455 4.156 4.155 6.485-6.486-.06.691L32 19.495l-1.885 2.259.262 2.988-2.79.666-1.46 2.584-2.627-1.185L20.873 28l-1.46-2.583-2.79-.667.262-2.996L15 19.495l1.885-2.265-.262-2.989 2.79-.658L20.873 11l2.627 1.185L26.127 11zM32 14.721l-1.826 1.825.203-2.296-.658-.157.827-.826z'/></svg>",
"folder-filter-open": "<svg viewBox='0 0 16 16'><path fill='#7e57c2' d='M14.483 6H4.721a1 1 0 0 0-.949.684L2 12V5h12a1 1 0 0 0-1-1H7.562a1 1 0 0 1-.64-.232l-.644-.536A1 1 0 0 0 5.638 3H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h11l2.403-5.606A1 1 0 0 0 14.483 6'/><path fill='#d1c4e9' d='M15 6H7a.414.414 0 0 0-.293.707L10 10v4l1.293 1.293A.414.414 0 0 0 12 15v-5l3.293-3.293A.414.414 0 0 0 15 6'/></svg>",
"folder-filter": "<svg viewBox='0 0 16 16'><path fill='#7e57c2' d='m6.922 3.768-.644-.536A1 1 0 0 0 5.638 3H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H7.562a1 1 0 0 1-.64-.232'/><path fill='#d1c4e9' d='M15 6H7a.414.414 0 0 0-.293.707L10 10v4l1.293 1.293A.414.414 0 0 0 12 15v-5l3.293-3.293A.414.414 0 0 0 15 6'/></svg>",
"folder-firebase-open": "<svg viewBox='0 0 32 32'><path fill='#ff9100' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#ffe0b2' d='M24 28.014s-4.584.213-7-4.014c-.66-1.156-1.006-2.805-1-4 .012-2.264.962-3.881 1.038-4 .117-.181 2.954-.867 4.962 0s3.979 3.215 4 6-2.275 4.565-2.691 4.881.691 1.133.691 1.133M22 25.5c2.051-1.646 1.875-2.063 2-3.5s-1.007-3.071-2-4-2.934-.65-3.5-.5c-2.418 5.405 3.5 8 3.5 8'/><path fill='#ffe0b2' d='m24 28.014 2.527-.941s-1.988-1.265-2.909-2.168C21.381 22.71 20.021 20.085 20 16s4-8 4-8 8.063 6.276 8 12c-.644 8.183-8 8.014-8 8.014m4-3.023c1.044-1.135 1.95-2.042 2-4.991.075-4.381-6-9.5-6-9.5s-1.856 2.393-2 5.5c-.338 7.273 6 8.991 6 8.991'/><path fill='#ffe0b2' d='M22.34 25.64s3.453-.086 5.66-.649c3.451-.879-1.022 2.191-1.022 2.191L24 28.015l-1.313-.536z'/></svg>",
@ -459,6 +461,8 @@
"folder-less": "<svg viewBox='0 0 32 32'><path fill='#0277bd' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#b3e5fc' d='M20 21a1 1 0 0 0-1-1 1 1 0 0 0 1-1v-5h2v-2h-2a2 2 0 0 0-2 2v4a1 1 0 0 1-1 1h-1v2h1a1 1 0 0 1 1 1v4a2 2 0 0 0 2 2h2v-2h-2Zm11-2a1 1 0 0 1-1-1v-4a2 2 0 0 0-2-2h-2v2h2v5a1 1 0 0 0 1 1 1 1 0 0 0-1 1v5h-2v2h2a2 2 0 0 0 2-2v-4a1 1 0 0 1 1-1h1v-2Z'/></svg>",
"folder-lib-open": "<svg viewBox='0 0 32 32'><path fill='#c0ca33' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#f0f4c3' d='M23 16a3 3 0 0 0 .003-6H23a3 3 0 0 0-3 2.999V13a3 3 0 0 0 2.999 3zm0 3.973c-2.225-2.078-5.955-3.978-9-3.973v10c3.19 0 6.85 2.004 9 4 2.225-2.078 5.955-4.005 9-4V16c-3.045-.005-6.775 1.895-9 3.973'/></svg>",
"folder-lib": "<svg viewBox='0 0 32 32'><path fill='#c0ca33' d='m13.844 7.536-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464'/><path fill='#f0f4c3' d='M22.931 16a3 3 0 0 0 .003-6h-.003a3 3 0 0 0-3 2.999V13a3 3 0 0 0 2.999 3zm0 3.973c-2.225-2.078-5.955-3.978-9-3.973v10c3.19 0 6.85 2.004 9 4 2.226-2.078 5.955-4.005 9-4V16c-3.044-.005-6.774 1.895-9 3.973'/></svg>",
"folder-license-open": "<svg viewBox='0 0 16 16'><path fill='#ff5722' d='M14.48 6H4.72a1 1 0 0 0-.95.68L2 12V5h12a1 1 0 0 0-1-1H7.56a1 1 0 0 1-.64-.23l-.64-.54A1 1 0 0 0 5.64 3H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h11l2.4-5.6a1 1 0 0 0-.92-1.4'/><path fill='#ffccbc' d='M11 4.5a4.5 4.5 0 0 0-3 7.85V16l3-1.12L14 16v-3.65a4.5 4.5 0 0 0-3-7.85M11 6a3 3 0 1 1 0 6 3 3 0 0 1 0-6m0 1a2 2 0 1 0 0 4 2 2 0 0 0 0-4'/></svg>",
"folder-license": "<svg viewBox='0 0 16 16'><path fill='#ff5722' d='m6.92 3.77-.64-.54A1 1 0 0 0 5.64 3H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H7.56a1 1 0 0 1-.64-.23'/><path fill='#ffccbc' d='M11 4.5a4.5 4.5 0 0 0-3 7.85V16l3-1.12L14 16v-3.65a4.5 4.5 0 0 0-3-7.85M11 6a3 3 0 1 1 0 6 3 3 0 0 1 0-6m0 1a2 2 0 1 0 0 4 2 2 0 0 0 0-4'/></svg>",
"folder-link-open": "<svg viewBox='0 0 1024 1024'><path fill='#7e57c2' d='M926.912 384H302.144a64 64 0 0 0-60.736 43.776L128 768V320h768a64 64 0 0 0-64-64H483.968a64 64 0 0 1-40.96-14.848l-41.216-34.304A64 64 0 0 0 360.832 192H128a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h704l153.792-358.784A64 64 0 0 0 926.912 384'/><path fill='#d1c4e9' d='M736 320c-53.02 0-96 42.98-96 96 .104 40.593 25.729 76.733 64 90.264V576h-32c-17.673 0-32 14.327-32 32s14.327 32 32 32h32v190.547C595.489 821.653 512 768.467 512 704h64L448 576v128c0 106.039 128.942 192 288 192s288-85.961 288-192V576L896 704h64c0 64.467-83.489 117.653-192 126.547V640h32c17.673 0 32-14.327 32-32s-14.327-32-32-32h-32v-69.736c38.271-13.531 63.896-49.671 64-90.264 0-53.02-42.98-96-96-96m0 64c17.673 0 32 14.327 32 32s-14.327 32-32 32-32-14.327-32-32 14.327-32 32-32'/></svg>",
"folder-link": "<svg viewBox='0 0 1024 1024'><path fill='#7e57c2' d='m443.008 241.152-41.216-34.304A64 64 0 0 0 360.832 192H128a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h768a64 64 0 0 0 64-64V320a64 64 0 0 0-64-64H483.968a64 64 0 0 1-40.96-14.848'/><path fill='#d1c4e9' d='M736 320c-53.02 0-96 42.98-96 96 .104 40.593 25.729 76.733 64 90.264V576h-32c-17.673 0-32 14.327-32 32s14.327 32 32 32h32v190.547C595.489 821.653 512 768.467 512 704h64L448 576v128c0 106.039 128.942 192 288 192s288-85.961 288-192V576L896 704h64c0 64.467-83.489 117.653-192 126.547V640h32c17.673 0 32-14.327 32-32s-14.327-32-32-32h-32v-69.736c38.271-13.531 63.896-49.671 64-90.264 0-53.02-42.98-96-96-96m0 64c17.673 0 32 14.327 32 32s-14.327 32-32 32-32-14.327-32-32 14.327-32 32-32'/></svg>",
"folder-linux-open": "<svg viewBox='0 0 32 32'><path fill='#f9a825' d='M28.967 12H9.442a2 2 0 0 0-1.898 1.368L4 24V10h24a2 2 0 0 0-2-2H15.124a2 2 0 0 1-1.28-.464l-1.288-1.072A2 2 0 0 0 11.276 6H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h22l4.805-11.212A2 2 0 0 0 28.967 12'/><path fill='#ffecb3' d='M24.62 16.35c-.42.28-1.75 1.04-1.95 1.19a.825.825 0 0 1-1.14-.01c-.2-.16-1.53-.92-1.95-1.19-.48-.309-.45-.699.08-.919a6.16 6.16 0 0 1 4.91.03c.49.21.51.6.05.9Zm7.218 7.279A19.1 19.1 0 0 0 28 17.971a4.3 4.3 0 0 1-1.06-1.88c-.1-.33-.17-.67-.24-1.01a11.3 11.3 0 0 0-.7-2.609 4.06 4.06 0 0 0-3.839-2.47 4.2 4.2 0 0 0-3.95 2.4 6 6 0 0 0-.46 1.34c-.17.76-.32 1.55-.5 2.319a3.4 3.4 0 0 1-.959 1.71 19.5 19.5 0 0 0-3.88 5.348 6 6 0 0 0-.37.88c-.19.66.29 1.12.99.96.44-.09.88-.18 1.3-.31.41-.15.57-.05.67.35a6.73 6.73 0 0 0 4.24 4.498c4.119 1.56 8.928-.66 9.968-4.578.07-.27.17-.37.47-.27.46.14.93.24 1.4.35a.724.724 0 0 0 .92-.64 1.44 1.44 0 0 0-.16-.73Z'/></svg>",
@ -829,11 +833,13 @@
"latex.clone": "<svg viewBox='0 0 1024 1024'><path fill='#00bfa5' d='M80 192 64 320h32c16-80 16-96 63.242-96H176c8.837 0 16 7.163 16 16v352c0 8.837 0 16-32 16h-32v32h192v-32h-32c-32 0-32-7.163-32-16V240c0-8.837 7.163-16 16-16h16c48 0 48 16 64 96h32l-16-128zm560 0v32c16 0 45.713 0 52.57 16L776 434.666 708.57 592c-6.857 16-52.57 16-68.57 16v32h128v-32s-34.285 0-27.428-16L792 472l51.428 120c3.103 7.24-1.52 16-11.428 16v32h128v-32c-16 0-45.713 0-52.57-16L824 397.334 891.43 240c6.857-16 52.57-16 68.57-16v-32H832v32s34.285 0 27.428 16L808 360l-51.428-120c-3.103-7.24 1.52-16 11.428-16v-32zM320 384v32h32c32 0 32 7.163 32 16v352c0 8.837 0 16-32 16h-32v32h304l16-128h-32c-16 80-16 96-64 96h-64c-32 0-32-7.163-32-16V624h80c8.837 0 16 0 16 32v16h32V544h-32v16c0 32-7.163 32-16 32h-80V432c0-8.837 0-16 32-16h64c48 0 48 16 64 96h32l-16-128z'/></svg>",
"latexmk": "<svg viewBox='0 0 1024 1024'><path fill='#00bfa5' d='M1024 128q-384 0-576 384L256 896l64-64 64-96c64 0 192-64 224-160-64 16-96-32-96-64 224 0 272-96 352-224 37.924-60.678 80-128 160-160M80 192 64 320h32c16-80 16-96 63.242-96H176c8.837 0 16 7.163 16 16v352c0 8.837 0 16-32 16h-32v32h192v-32h-32c-32 0-32-7.163-32-16V240c0-8.837 7.163-16 16-16h16c48 0 48 16 64 96h32l-16-128z'/></svg>",
"lbx": "<svg viewBox='0 0 1024 1024'><path fill='#2e7d32' d='M128 704v128c0 70.692 57.308 128 128 128h608c17.728 0 32-14.272 32-32V704z'/><path fill='#ffe082' d='M704 704v192h128V704z'/><path fill='#fff8e1' d='M192 704v96c0 53.184 42.816 96 96 96h544a96 96 0 0 1-96-96 96 96 0 0 1 96-96z'/><path fill='#ff1744' d='M320 832h192v192l-96-96-96 96z'/><path fill='#4caf50' d='M256 64c-70.692 0-128 57.308-128 128v640c0 11.088 1.557 21.787 4.207 32.047C146.767 807.565 197.672 768.07 256 768h608c17.728 0 32-14.272 32-32V96c0-17.728-14.272-32-32-32z'/><path fill='#e8f5e9' d='M448 128v64H256v64h256c0 33.778-26.676 86.11-73.947 144.959-7.257 9.034-15.068 18.276-23.123 27.611-4.479-5.242-8.838-10.461-13.002-15.634C370.55 373.95 352 336 352 320h-64c0 48 29.45 90.049 64.072 133.064 6.302 7.83 12.933 15.627 19.68 23.39-35.13 37.553-74.249 76.79-114.379 116.919l45.254 45.254c38.924-38.924 77.278-77.307 112.568-114.883 17.182 17.87 34.328 35.033 50.178 50.883l45.254-45.254c-16.799-16.799-34.744-34.801-52.309-53.17 10.301-11.813 20.31-23.56 29.63-35.162C538.675 377.889 576 318.222 576 256h128v-64H512v-64zm192 192L512 704h64l21.334-64h149.332L768 704h64L704 320zm32 96 53.334 160H618.666z'/></svg>",
"lean": "<svg viewBox='-2 -2 4.5 4.5'><path fill='#448aff' d='m-1.353-1.719-.366.188 1.97 3.75 1.968-3.75-.366-.188-.871 1.66H-.482zM-.313.25H.813L.25 1.375z' color='#000' style='-inkscape-stroke:none'/></svg>",
"lefthook": "<svg viewBox='0 0 32 32'><path fill='#f44336' d='M6 16v6H2zm5.106-6.537-3.317 1.775a2.22 2.22 0 0 0-.895 2.873l.333.71L14 11.571v-.193a2.006 2.006 0 0 0-2.894-1.915m18.82 7.545a2 2 0 0 0-.393-.744l-7.89-8.883a2.76 2.76 0 0 0-3.138-.384L16 8v4.559a3.97 3.97 0 0 1-1.566 3.18L16 20l8.457 2.204 4.624-2.979a2 2 0 0 0 .845-2.217'/><path fill='#b71c1c' fill-rule='evenodd' d='m2 22 4-2 4 2-4 2zm12.434-6.262a6 6 0 0 1-1.194.695l-2.544 1.136A6.55 6.55 0 0 1 8 18v.764l9.71 4.855a4.05 4.05 0 0 0 2.343.366 7.8 7.8 0 0 0 2.667-.82 24 24 0 0 0 1.737-.96zm-6.97-1.635 5.829-2.937a.5.5 0 0 1 .712.475c.007.417-.005.871-.005 1.153a2.1 2.1 0 0 1-1.367 2.03l-2.987 1.067c-1.629.581-3.103-1.324-2.182-1.788'/></svg>",
"lerna": "<svg viewBox='0 0 24 24'><path fill='#448aff' d='M7.57 8.21c-.41.08-.99.06-1.26.06-.4 0-1.27-.48-1.75-.98-.25-.26-.71-.82-1.01-1.25-.31-.43-.62-.78-.71-.78-.17 0-.28.6-.29 1.6 0 .12-.01.22-.01.31l.01.03-.14 8.47c.27.07.54.13.82.19.58.12 1.06.22 1.08.23.01.02-.19.34-.45.71-.95 1.35-1.14 2-.6 2 .17 0 .53-.07.82-.17.33-.1.93-.16 1.58-.16 1.34 0 2.68.33 4.37 1.08 1.61.71 2.14.88 4 1.25.85.18 1.59.37 1.64.41.05.05-.07.22-.28.38s-.38.33-.38.37.22.04.51-.01c.27-.04.89-.14 1.36-.19 1.43-.18 2.45-.82 2.47-1.56.04-.94-.25-3.03-.43-3.14-.02-.01-.38.11-.46.24-.07.13-.16.33-.25.56-.18.46-.15.56-1.57.66-1.03.05-2.02-.19-2.82-.65-.67-.39-1.66-.74-2.18-1.54-.42-.65-.86-1.72-.78-1.87.03-.04.17-.03.32.02.39.15.96.38 2.73.15 1.54-.2 3-.04 3.39.16s.92.47.94.99c.01.52.33.63.36.63.17-.03.59-1.42.83-1.83.2-.32.31-.5 1.1-1.04.62-.43 1.13-.83 1.11-.9-.01-.06-.36-.47-.77-.92-.65-.7-.83-.82-1.42-1.02-2.05-.67-4.23-2.12-5.36-3.54-1.25-1.58-1.6-1.94-2.5-2.53-1.01-.67-2.32-1.24-3.3-1.43-1-.19-.72.13-.29.8 1.53 1.75 1.49 1.81 1.49 2.97-.16.9-1 1.03-1.92 1.24m3.17 1.03c.17 0 .58.11 1.29.35 1.74.58 1.84.85 2.41 1.85.13.22.22.41.21.42s-.17.05-.35.09c-.45.09-1.16.09-1.58 0-.76-.16-.72-.19-1.11-.57-.16-.16-.35-.42-.41-.56-.04-.1-.03-.12.19-.3.05-.04.1-.11.1-.15 0-.07-.22-.37-.34-.47-.13-.11-.49-.54-.49-.59 0-.03.03-.05.1-.05z'/></svg>",
"less": "<svg viewBox='0 0 24 24'><path fill='#0277bd' d='M8 3a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2H3v2h1a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h2v-2H8v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2V3m6 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3z'/></svg>",
"liara": "<svg viewBox='0 0 16 16'><defs><linearGradient id='a' x1='11.328' x2='90.301' y1='8.203' y2='10.761' gradientTransform='matrix(.15939 0 0 .16254 -.134 -.857)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#69f0ae'/><stop offset='1' stop-color='#4fc3f7'/></linearGradient><linearGradient id='b' x1='11.328' x2='90.301' y1='8.203' y2='10.761' gradientTransform='matrix(.15939 0 0 .16253 .195 -.856)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#69f0ae'/><stop offset='1' stop-color='#4fc3f7'/></linearGradient><linearGradient id='c' x1='11.328' x2='90.301' y1='8.203' y2='10.761' gradientTransform='matrix(.15784 0 0 .16493 .15 -.355)' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#69f0ae'/><stop offset='1' stop-color='#4fc3f7'/></linearGradient></defs><path fill='url(#a)' d='M8.5 8.811c0-.53.368-1.174.82-1.44l3.86-2.257c.452-.265.82-.052.82.479v4.596c0 .527-.368 1.17-.82 1.436l-3.86 2.261c-.452.265-.82.051-.82-.479zm0 0'/><path fill='url(#b)' d='M2 5.593c0-.53.368-.745.82-.479l3.86 2.258c.452.264.82.909.82 1.44v4.595c0 .53-.368.744-.82.48l-3.86-2.262c-.452-.264-.82-.909-.82-1.44zm0 0'/><path fill='url(#c)' d='M3.336 4.467c-.448-.27-.448-.706 0-.972l3.821-2.293c.447-.27 1.173-.27 1.62 0l3.888 2.332c.447.268.447.702 0 .971L8.843 6.799c-.447.268-1.173.268-1.624 0zm0 0'/></svg>",
"lib": "<svg fill='none' viewBox='0 0 24 24'><path d='M0 0h24v24H0z'/><path fill='#8bc34a' d='M4 6H2v14c0 1.1.9 2 2 2h14v-2H4zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m0 14H8V4h12zM10 9h8v2h-8zm0 3h4v2h-4zm0-6h8v2h-8z'/></svg>",
"license": "<svg viewBox='0 0 16 16'><path fill='#ff5722' d='M8 1a5.5 5.5 0 0 0-4 9.26V15l4-1.5 4 1.5v-4.74A5.49 5.49 0 0 0 8 1m0 1.5a4 4 0 1 1 0 8 4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4'/></svg>",
"lighthouse": "<svg viewBox='0 0 24 24'><path fill='#f4511e' d='M8.852 10.182V8.364h.851V4.727h-.851v-.909L12.258 2l3.407 1.818v.91h-.852v3.636h.852v1.818h-1.073L9.226 13.49l.477-3.31h-.851zm4.258-1.818V4.727h-1.703v3.637zM8 22l.034-.218L15.792 17l.443 3.073L13.11 22zm.894-6.21L15.077 12l.443 3.064-7.154 4.409z'/></svg>",
"lilypond": "<svg viewBox='0 0 32 32'><path fill='#66bb6a' d='M10 8v11.023A4.986 4.986 0 1 0 11.9 24h.1V11.6l16-3.2v8.623A4.986 4.986 0 1 0 29.9 22h.1V4Z'/></svg>",
"lintstaged": "<svg viewBox='0 0 16 16'><path fill='#ff5252' d='M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7m0 2a5 5 0 0 1 5 5 5 5 0 0 1-.842 2.744L5.256 3.842A5 5 0 0 1 8 3M3.842 5.256l6.902 6.902A5 5 0 0 1 8 13a5 5 0 0 1-5-5 5 5 0 0 1 .842-2.744'/></svg>",
@ -846,6 +852,7 @@
"lottie": "<svg viewBox='0 0 32 32'><path fill='#00bfa5' d='M2 4v24a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2m20.237 8.11c-2.974.426-3.518 2.058-4.34 4.523-.92 2.764-2.145 6.436-7.635 7.217a1.996 1.996 0 1 1-.499-3.96c2.974-.426 3.518-2.058 4.34-4.523.92-2.764 2.145-6.436 7.635-7.217a1.996 1.996 0 1 1 .499 3.96'/></svg>",
"lua": "<svg viewBox='0 0 32 32'><path fill='#42a5f5' d='M30 6a3.86 3.86 0 0 1-1.167 2.833 4.024 4.024 0 0 1-5.666 0A3.86 3.86 0 0 1 22 6a3.86 3.86 0 0 1 1.167-2.833 4.024 4.024 0 0 1 5.666 0A3.86 3.86 0 0 1 30 6m-9.208 5.208A10.6 10.6 0 0 0 13 8a10.6 10.6 0 0 0-7.792 3.208A10.6 10.6 0 0 0 2 19a10.6 10.6 0 0 0 3.208 7.792A10.6 10.6 0 0 0 13 30a10.6 10.6 0 0 0 7.792-3.208A10.6 10.6 0 0 0 24 19a10.6 10.6 0 0 0-3.208-7.792m-1.959 7.625a4.024 4.024 0 0 1-5.666 0 4.024 4.024 0 0 1 0-5.666 4.024 4.024 0 0 1 5.666 0 4.024 4.024 0 0 1 0 5.666'/></svg>",
"luau": "<svg fill='none' viewBox='0 0 24 24'><path fill='#03a9f4' d='M22.495 6.331 6.33 2 2 18.164l16.164 4.33z'/><path fill='#fafafa' d='M19.933 7.81 16.7 6.944l-.866 3.233 3.233.866z'/></svg>",
"lynx": "<svg fill='none' viewBox='0 0 24 24'><path fill='#ff3d00' fill-rule='evenodd' d='m7.604 5.868-2.71 1.98a1.47 1.47 0 0 0-.57.905l-.266 1.352a.45.45 0 0 1-.09.194l-1.23 1.71c-.166.205-.162.748.292 1.095.174.161.406.522.67.932.56.868 1.264 1.958 1.858 1.845.84-.313 1.868-.413 2.658 0 1.534 1.405 1.158 2.701.617 4.56-.217.747-.46 1.585-.617 2.559.737-2.846 2.394-6.151 5.383-7.273-.538-.466-1.637-.9-2.61-1.004 0 0 2.988-2.714 6.673-3.96-2.572-6.493-6.819-9.596-6.819-9.596a.42.42 0 0 0-.743.159c-.073 1.026-.193 1.724-.39 2.382L8.215 1.88c-.156-.2-.468-.084-.467.172.253 1.492.218 2.328-.145 3.816m1.051-.136h.01a.4.4 0 0 0 .057-.009zm2.02-3.348c1.024 1.876 1.488 2.942 1.645 4.93-1.089-.656-1.563-.822-2.364-.818.468-1.507.61-2.414.72-4.112' clip-rule='evenodd'/><path fill='#ff3d00' d='M17.253 16.316C13.084 17.328 10.716 18.85 8.905 23c3.251-5.785 13.092-4.75 13.092-4.75-.183-.946-2.14-2.748-3.427-3.853 0 0 1.038-1.354 3.333-2.04 0 0-4.412.272-6.981 1.743.832.466 1.898 1.252 2.33 2.216'/></svg>",
"lyric": "<svg viewBox='0 0 32 32'><path fill='#50c5ef' d='M26 7.7c0-.1-.1-.2-.2-.3l-5.2-5.2c-.1-.1-.3-.2-.4-.2h-.1q-.15 0 0 0H7.7c-1 .1-1.7.8-1.7 1.8v24.5c0 1 .8 1.7 1.7 1.7h16.6c1 0 1.7-.8 1.7-1.7zq0 .15 0 0M22 12h-4v8c0 2.2-1.8 4-4 4s-4-1.8-4-4 1.8-4 4-4c.7 0 1.4.2 2 .6V8h6z'/></svg>",
"makefile": "<svg viewBox='0 0 32 32'><path fill='#ef5350' d='m29.5 24.02-1.6-.92a4.4 4.4 0 0 0 .09-.9A1.3 1.3 0 0 0 28 22a5.6 5.6 0 0 0-.1-1.1l1.6-.92a.493.493 0 0 0 .18-.68l-1.5-2.6a.45.45 0 0 0-.18-.18V6.01a2.006 2.006 0 0 0-2-2H4a2.006 2.006 0 0 0-2 2V22a2.006 2.006 0 0 0 2 2h10.53l-.03.02a.493.493 0 0 0-.18.68l1.5 2.6a.493.493 0 0 0 .68.18l1.6-.92a5.9 5.9 0 0 0 1.9 1.09v1.85a.495.495 0 0 0 .5.5h3a.495.495 0 0 0 .5-.5v-1.85a5.9 5.9 0 0 0 1.9-1.09l1.6.92a.493.493 0 0 0 .68-.18l1.5-2.6a.493.493 0 0 0-.18-.68M24 22.01a1.99 1.99 0 0 1-.88 1.65l-.18.11a2.04 2.04 0 0 1-1.88 0l-.18-.11a1.99 1.99 0 0 1-.88-1.65V22a2 2 0 0 1 .88-1.66l.18-.11a2.04 2.04 0 0 1 1.88 0l.18.11A2 2 0 0 1 24 22Zm2-4.63-.1.06a5.9 5.9 0 0 0-1.9-1.09V14.5a.495.495 0 0 0-.5-.5h-3a.495.495 0 0 0-.5.5v1.85a5.9 5.9 0 0 0-1.9 1.09l-1.6-.92a.493.493 0 0 0-.68.18l-1.5 2.6a.493.493 0 0 0 .18.68l1.6.92A5.6 5.6 0 0 0 16 22v.01L4 22V10.01h22Z'/></svg>",
"markdoc-config": "<svg viewBox='0 0 32 32'><path fill='#757575' d='M15 2H6a2.006 2.006 0 0 0-2 2v22a2.006 2.006 0 0 0 2 2h6v-4H6v-2h6v-2H6v-2h6v-2H6v-2h6v-2h2V4l8 8h2v-1Z'/><path fill='#ffb300' d='M12 12v18h18V12Zm10 14h-2v-6l-2 2-2-2v6h-2V16h2l2 2 2-2h2Zm6 2h-4V14h4Z'/></svg>",
@ -922,7 +929,7 @@
"opentofu": "<svg data-name='Layer 1' viewBox='0 0 32 32'><g stroke-width='.75'><path fill='#ffca28' d='M15.767 2.065a.44.44 0 0 1 .458 0l10.164 6.14c.33.197.33.716 0 .912l-10.164 6.14a.44.44 0 0 1-.458 0L5.603 9.118c-.33-.196-.33-.715 0-.912z'/><path fill='#fafafa' d='M27.299 10.556c.315-.189.701.063.701.456v12.273c0 .189-.093.37-.243.456l-10.264 6.195c-.315.189-.701-.063-.701-.456V17.207c0-.188.093-.37.243-.456z'/><path fill='#ffd600' d='M14.959 16.751 4.702 10.556c-.315-.189-.701.063-.701.456v12.273c0 .188.093.37.243.456l10.264 6.195c.315.189.701-.063.701-.456V17.207a.53.53 0 0 0-.243-.456zm-6.657 3.908-2.405-1.392v-.016c.058-.825.637-1.18 1.303-.794s1.16 1.36 1.102 2.186zm3.787 2.397-2.405-1.391v-.016c.057-.825.637-1.18 1.303-.794s1.16 1.36 1.102 2.186z'/></g></svg>",
"opentofu_light": "<svg data-name='Layer 1' viewBox='0 0 32 32'><g stroke-width='.657'><path fill='#263238' fill-rule='evenodd' d='M16.875 2.24a1.71 1.71 0 0 0-1.75 0L6.148 7.568s-.013.007-.02.014l-1.181.703C4.36 8.625 4 9.288 4 10.004V21.99c0 .717.36 1.379.941 1.727l9.064 5.381.038.02 1.081.643c.544.32 1.207.32 1.751 0l1.087-.642s.02-.014.032-.02l9.064-5.382c.581-.348.942-1.004.942-1.72V10.003c0-.716-.36-1.379-.942-1.72l-1.176-.697s-.019-.013-.025-.013zM17.45 16l8.4-4.985s.02-.014.026-.014l.1-.061c.279-.164.62.054.62.396v9.327c0 .342-.341.56-.62.397l-.075-.048s-.032-.02-.05-.028zM6.123 10.995s.013.007.02.013l8.4 4.985-8.395 4.991-.044.028-.082.048c-.278.163-.62-.055-.62-.397V11.33c0-.341.342-.56.62-.396l.107.061zm19.063-2.097-7.87-4.67c-.277-.164-.612.047-.619.382v10.113l8.49-5.04a.474.474 0 0 0 0-.778zm-18.38.779 8.488 5.039V4.61c-.006-.335-.348-.546-.62-.382l-7.869 4.67c-.278.178-.278.608 0 .779m.012 13.425a.468.468 0 0 1-.02-.779l8.502-5.046V27.39c-.006.328-.335.54-.607.396L6.818 23.11zm9.88 4.267V17.27l8.5 5.047c.266.184.26.614-.025.778l-7.869 4.678c-.272.15-.594-.062-.607-.39z'/><path fill='#ffca28' d='M15.795 3.559a.39.39 0 0 1 .405 0l8.975 5.333c.29.17.29.621 0 .792L16.2 15.017a.39.39 0 0 1-.405 0L6.82 9.684a.47.47 0 0 1 0-.792z'/><path fill='#ffd600' d='M5.397 11.329c0-.341.341-.56.62-.396l9.063 5.38a.46.46 0 0 1 .215.397v10.659c0 .341-.341.56-.62.396L5.619 22.39a.46.46 0 0 1-.215-.396V11.329z'/><path fill='#fafafa' d='M25.977 10.933c.278-.164.62.055.62.396v10.66c0 .163-.083.32-.215.395l-9.064 5.38c-.279.165-.62-.054-.62-.395v-10.66c0-.163.082-.32.215-.395z'/><path d='M9.202 19.694v.014l-2.124-1.209v-.014c.05-.717.563-1.024 1.15-.69.588.335 1.025 1.182.974 1.899m3.344 2.083v.014l-2.124-1.209v-.014c.05-.717.563-1.024 1.15-.69.588.335 1.025 1.182.974 1.899' class='cls-4'/></g></svg>",
"otne": "<svg viewBox='0 0 1024 1024'><g fill='#00c853'><path d='M512 254.16A257.84 257.84 0 0 0 254.16 512 257.84 257.84 0 0 0 512 769.841a257.84 257.84 0 0 0 257.841-257.84 257.84 257.84 0 0 0-257.84-257.842zM941.74 512A429.74 429.74 0 0 1 512 941.74 429.74 429.74 0 0 1 82.262 512 429.74 429.74 0 0 1 512 82.262 429.74 429.74 0 0 1 941.74 512'/><path d='M695.945 450.836h-92.08l-.005 122.318h92.084zm-122.854-122.78H450.92v367.89h122.17zm-152.942 122.78h-92.084v122.318h92.08z'/></g></svg>",
"oxlint": "<svg viewBox='0 0 16 16'><path fill='#ff7043' d='M8 1a2.5 2.5 0 0 0-2.5 2.5 2.5 2.5 0 0 0 1.75 2.38V7H5v1h2.25v5.5C5.467 13.193 3.897 12.523 3 11l1-1-3-2v2c0 2.266 2.88 5 7 5 2.764.13 7-1.779 7-5V8l-3 2 1 1c-.755 1.631-2.591 2.091-4.25 2.5V8H11V7H8.75V5.88A2.5 2.5 0 0 0 10.5 3.5 2.5 2.5 0 0 0 8 1m0 1.25A1.25 1.25 0 0 1 9.25 3.5 1.25 1.25 0 0 1 8 4.75 1.25 1.25 0 0 1 6.75 3.5 1.25 1.25 0 0 1 8 2.25'/></svg>",
"oxc": "<svg viewBox='0 0 16 16'><path fill='#ff7043' d='M8 1a2.5 2.5 0 0 0-2.5 2.5 2.5 2.5 0 0 0 1.75 2.38V7H5v1h2.25v5.5C5.467 13.193 3.897 12.523 3 11l1-1-3-2v2c0 2.266 2.88 5 7 5 2.764.13 7-1.779 7-5V8l-3 2 1 1c-.755 1.631-2.591 2.091-4.25 2.5V8H11V7H8.75V5.88A2.5 2.5 0 0 0 10.5 3.5 2.5 2.5 0 0 0 8 1m0 1.25A1.25 1.25 0 0 1 9.25 3.5 1.25 1.25 0 0 1 8 4.75 1.25 1.25 0 0 1 6.75 3.5 1.25 1.25 0 0 1 8 2.25'/></svg>",
"packship": "<svg viewBox='0 0 16 16'><path fill='#9575cd' d='M2.997 14.967c-.344-.086-.654-.345-.765-.64-.136-.36-.157-.283 1.119-4.186s1.181-3.58 1.01-3.467c-.741.491-1.703.082-1.95-.83-.175-.648.027-1.006.899-1.587 1.274-.85 1.979-1.476 2.603-2.312.492-.659.682-.798 1.194-.873.623-.09 1.204.138 1.418.56.075.147.02.153.417-.053 4.991-2.592 6.93 4.168 2.114 7.369-1.361.904-3.966 1.598-4.84 1.29l-.128-.045-.144.439c-.08.241-.378 1.16-.663 2.042-.622 1.929-.645 1.972-1.152 2.21-.228.106-.854.153-1.132.083m.729-.772c.272-.068.224.052 1.27-3.18 1.267-3.918 1.463-4.385 2.38-5.681 2.03-2.868 4.835-3.397 4.577-.864-.218 2.145-2.177 3.72-5 4.02-.493.052-.577.117-.647.496-.1.542.18.631 1.35.431 3.043-.52 5.106-2.398 5.391-4.906.365-3.207-3.167-3.65-5.56-.698-.289.356-.294.418.059-.698.392-1.241.389-1.266-.198-1.266-.306 0-.397.047-.604.31-.072.09-.243.31-.38.486-.58.745-1.443 1.49-2.632 2.275-.652.43-.708.542-.468.922.157.247.337.32.56.226.238-.099 1.576-1.11 2.002-1.51.124-.118.226-.205.226-.193s-.7 2.17-1.555 4.795a462 462 0 0 0-1.556 4.82c0 .198.407.31.785.215m3.962-6.61c2.008-.431 3.407-1.742 3.486-3.266.031-.597-.03-.691-.436-.666-1.12.068-3.05 2.1-3.756 3.956l-.034.087.218-.022a6 6 0 0 0 .522-.088'/></svg>",
"palette": "<svg viewBox='0 0 16 16'><path fill='#4fc3f7' d='M12.278 8a1.167 1.167 0 0 1-1.167-1.167 1.167 1.167 0 0 1 1.167-1.166 1.167 1.167 0 0 1 1.166 1.166A1.167 1.167 0 0 1 12.278 8M9.944 4.889a1.167 1.167 0 0 1-1.166-1.167 1.167 1.167 0 0 1 1.166-1.166 1.167 1.167 0 0 1 1.167 1.166A1.167 1.167 0 0 1 9.944 4.89m-3.888 0a1.167 1.167 0 0 1-1.167-1.167 1.167 1.167 0 0 1 1.167-1.166 1.167 1.167 0 0 1 1.166 1.166A1.167 1.167 0 0 1 6.056 4.89M3.722 8a1.167 1.167 0 0 1-1.166-1.167 1.167 1.167 0 0 1 1.166-1.166A1.167 1.167 0 0 1 4.89 6.833 1.167 1.167 0 0 1 3.722 8M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 1.167 1.167 0 0 0 1.167-1.167c0-.303-.117-.575-.304-.777a1.2 1.2 0 0 1-.295-.778 1.167 1.167 0 0 1 1.166-1.167h1.377A3.89 3.89 0 0 0 15 7.222C15 3.784 11.866 1 8 1'/></svg>",
"panda": "<svg viewBox='0 0 24 24'><path fill='#ffd740' d='M4.524 20.862c-.258-.317-.958-2.683-1.319-4.451-1.238-6.075.1-10.397 3.824-12.354 1.596-.838 2.918-1.114 5.37-1.118 3.212-.007 5.102.617 6.808 2.244 2.52 2.403 2.735 6.732.459 9.222-1.267 1.387-4.598 2.82-6.551 2.82h-.593l-.408-1.239c-.224-.68-.456-1.502-.516-1.825l-.108-.586.656.088c.777.104 1.89-.27 2.365-.798.998-1.102.824-3.595-.302-4.333-1.063-.697-3.124-.653-4.166.089-1.888 1.345-1.382 6.248 1.172 11.343.248.495.406.944.351.999-.054.055-1.624.1-3.49.1-2.519 0-3.431-.052-3.552-.2z'/></svg>",
@ -971,7 +978,7 @@
"python": "<svg viewBox='0 0 24 24'><path fill='#0288d1' d='M9.86 2A2.86 2.86 0 0 0 7 4.86v1.68h4.29c.39 0 .71.57.71.96H4.86A2.86 2.86 0 0 0 2 10.36v3.781a2.86 2.86 0 0 0 2.86 2.86h1.18v-2.68a2.85 2.85 0 0 1 2.85-2.86h5.25c1.58 0 2.86-1.271 2.86-2.851V4.86A2.86 2.86 0 0 0 14.14 2zm-.72 1.61c.4 0 .72.12.72.71s-.32.891-.72.891c-.39 0-.71-.3-.71-.89s.32-.711.71-.711'/><path fill='#fdd835' d='M17.959 7v2.68a2.85 2.85 0 0 1-2.85 2.859H9.86A2.85 2.85 0 0 0 7 15.389v3.75a2.86 2.86 0 0 0 2.86 2.86h4.28A2.86 2.86 0 0 0 17 19.14v-1.68h-4.291c-.39 0-.709-.57-.709-.96h7.14A2.86 2.86 0 0 0 22 13.64V9.86A2.86 2.86 0 0 0 19.14 7zM8.32 11.513l-.004.004.038-.004zm6.54 7.276c.39 0 .71.3.71.89a.71.71 0 0 1-.71.71c-.4 0-.72-.12-.72-.71s.32-.89.72-.89'/></svg>",
"pytorch": "<svg viewBox='0 0 32 32'><circle cx='20' cy='8' r='2' fill='#f4511e'/><path fill='#f4511e' d='M25.573 11.335a11.4 11.4 0 0 0-1.1-1.219l-2.825 2.832a9 9 0 0 1 .746.812 7.36 7.36 0 0 1 1.443 3.011 8 8 0 0 1 .164 1.23A8 8 0 0 1 8 18a5.76 5.76 0 0 1 .695-2.762 7.4 7.4 0 0 1 1.277-1.896c.18-.18 1.814-1.746 3.74-3.584L16 7.553V2l-5.057 4.873c-1.112 1.06-3.713 3.545-3.877 3.722a11.5 11.5 0 0 0-1.99 2.942A9.8 9.8 0 0 0 4 18a12 12 0 1 0 24 0 12.6 12.6 0 0 0-.254-2.074 11.26 11.26 0 0 0-2.173-4.591'/></svg>",
"qsharp": "<svg viewBox='0 0 24 24'><path fill='#fbc02d' d='M11.938 16.414c.9-1.101 1.468-2.936 1.468-4.735 0-1.963-.697-3.854-1.872-5.12-1.156-1.248-2.66-1.853-4.57-1.853s-3.413.605-4.569 1.853C1.22 7.825.523 9.716.523 11.716s.697 3.89 1.872 5.157c1.156 1.248 2.68 1.853 4.57 1.853 1.376 0 2.404-.275 3.468-.917l1.578 1.486 1.395-1.486zm-3.395-3.212-1.396 1.487 1.413 1.34c-.422.22-1.027.348-1.615.348-2.202 0-3.67-1.853-3.67-4.66 0-2.809 1.468-4.662 3.689-4.662 2.239 0 3.689 1.835 3.689 4.68 0 1.1-.202 2.092-.606 2.9z' aria-label='Q'/><path fill='#fbc02d' d='m17.589 4.728-.611 4h-1.5l-.34 2h1.5l-.32 2h-1.5l-.34 2h1.5l-.61 4h2l.61-4h1l-.61 4h2l.61-4h1.5l.34-2h-1.5l.32-2h1.5l.34-2h-1.5l.611-4h-2l-.611 4h-1l.611-4zm1.049 6h1l-.32 2h-1z'/></svg>",
"quarto": "<svg viewBox='0 0 4.233 4.233'><path fill='#82b1ff' d='M2.381.26v1.588h1.588C3.904 1.076 3.153.325 2.381.26m-.53 0C1.082.326.331 1.078.264 1.848h1.588zM.264 2.377c.066.77.818 1.521 1.588 1.587V2.377zm2.117 0v1.587c.77-.066 1.521-.818 1.588-1.587z' paint-order='stroke fill markers'/></svg>",
"quarto": "<svg viewBox='0 0 16 16'><path fill='#82b1ff' d='M9 1v6h6c-.246-2.918-3.083-5.754-6-6M7 1c-2.906.25-5.747 3.09-6 6h6zM1 9c.25 2.91 3.09 5.75 6 6V9zm8 0v6c2.91-.25 5.747-3.093 6-6z' paint-order='stroke fill markers'/></svg>",
"quasar": "<svg viewBox='0 0 50.843 50.843'><path fill='#1976d2' d='M29.585 25.422a4.16 4.16 0 0 1-4.16 4.159 4.16 4.16 0 0 1-4.159-4.16 4.16 4.16 0 0 1 4.16-4.159 4.16 4.16 0 0 1 4.159 4.16M43.888 14.76a21.3 21.3 0 0 0-3.267-4.272l-4.808 2.776a16 16 0 0 0-5.02-2.91c-1.642 1.664-2.946 3.523-3.886 5.545 5.352-.364 10.88 1.573 16.012 5.582l3.026-1.747a21.3 21.3 0 0 0-2.057-4.974m.001 21.32a21.3 21.3 0 0 0 2.066-4.966l-4.808-2.776c.359-1.938.356-3.904.01-5.803-2.262-.59-4.524-.79-6.745-.593 2.992 4.454 4.078 10.21 3.172 16.659l3.026 1.747a21.3 21.3 0 0 0 3.28-4.269zM25.427 46.74a21.3 21.3 0 0 0 5.333-.694v-5.552a16 16 0 0 0 5.03-2.893c-.62-2.253-1.578-4.312-2.859-6.137-2.36 4.817-6.802 8.636-12.84 11.076v3.494a21.3 21.3 0 0 0 5.336.706M6.963 36.082a21.3 21.3 0 0 0 3.267 4.272l4.809-2.777a16 16 0 0 0 5.02 2.91c1.642-1.664 2.946-3.523 3.886-5.544-5.353.364-10.88-1.573-16.012-5.583l-3.027 1.747a21.3 21.3 0 0 0 2.057 4.975m-.001-21.32a21.3 21.3 0 0 0-2.066 4.966l4.809 2.776a16 16 0 0 0-.01 5.802c2.262.59 4.524.79 6.744.593-2.991-4.453-4.078-10.209-3.171-16.658l-3.026-1.747a21.3 21.3 0 0 0-3.28 4.269zm18.462-10.66a21.3 21.3 0 0 0-5.333.694v5.552a16 16 0 0 0-5.03 2.893c.62 2.253 1.578 4.312 2.86 6.137 2.36-4.818 6.802-8.636 12.84-11.076V4.808a21.3 21.3 0 0 0-5.337-.706'/></svg>",
"quokka": "<svg viewBox='0 0 16 16'><path fill='#ff6d00' d='M8 2v6H2v6h12V2z' paint-order='fill markers stroke'/></svg>",
"qwik": "<svg viewBox='0 0 24 24'><path fill='#29b6f6' d='m19.26 22.182-3.657-3.636-.056.008v-.04l-7.776-7.678 1.916-1.85-1.126-6.46-5.341 6.62c-.91.917-1.078 2.408-.423 3.508l3.337 5.534a2.8 2.8 0 0 0 2.435 1.356l1.653-.016z'/><path fill='#b388ff' d='m21.255 9.018-.734-1.356-.383-.693-.152-.272-.016.016-2.012-3.484a2.82 2.82 0 0 0-2.467-1.411l-1.765.047-5.261.016A2.82 2.82 0 0 0 6.054 3.27L2.852 9.616 8.577 2.51l7.505 8.245-1.334 1.347.799 6.451.008-.016v.016h-.016l.016.016.623.606 3.025 2.958c.128.12.336-.024.248-.175l-1.868-3.676 3.257-6.02.104-.12c.04-.048.08-.096.112-.143.638-.87.726-2.034.2-2.983z'/><path fill='#eceff1' d='M16.106 10.724 8.576 2.52l1.07 6.427-1.916 1.858 7.8 7.742-.702-6.426z'/></svg>",
@ -1126,6 +1133,7 @@
"uml": "<svg viewBox='0 0 100 100'><path fill='#b39ddb' d='M87 76.652 53.84 93.907l-.038-41.04 13.9-7.15v29.622l19.224-9.85z'/><path fill='#fbc02d' d='m38.693 89.604 8.576 4.303V52.743l-13.027-6.29-4.126 19.643-4.16-23.69L13 36.077V77.28l8.54 4.378V56.826l4.669 26.817 7.599 3.863 4.885-22.293z'/><path fill='#f06292' d='m45.237 6.093-9.775 8.755s19.072 9.931 21.39 11.105c2.317 1.173 5.615 3.43 2.05 6.771s-7.487 2.89-10.16 1.535a21830 21830 0 0 1-22.458-11.466l-10.07 8.667S35.642 41.48 38.85 43.196c3.208 1.715 15.15 5.958 26.47-2.98 11.318-8.937 9.714-12.188 9.714-12.82s-.267-3.972-2.228-6.048c-1.96-2.077-7.664-5.056-10.07-6.32S45.239 6.092 45.239 6.092z'/></svg>",
"uml_light": "<svg viewBox='0 0 100 100'><path fill='#9575cd' d='M67.702 45.716V75.34l19.224-9.85L87 76.653 53.84 93.907l-.038-41.04z'/><path fill='#f9a825' d='m30.116 66.096-4.16-23.69L13 36.077V77.28l8.54 4.378V56.826l4.669 26.817 7.599 3.863 4.885-22.293v24.391l8.576 4.303V52.743l-13.027-6.29z'/><path fill='#ec407a' d='m45.237 6.093-9.775 8.755s19.072 9.931 21.39 11.105c2.317 1.174 5.615 3.43 2.05 6.772-3.565 3.34-7.487 2.889-10.16 1.535a21830 21830 0 0 1-22.458-11.468l-10.07 8.667S35.641 41.482 38.85 43.196c3.208 1.716 15.15 5.959 26.47-2.979 11.318-8.938 9.714-12.188 9.714-12.82s-.267-3.972-2.228-6.049c-1.96-2.076-7.664-5.056-10.07-6.32S45.239 6.093 45.239 6.093z'/></svg>",
"unity": "<svg viewBox='0 0 16 16'><path fill='#42a5f5' d='M8 6.5 5 5l2-1V2L2 5v5l2-1V6.5L7 8v4.5L4 11l-2 1 6 3 6-3-2-1-3 1.5V8l3-1.5V9l2 1V5L9 2v2l2 1Z'/></svg>",
"unlicense": "<svg viewBox='0 0 16 16'><path fill='#ff5722' d='m2.5 1.5-1 1 1.57 1.57A5.5 5.5 0 0 0 4 10.26V15l4-1.5 4 1.5v-2l1.5 1.5 1-1zM8 1c-1.14 0-2.19.34-3.07.93l1.1 1.1a4 4 0 0 1 5.45 5.45l1.08 1.08A5.48 5.48 0 0 0 8 1m0 3.5q-.23 0-.45.05l2.4 2.4A2 2 0 0 0 8 4.5m-3.79.71L9.3 10.3a3.99 3.99 0 0 1-5.1-5.1'/></svg>",
"unocss": "<svg viewBox='0 0 32 32'><circle cx='24' cy='24' r='6' fill='#78909c'/><path fill='#546e7a' d='M2 18v6a6 6 0 0 0 12 0v-6Z'/><path fill='#b0bec5' d='M30 14V8a6 6 0 0 0-12 0v6Z'/></svg>",
"url": "<svg viewBox='0 0 32 32'><path fill='#42a5f5' d='M10 14h12v4H10z'/><path fill='#42a5f5' d='M12 22H9.562A5.57 5.57 0 0 1 4 16.438v-.876A5.57 5.57 0 0 1 9.562 10H12V6H9.562A9.56 9.56 0 0 0 0 15.562v.876A9.56 9.56 0 0 0 9.562 26H12ZM22.438 6H20v4h2.438A5.57 5.57 0 0 1 28 15.562v.876A5.57 5.57 0 0 1 22.438 22H20v4h2.438A9.56 9.56 0 0 0 32 16.438v-.876A9.56 9.56 0 0 0 22.438 6'/></svg>",
"uv": "<svg viewBox='0 0 16 16'><path fill='#e040fb' d='M2 2v11c0 .5.5 1 1 1h8c.5 0 1-.5 1-1h1v1h1V2H8v8H7V2z'/></svg>",
@ -1159,6 +1167,8 @@
"wakatime_light": "<svg fill='none' viewBox='0 0 340 340'><path stroke='#455a64' stroke-width='33.39' d='M170 44.788c-69.154 0-125.212 56.058-125.212 125.212s56.058 125.213 125.213 125.213S295.212 239.155 295.212 170 239.155 44.788 170 44.788z'/><path fill='#455a64' d='M186.846 206.343c-1.205 1.588-3.011 2.61-5.035 2.61a6 6 0 0 1-.591-.034 7 7 0 0 1-.7-.109 6.7 6.7 0 0 1-1.15-.385 8 8 0 0 1-.547-.28 6.6 6.6 0 0 1-.856-.591 7 7 0 0 1-.42-.367 8 8 0 0 1-.586-.64 7.5 7.5 0 0 1-.754-1.144l-7.378-11.854-7.374 11.854c-1.157 2.107-3.249 3.55-5.652 3.55-2.412 0-4.514-1.454-5.636-3.607l-32.252-46.985c-1.06-1.278-1.712-2.973-1.712-4.844 0-3.96 2.911-7.173 6.501-7.173 2.324 0 4.358 1.35 5.508 3.375l27.224 40.228 7.663-12.477c1.104-2.224 3.248-3.734 5.71-3.734 2.252 0 4.238 1.266 5.404 3.188l7.903 12.972 42.712-61.15c1.16-1.967 3.164-3.269 5.45-3.269 3.59 0 6.5 3.212 6.5 7.172 0 1.73-.553 3.317-1.478 4.555z'/></svg>",
"wallaby": "<svg viewBox='0 0 32 32'><path fill='#4caf50' d='M16 2v14H2v14h28V2z'/></svg>",
"wally": "<svg viewBox='0 0 32 32'><path fill='#e65100' d='M8.454 3.084c-.897-.112-1.438.473-2.502 2.43-1.457 2.682-3.888 1.135-1.765 5.275a4.7 4.7 0 0 0 .759 1.122l5.749-9.219c-.84-.614-1.513.448-2.241.392'/><path fill='#ffcc80' d='m28.565 29.985-5.982-8.28a1.67 1.67 0 0 0-.57-1.362c-1.794-1.789-2.69-3.51-1.905-6.975a3.94 3.94 0 0 0-1.326-3.766 29 29 0 0 0 .206-3.22s-.673-2.907-2.354-3.13c0 0-4.426-2.236-7.956.279S7.95 5.71 5.82 8.897a4.5 4.5 0 0 0-.444.867 7 7 0 0 0-1.237.307c-1.4.56-1.344 2.07.56 3.635a1 1 0 0 0 .29.148 8.2 8.2 0 0 1-.29 2.198c-.595 1.674.853 3.91 3.024 1.789a7 7 0 0 0 1.648.942 8.1 8.1 0 0 1 .707 2.522l-.558 8.68Z'/><path fill='#3e2723' d='M8.651 9.017a.803.803 0 0 1-.807-.805v-1.88a.807.807 0 0 1 1.615 0v1.88a.803.803 0 0 1-.808.805m5.314 0a.814.814 0 0 1-.813-.811V6.339a.814.814 0 0 1 1.627 0v1.948a.8.8 0 0 1-.814.73'/><path fill='#e65100' d='M17.082 14.15a6.06 6.06 0 0 1-4.818 4.528 4.75 4.75 0 0 1-5.828-3.13 3.8 3.8 0 0 0 2.999 3.679c.84 1.302.355 3.547.307 4.874a5.4 5.4 0 0 1-.061.84l-1.08 5.044 5.421.015c-.724-.397-1.02-2.476-1.14-3.901l-.079-1.405a1.415 1.415 0 0 1 .974-1.432c2.796-1.81-.672-1.789-.672-2.292a3.6 3.6 0 0 1 1.068-2.195c2.535-1.273 3.114-4.013 2.91-4.624'/><path fill='#e65100' d='M20.518 17.614c.588-.523 1.012-2.457 1.492-3.362.785-1.48 4.797-2.989 3.136-5.446-1.55-2.292-2.24-1.658-3.136-3.28-.84-1.452-1.073-1.763-3.154-1.763a7.7 7.7 0 0 1-2.296-1.124A3.07 3.07 0 0 0 14.531 2c-3.98 0-4.9-.091-5.853 1.195 0 0 4.429-1.841 8.025.533 1.213 1.634 2.441 4.61 1.612 9.64a8.8 8.8 0 0 0 .093 3.228 1.5 1.5 0 0 0 .096.466s4.621 9.882 9.222 12.922H29s-5.867-7.368-8.482-12.371M5.035 16.05a2.496 2.496 0 0 0-1.176-3.24 3 3 0 0 1 .56 3.185c-.56 1.118 0 3.242 2.073 2.627 0 0-2.41.112-1.457-2.571'/><path fill='#b71c1c' d='M9.736 23.933a9.63 9.63 0 0 0 8.41-.28c3.195-1.733 3.788-3.732 3.451-4.123a2.42 2.42 0 0 1 2.053 1.036c.56.839-.46 3.645-5.167 5.602-4.427 1.788-7.848.974-8.632.191-.729-.782-.115-2.426-.115-2.426m-6.55-13.192c-.616.615.393 2.46 1.625 3.018 1.401.615 3.418-1.788 3.026-2.459-.448-.894-3.362-1.844-4.65-.559'/></svg>",
"warp": "<svg viewBox='0 0 16 16'><g fill='#cfd8dc'><path d='M8.443 2h5.064C14.362 2 15 2.716 15 3.6v6.8c0 .884-.718 1.6-1.573 1.6H6.224z'/><path d='M7 4H2.533C1.686 4 1 4.715 1 5.599v6.802C1 13.284 1.717 14 2.563 14H7.79L8 13H5z'/></g></svg>",
"warp_light": "<svg viewBox='0 0 16 16'><g fill='#546e7a'><path d='M8.443 2h5.064C14.362 2 15 2.716 15 3.6v6.8c0 .884-.718 1.6-1.573 1.6H6.224z'/><path d='M7 4H2.533C1.686 4 1 4.715 1 5.599v6.802C1 13.284 1.717 14 2.563 14H7.79L8 13H5z'/></g></svg>",
"watchman": "<svg viewBox='0 0 420 419'><circle cx='210' cy='209.5' r='188.15' fill='#fafafa' paint-order='stroke fill markers'/><path fill='#304ffe' d='M191.07 397.1c-35.512-4.049-66.779-16.485-95.318-37.913-22.723-17.061-44.027-43.274-56.077-68.997l-3.932-8.393 25.692-26.37c14.13-14.505 27.522-28.059 29.758-30.12l4.067-3.748 12.5 11.364c16.495 14.996 26.818 23.219 41.05 32.697 14.94 9.95 23.867 14.578 35.877 18.597 22.823 7.637 42.099 5.991 66.082-5.642 17.83-8.65 44.399-28.24 66.179-48.797l8.878-8.38 29.147 29.137c16.03 16.025 29.147 29.84 29.147 30.7s-1.6 4.912-3.554 9.005c-18.398 38.533-49.46 69.834-87.797 88.466-17.732 8.619-33.936 13.787-53.563 17.084-10.16 1.708-38.005 2.465-48.137 1.31zm6.3-123.69c-17.457-3.809-39.276-16.397-63.835-36.829-13.001-10.816-26.615-23.771-26.615-25.327 0-1.265 16.792-17.101 29.7-28.009 20.328-17.179 36.936-27.484 53.753-33.355 6.275-2.19 8.25-2.443 19.147-2.443 10.892 0 12.873.253 19.136 2.44 22.614 7.894 50.68 27.157 79.189 54.35 3.836 3.659 6.975 7.021 6.975 7.472 0 1.1-21.726 20.758-32.4 29.316-19.403 15.557-41.794 28.276-56.169 31.907-8.267 2.088-20.566 2.29-28.881.477zm37.472-20.429c14.201-7.184 18.451-9.747 19.779-11.925 1.556-2.552 1.692-4.934 1.692-29.774 0-24.91-.132-27.216-1.705-29.795-1.34-2.2-5.607-4.783-20.022-12.124-18.175-9.256-18.368-9.329-24.812-9.355-6.44-.026-6.632.044-22.95 8.376-10.12 5.168-17.582 9.58-19.38 11.462l-2.925 3.06.003 27.237c.003 25.164.134 27.452 1.712 30.04 1.301 2.134 5.186 4.602 16.288 10.35 20.15 10.433 22.925 11.538 29.04 11.571 4.82.026 6.487-.627 23.28-9.123m-40.872-12.388c-7.315-3.808-13.914-7.538-14.665-8.29-1.181-1.181-1.28-4.249-.736-22.667l.63-21.3 13.888-7.202c8.096-4.199 14.986-7.202 16.52-7.202 2.953 0 28.618 12.505 31.424 15.311 1.68 1.682 1.788 3.033 1.788 22.477v20.69l-3.46 2.183c-1.902 1.202-8.376 4.65-14.386 7.662-8.03 4.025-11.825 5.448-14.315 5.37-2.329-.075-7.546-2.273-16.688-7.032M29.5 263.432c-1.475-3.866-4.192-15.727-5.967-26.05-2.405-13.986-2.19-43.337.424-58.05 2.41-13.562 4.216-20.627 5.613-21.959.772-.736 7.977 6.088 27.938 26.459 14.794 15.098 26.902 27.655 26.906 27.906s-12.122 12.581-26.948 27.4L30.51 266.082zm333.59-24.851c-14.17-14.479-25.763-26.679-25.763-27.11 0-1.574 51.776-53.76 53.1-53.521 1.493.27 3.338 6.773 5.975 21.06 2.353 12.751 2.343 48.232-.017 61.072-2.37 12.886-5.358 24.1-6.527 24.49-.553.184-12.599-11.512-26.768-25.991M64.75 170.903c-26.893-26.902-29.95-30.252-29.354-32.175 1.335-4.308 8.33-18.529 12.565-25.546 28.808-47.732 75.832-79.809 131.42-89.647 15.231-2.696 45.201-2.944 59.361-.492 41.827 7.244 76.236 24.833 104.91 53.625 15.582 15.647 27.713 32.758 36.971 52.151 6.183 12.95 8.732 8.549-24.429 42.185-15.984 16.213-29.386 29.61-29.782 29.768-.396.16-5.695-4.244-11.775-9.786-32.548-29.67-61.86-48.734-85.116-55.361-7.771-2.215-9.327-2.357-22.05-2.009-12.468.34-14.427.627-21.965 3.212-23.775 8.152-49.675 26.108-80.457 55.78-4.75 4.579-9.006 8.325-9.457 8.325s-14.329-13.513-30.839-30.029z'/></svg>",
"webassembly": "<svg viewBox='0 0 32 32'><path fill='#7c4dff' d='M22 18h4v4h-4z'/><path fill='#7c4dff' d='M20 2a4 4 0 0 1-8 0H2v28h28V2Zm-2 24h-2v2h-4v-2h-2v2H6v-2H4V16h2v10h4V16h2v10h4V16h2Zm10 2h-2v-4h-4v4h-2V18h2v-2h4v2h2Z'/></svg>",
"webhint": "<svg viewBox='0 0 120 120'><path fill='#1e88e5' d='M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422q.638-.64.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957q0-1.493.425-2.986c-4.265-2.702-8.53-3.838-14.787-3.838z'/></svg>",

@ -215,6 +215,7 @@ more = More
buttons.heading.tooltip = Add heading
buttons.bold.tooltip = Add bold text
buttons.italic.tooltip = Add italic text
buttons.strikethrough.tooltip = Add strikethrough text
buttons.quote.tooltip = Quote text
buttons.code.tooltip = Add code
buttons.link.tooltip = Add a link
@ -1354,8 +1355,11 @@ editor.this_file_locked = File is locked
editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file.
editor.fork_before_edit = You must fork this repository to make or propose changes to this file.
editor.delete_this_file = Delete File
editor.delete_this_directory = Delete Directory
editor.must_have_write_access = You must have write access to make or propose changes to this file.
editor.file_delete_success = File "%s" has been deleted.
editor.directory_delete_success = Directory "%s" has been deleted.
editor.delete_directory = Delete directory '%s'
editor.name_your_file = Name your file…
editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field.
editor.or = or

@ -215,6 +215,7 @@ more=Plus
buttons.heading.tooltip=Ajouter un en-tête
buttons.bold.tooltip=Ajouter du texte en gras
buttons.italic.tooltip=Ajouter du texte en italique
buttons.strikethrough.tooltip=Ajouté un texte barré
buttons.quote.tooltip=Citer le texte
buttons.code.tooltip=Ajouter du code
buttons.link.tooltip=Ajouter un lien
@ -1354,8 +1355,11 @@ editor.this_file_locked=Le fichier est verrouillé
editor.must_be_on_a_branch=Vous devez être sur une branche pour appliquer ou proposer des modifications à ce fichier.
editor.fork_before_edit=Vous devez faire bifurquer ce dépôt pour appliquer ou proposer des modifications à ce fichier.
editor.delete_this_file=Supprimer le fichier
editor.delete_this_directory=Supprimer le répertoire
editor.must_have_write_access=Vous devez avoir un accès en écriture pour appliquer ou proposer des modifications à ce fichier.
editor.file_delete_success=Le fichier "%s" a été supprimé.
editor.directory_delete_success=Le répertoire « %s » a été supprimé.
editor.delete_directory=Supprimer le répertoire « %s »
editor.name_your_file=Nommez votre fichier…
editor.filename_help=Ajoutez un dossier en entrant son nom suivi d'une barre oblique ('/'). Supprimez un dossier avec un retour arrière au début du champ.
editor.or=ou
@ -1482,6 +1486,7 @@ projects.column.new_submit=Créer une colonne
projects.column.new=Nouvelle colonne
projects.column.set_default=Définir par défaut
projects.column.set_default_desc=Les tickets et demandes dajout non-catégorisés seront placés dans cette colonne.
projects.column.default_column_hint=Les nouveaux tickets ajoutés à ce projet seront ajoutés dans cette colonne
projects.column.delete=Supprimer la colonne
projects.column.deletion_desc=La suppression dune colonne déplace tous ses tickets dans la colonne par défaut. Continuer ?
projects.column.color=Couleur

@ -215,6 +215,7 @@ more=Níos mó
buttons.heading.tooltip=Cuir ceannteideal leis
buttons.bold.tooltip=Cuir téacs trom leis
buttons.italic.tooltip=Cuir téacs iodálach leis
buttons.strikethrough.tooltip=Cuir téacs trína chéile
buttons.quote.tooltip=Téacs luaigh
buttons.code.tooltip=Cuir cód leis
buttons.link.tooltip=Cuir nasc leis
@ -1354,8 +1355,11 @@ editor.this_file_locked=Tá an comhad faoi ghlas
editor.must_be_on_a_branch=Caithfidh tú a bheith ar bhrainse chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo.
editor.fork_before_edit=Ní mór duit an stór seo a fhorcáil chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo.
editor.delete_this_file=Scrios Comhad
editor.delete_this_directory=Scrios Eolaire
editor.must_have_write_access=Caithfidh rochtain scríofa a bheith agat chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo.
editor.file_delete_success=Tá an comhad "%s" scriosta.
editor.directory_delete_success=Scriosadh an eolaire "%s".
editor.delete_directory=Scrios an eolaire '%s'
editor.name_your_file=Ainmnigh do chomhad…
editor.filename_help=Cuir eolaire leis trína ainm a chlóscríobh ina dhiaidh sin le slash ('/'). Bain eolaire trí backspace a chlóscríobh ag tús an réimse ionchuir.
editor.or=
@ -1482,6 +1486,7 @@ projects.column.new_submit=Cruthaigh Colún
projects.column.new=Colún Nua
projects.column.set_default=Socraigh Réamhshocrú
projects.column.set_default_desc=Socraigh an colún seo mar réamhshocrú le haghaidh saincheisteanna agus tarraingtí gan chatagóir
projects.column.default_column_hint=Cuirfear saincheisteanna nua a chuirtear leis an tionscadal seo leis an gcolún seo
projects.column.delete=Scrios Colún
projects.column.deletion_desc=Ag scriosadh colún tionscadail aistríonn gach saincheist ghaolmhar chuig an gcolún. Lean ar aghaidh?
projects.column.color=Dath
@ -3038,6 +3043,7 @@ dashboard.update_migration_poster_id=Nuashonraigh ID póstaer imir
dashboard.git_gc_repos=Bailitheoir bruscair gach stórais
dashboard.resync_all_sshkeys=Nuashonraigh an comhad '.ssh/authorized_keys' le heochracha SSH Gitea
dashboard.resync_all_sshprincipals=Nuashonraigh an comhad '.ssh/authorized_principals' le príomhoidí SSH Gitea
dashboard.resync_all_hooks=Athshioncrónaigh crúcaí git na stórtha uile (réamhghlacadh, nuashonrú, iarghlacadh, próiseasghlacadh, ...)
dashboard.reinit_missing_repos=Aththosaigh gach stórais Git atá in easnamh a bhfuil taifid ann dóibh
dashboard.sync_external_users=Sioncrónaigh sonraí úsáideoirí seachtracha
dashboard.cleanup_hook_task_table=Glan suas an tábla hook_task

@ -1354,8 +1354,11 @@ editor.this_file_locked=ファイルはロックされています
editor.must_be_on_a_branch=このファイルを変更したり変更の提案をするには、ブランチ上にいる必要があります。
editor.fork_before_edit=このファイルを変更したり変更の提案をするには、リポジトリをフォークする必要があります。
editor.delete_this_file=ファイルを削除
editor.delete_this_directory=ディレクトリを削除
editor.must_have_write_access=このファイルを変更したり変更の提案をするには、書き込み権限が必要です。
editor.file_delete_success=ファイル "%s" を削除しました。
editor.directory_delete_success=ディレクトリ "%s" を削除しました。
editor.delete_directory=ディレクトリ'%s'を削除
editor.name_your_file=ファイル名を指定…
editor.filename_help=ディレクトリを追加するにはディレクトリ名に続けてスラッシュ('/')を入力します。 ディレクトリを削除するには入力欄の先頭でbackspaceキーを押します。
editor.or=または
@ -1482,6 +1485,7 @@ projects.column.new_submit=列を作成
projects.column.new=新しい列
projects.column.set_default=デフォルトに設定
projects.column.set_default_desc=この列を未分類のイシューやプルリクエストが入るデフォルトの列にします
projects.column.default_column_hint=このプロジェクトに追加された新しいイシューがこの列に追加されます
projects.column.delete=列を削除
projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?
projects.column.color=カラー
@ -3038,6 +3042,7 @@ dashboard.update_migration_poster_id=移行する投稿者IDの更新
dashboard.git_gc_repos=すべてのリポジトリでガベージコレクションを実行
dashboard.resync_all_sshkeys='.ssh/authorized_keys' ファイルをGitea上のSSHキーで更新
dashboard.resync_all_sshprincipals='.ssh/authorized_principals' ファイルをGitea上のSSHプリンシパルで更新
dashboard.resync_all_hooks=すべてのリポジトリのGitフックを再同期する (pre-receive, update, post-receive, proc-receive, ...)
dashboard.reinit_missing_repos=レコードが存在するが見当たらないすべてのGitリポジトリを再初期化する
dashboard.sync_external_users=外部ユーザーデータの同期
dashboard.cleanup_hook_task_table=hook_taskテーブルのクリーンアップ

@ -215,6 +215,7 @@ more=Mais
buttons.heading.tooltip=Adicionar cabeçalho
buttons.bold.tooltip=Adicionar texto em negrito
buttons.italic.tooltip=Adicionar texto em itálico
buttons.strikethrough.tooltip=Adicionar texto rasurado
buttons.quote.tooltip=Citar texto
buttons.code.tooltip=Adicionar código-fonte
buttons.link.tooltip=Adicionar uma ligação
@ -1354,8 +1355,11 @@ editor.this_file_locked=Ficheiro bloqueado
editor.must_be_on_a_branch=Tem que estar num ramo para fazer ou propor modificações neste ficheiro.
editor.fork_before_edit=Tem que fazer uma derivação deste repositório para fazer ou propor modificações neste ficheiro.
editor.delete_this_file=Eliminar ficheiro
editor.delete_this_directory=Eliminar pasta
editor.must_have_write_access=Tem que ter permissões de escrita para fazer ou propor modificações neste ficheiro.
editor.file_delete_success=O ficheiro "%s" foi eliminado.
editor.directory_delete_success=A pasta "%s" foi eliminada.
editor.delete_directory=Eliminar a pasta '%s'
editor.name_your_file=Nomeie o seu ficheiro…
editor.filename_help=Adicione uma pasta escrevendo o nome dessa pasta seguido de uma barra('/'). Remova uma pasta carregando na tecla de apagar ('←') no início do campo.
editor.or=ou

@ -426,7 +426,7 @@ need_account=需要一个帐户?
sign_up_tip=您正在系统中注册第一个帐户,它拥有管理员权限。请仔细记住您的用户名和密码。 如果您忘记了用户名或密码,请参阅 Gitea 文档以恢复账户。
sign_up_now=立即注册。
sign_up_successful=帐户创建成功。欢迎!
confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>。请在下一个 %s 中检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。
confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>。请在 %s 内检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。
must_change_password=更新您的密码
allow_password_change=要求用户更改密码(推荐)
reset_password_mail_sent_prompt=确认邮件已被发送到 <b>%s</b>。请您在 %s 内检查您的收件箱 ,完成密码重置流程。
@ -1483,6 +1483,7 @@ projects.column.new_submit=创建列
projects.column.new=创建列
projects.column.set_default=设为默认
projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值
projects.column.default_column_hint=添加到此项目的新议题将被添加到此列
projects.column.delete=删除列
projects.column.deletion_desc=删除项目列会将所有相关问题移至默认列。是否继续?
projects.column.color=颜色
@ -1970,6 +1971,9 @@ pulls.status_checks_requested=必须
pulls.status_checks_details=详情
pulls.status_checks_hide_all=隐藏所有检查
pulls.status_checks_show_all=显示所有检查
pulls.status_checks_approve_all=批准所有工作流
pulls.status_checks_need_approvals=%d 个工作流等待批准
pulls.status_checks_need_approvals_helper=此工作流在仓库维护者批准后才会运行。
pulls.update_branch=通过合并更新分支
pulls.update_branch_rebase=通过变基更新分支
pulls.update_branch_success=分支更新成功
@ -3036,6 +3040,7 @@ dashboard.update_migration_poster_id=更新迁移的发表者ID
dashboard.git_gc_repos=对仓库进行垃圾回收
dashboard.resync_all_sshkeys=使用 Gitea 的 SSH 密钥更新「.ssh/authorized_keys」文件。
dashboard.resync_all_sshprincipals=使用 Gitea 的 SSH 规则更新「.ssh/authorized_principals」文件。
dashboard.resync_all_hooks=重新同步所有仓库的 pre-receive、update 和 post-receive 钩子
dashboard.reinit_missing_repos=重新初始化所有丢失的 Git 仓库存在的记录
dashboard.sync_external_users=同步外部用户数据
dashboard.cleanup_hook_task_table=清理 hook_task 表
@ -3070,7 +3075,7 @@ dashboard.total_gc_time=GC 暂停时间总量
dashboard.total_gc_pause=GC 暂停时间总量
dashboard.last_gc_pause=上次 GC 暂停时间
dashboard.gc_times=GC 执行次数
dashboard.delete_old_actions=从数据库中删除所有旧工作流记录
dashboard.delete_old_actions=从数据库中删除所有旧操作记录
dashboard.delete_old_actions.started=已开始从数据库中删除所有旧工作流记录。
dashboard.update_checker=更新检查器
dashboard.delete_old_system_notices=从数据库中删除所有旧系统通知
@ -3890,6 +3895,7 @@ workflow.has_workflow_dispatch=此工作流有一个 workflow_dispatch 事件触
workflow.has_no_workflow_dispatch=工作流「%s」没有 workflow_dispatch 事件触发器。
need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。
approve_all_success=已成功批准所有工作流运行。
variables=变量
variables.management=变量管理
@ -3910,6 +3916,14 @@ variables.update.success=变量已编辑。
logs.always_auto_scroll=总是自动滚动日志
logs.always_expand_running=总是展开运行日志
general=常规
general.enable_actions=启用工作流
general.collaborative_owners_management=协作所有者管理
general.collaborative_owners_management_help=协作所有者是指其私有仓库有权访问此仓库的工作流的用户或组织。
general.add_collaborative_owner=添加协作所有者
general.collaborative_owner_not_exist=协作所有者不存在。
general.remove_collaborative_owner=移除协作所有者
general.remove_collaborative_owner_desc=移除协作所有者将阻止该所有者的其他仓库访问此仓库中的工作流。是否继续?
[projects]
deleted.display_name=已删除项目

@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@10.22.0",
"packageManager": "pnpm@10.24.0",
"engines": {
"node": ">= 22.6.0",
"pnpm": ">= 10.0.0"
@ -12,10 +12,10 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/paste-markdown": "1.5.3",
"@github/relative-time-element": "4.5.0",
"@github/relative-time-element": "4.5.1",
"@github/text-expander-element": "2.9.2",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.21.0",
"@primer/octicons": "19.21.1",
"@resvg/resvg-wasm": "2.6.2",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
@ -36,9 +36,9 @@
"idiomorph": "0.7.4",
"jquery": "3.7.1",
"katex": "0.16.25",
"mermaid": "11.12.1",
"mermaid": "11.12.2",
"mini-css-extract-plugin": "2.9.4",
"monaco-editor": "0.54.0",
"monaco-editor": "0.55.1",
"monaco-editor-webpack-plugin": "7.1.1",
"online-3d-viewer": "0.16.0",
"pdfobject": "2.3.1",
@ -46,7 +46,7 @@
"postcss": "8.5.6",
"postcss-loader": "8.2.0",
"sortablejs": "1.15.6",
"swagger-ui-dist": "5.30.2",
"swagger-ui-dist": "5.30.3",
"tailwindcss": "3.4.17",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
@ -56,7 +56,7 @@
"typescript": "5.9.3",
"uint8-to-base64": "0.2.1",
"vanilla-colorful": "0.7.2",
"vue": "3.5.24",
"vue": "3.5.25",
"vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3",
"vue-loader": "17.4.2",
@ -66,7 +66,7 @@
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
"@playwright/test": "1.56.1",
"@playwright/test": "1.57.0",
"@stylistic/eslint-plugin": "5.6.1",
"@stylistic/stylelint-plugin": "4.0.0",
"@types/codemirror": "5.60.17",
@ -79,40 +79,39 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.47.0",
"@typescript-eslint/parser": "8.48.1",
"@vitejs/plugin-vue": "6.0.2",
"@vitest/eslint-plugin": "1.4.3",
"@vitest/eslint-plugin": "1.5.1",
"eslint": "9.39.1",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0",
"eslint-plugin-import-x": "4.16.1",
"eslint-plugin-no-use-extend-native": "0.7.2",
"eslint-plugin-playwright": "2.3.0",
"eslint-plugin-playwright": "2.4.0",
"eslint-plugin-regexp": "2.10.0",
"eslint-plugin-sonarjs": "3.0.5",
"eslint-plugin-unicorn": "62.0.0",
"eslint-plugin-vue": "10.5.1",
"eslint-plugin-vue": "10.6.2",
"eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.0.2",
"globals": "16.5.0",
"happy-dom": "20.0.10",
"happy-dom": "20.0.11",
"markdownlint-cli": "0.46.0",
"material-icon-theme": "5.28.0",
"material-icon-theme": "5.29.0",
"nolyfill": "1.0.44",
"postcss-html": "1.8.0",
"spectral-cli-bundle": "1.0.3",
"stylelint": "16.25.0",
"stylelint": "16.26.1",
"stylelint-config-recommended": "17.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "4.0.0",
"typescript-eslint": "8.47.0",
"updates": "16.9.1",
"typescript-eslint": "8.48.1",
"updates": "17.0.4",
"vite-string-plugin": "1.4.9",
"vitest": "4.0.10",
"vue-tsc": "3.1.4"
"vitest": "4.0.15",
"vue-tsc": "3.1.5"
},
"browserslist": [
"defaults"

File diff suppressed because it is too large Load Diff

@ -216,9 +216,12 @@ func EditUser(ctx *context.APIContext) {
}
if form.Email != nil {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err)
@ -227,10 +230,6 @@ func EditUser(ctx *context.APIContext) {
}
return
}
if !user_model.IsEmailDomainAllowed(*form.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
}
}
opts := &user_service.UpdateOptions{

@ -1242,6 +1242,7 @@ func Routes() *web.Router {
m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
m.Put("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch)
m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.RenameBranchRepoOption{}), repo.RenameBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() {

@ -380,6 +380,81 @@ func ListBranches(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiBranches)
}
// UpdateBranch moves a branch reference to a new commit.
func UpdateBranch(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch
// ---
// summary: Update a branch reference to a new commit
// consumes:
// - application/json
// 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: branch
// in: path
// description: name of the branch
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateBranchRepoOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption)
branchName := ctx.PathParam("*")
repo := ctx.Repo.Repository
if repo.IsEmpty {
ctx.APIError(http.StatusNotFound, "Git Repository is empty.")
return
}
if repo.IsMirror {
ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.")
return
}
// permission check has been done in api.go
if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusUnprocessableEntity, err)
case git.IsErrPushRejected(err):
rej := err.(*git.ErrPushRejected)
ctx.APIError(http.StatusForbidden, rej.Message)
default:
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// RenameBranch renames a repository's branch.
func RenameBranch(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoRenameBranch

@ -13,6 +13,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
@ -222,8 +223,7 @@ func GetAllCommits(ctx *context.APIContext) {
}
// Total commit count
commitsCountTotal, err = git.CommitsCount(ctx.Repo.GitRepo.Ctx, git.CommitsCountOptions{
RepoPath: ctx.Repo.GitRepo.Path,
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository, gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{baseCommit.ID.String()},
Since: since,
@ -245,9 +245,8 @@ func GetAllCommits(ctx *context.APIContext) {
sha = ctx.Repo.Repository.DefaultBranch
}
commitsCountTotal, err = git.CommitsCount(ctx,
git.CommitsCountOptions{
RepoPath: ctx.Repo.GitRepo.Path,
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{sha},
RelPath: []string{path},

@ -610,10 +610,6 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
return
}
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
return

@ -1165,7 +1165,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef)
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), baseRefToGuess, baseRef, headRefToGuess, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())

@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
@ -429,7 +429,7 @@ func ListPageRevisions(ctx *context.APIContext) {
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
page := max(ctx.FormInt("page"), 1)

@ -147,6 +147,8 @@ type swaggerParameterBodies struct {
// in:body
CreateBranchRepoOption api.CreateBranchRepoOption
// in:body
UpdateBranchRepoOption api.UpdateBranchRepoOption
// in:body
CreateBranchProtectionOption api.CreateBranchProtectionOption

@ -5,6 +5,7 @@ package common
import (
"fmt"
"log"
"net/http"
"strings"
@ -71,8 +72,13 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
req = req.WithContext(cache.WithCacheContext(ctx))
ds.SetContextValue(httplib.RequestContextKey, req)
ds.AddCleanUp(func() {
if req.MultipartForm != nil {
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded files to temp directory (TMPDIR) when parsing multipart-form.
// The "req" might have changed due to the new "req.WithContext" calls
// For example: in NewBaseContext, a new "req" with context is created, and the multipart-form is parsed there.
// So we always use the latest "req" from the data store.
ctxReq := ds.GetContextValue(httplib.RequestContextKey).(*http.Request)
if ctxReq.MultipartForm != nil {
_ = ctxReq.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
}
})
next.ServeHTTP(respWriter, req)
@ -107,7 +113,11 @@ func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Han
return proxy.ForwardedHeaders(opt)
}
func Sessioner() (func(next http.Handler) http.Handler, error) {
func MustInitSessioner() func(next http.Handler) http.Handler {
// TODO: CHI-SESSION-GOB-REGISTER: chi-session has a design problem: it calls gob.Register for "Set"
// But if the server restarts, then the first "Get" will fail to decode the previously stored session data because the structs are not registered yet.
// So each package should make sure their structs are registered correctly during startup for session storage.
middleware, err := session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider,
ProviderConfig: setting.SessionConfig.ProviderConfig,
@ -120,8 +130,7 @@ func Sessioner() (func(next http.Handler) http.Handler, error) {
Domain: setting.SessionConfig.Domain,
})
if err != nil {
return nil, fmt.Errorf("failed to create session middleware: %w", err)
log.Fatalf("common.Sessioner failed: %v", err)
}
return middleware, nil
return middleware
}

@ -55,8 +55,8 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
return dbTypeNames
}
// Contexter prepare for rendering installation page
func Contexter() func(next http.Handler) http.Handler {
// installContexter prepare for rendering installation page
func installContexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
dbTypeNames := getSupportedDbTypeNames()
envConfigKeys := setting.CollectEnvConfigKeys()

@ -8,7 +8,6 @@ import (
"html"
"net/http"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@ -25,11 +24,8 @@ func Routes() *web.Router {
base.Methods("GET, HEAD", "/assets/*", public.FileHandlerFunc())
r := web.NewRouter()
if sessionMid, err := common.Sessioner(); err == nil && sessionMid != nil {
r.Use(sessionMid, Contexter())
} else {
log.Fatal("common.Sessioner failed: %v", err)
}
r.Use(common.MustInitSessioner(), installContexter())
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone)

@ -108,21 +108,19 @@ func ServCommand(ctx *context.PrivateContext) {
results.RepoName = repoName[:len(repoName)-5]
}
// Check if there is a user redirect for the requested owner
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
if err == nil {
owner, err := user_model.GetUserByID(ctx, redirectedUserID)
if err == nil {
log.Info("User %s has been redirected to %s", results.OwnerName, owner.Name)
results.OwnerName = owner.Name
} else {
log.Warn("User %s has a redirect to user with ID %d, but no user with this ID could be found. Trying without redirect...", results.OwnerName, redirectedUserID)
}
}
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if !user_model.IsErrUserNotExist(err) {
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
// Check if there is a user redirect for the requested owner
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
if err != nil {
// User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{
@ -130,11 +128,20 @@ func ServCommand(ctx *context.PrivateContext) {
})
return
}
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID)
if err != nil {
// User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
})
return
}
log.Info("User %s has been redirected to %s", results.OwnerName, redirectUser.Name)
results.OwnerName = redirectUser.Name
owner = redirectUser
}
if !owner.IsOrganization() && !owner.IsActive {
ctx.JSON(http.StatusForbidden, private.Response{
@ -143,24 +150,33 @@ func ServCommand(ctx *context.PrivateContext) {
return
}
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
if err == nil {
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
if err == nil {
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
results.RepoName = redirectedRepo.Name
results.OwnerName = redirectedRepo.OwnerName
owner.ID = redirectedRepo.OwnerID
} else {
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
}
}
// Now get the Repository and set the results section
repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
if !repo_model.IsErrRepoNotExist(err) {
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
if err == nil {
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
if err == nil {
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
results.RepoName = redirectedRepo.Name
results.OwnerName = redirectedRepo.OwnerName
repo = redirectedRepo
owner.ID = redirectedRepo.OwnerID
} else {
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
}
}
if repo == nil {
repoExist = false
if mode == perm.AccessModeRead {
// User is fetching/cloning a non-existent repository
@ -170,13 +186,6 @@ func ServCommand(ctx *context.PrivateContext) {
})
return
}
// else fallthrough (push-to-create may kick in below)
} else {
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
}

@ -409,7 +409,7 @@ func EditUserPost(ctx *context.Context) {
}
if form.Email != "" {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, u, form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true

@ -277,8 +277,11 @@ type LinkAccountData struct {
GothUser goth.User
}
func init() {
gob.Register(LinkAccountData{}) // TODO: CHI-SESSION-GOB-REGISTER
}
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
gob.Register(LinkAccountData{})
v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData)
if !ok {
return nil
@ -287,7 +290,6 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
}
func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error {
gob.Register(LinkAccountData{})
return updateSession(ctx, nil, map[string]any{
"linkAccountData": linkAccountData,
})

@ -10,7 +10,6 @@ import (
"net/url"
"path"
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@ -42,8 +41,8 @@ type blameRow struct {
// RefBlame render blame page
func RefBlame(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["IsBlame"] = true
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
// Get current entry user currently looking at.
if ctx.Repo.TreePath == "" {
@ -56,17 +55,6 @@ func RefBlame(ctx *context.Context) {
return
}
treeNames := strings.Split(ctx.Repo.TreePath, "/")
var paths []string
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["Paths"] = paths
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
blob := entry.Blob()
fileSize := blob.Size()
ctx.Data["FileSize"] = fileSize

@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
@ -133,8 +134,7 @@ func RestoreBranchPost(ctx *context.Context) {
return
}
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{
Remote: ctx.Repo.Repository.RepoPath(),
if err := gitrepo.Push(ctx, ctx.Repo.Repository, ctx.Repo.Repository, git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.CommitID, git.BranchPrefix, deletedBranch.Name),
Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
}); err != nil {

@ -222,7 +222,7 @@ func FileHistory(ctx *context.Context) {
return
}
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("FileCommitsCount", err)
return

@ -41,7 +41,12 @@ const (
editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
func prepareEditorPage(ctx *context.Context, editorAction string) *context.CommitFormOptions {
prepareHomeTreeSideBarSwitch(ctx)
return prepareEditorPageFormOptions(ctx, editorAction)
}
func prepareEditorPageFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
if cleanedTreePath != ctx.Repo.TreePath {
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
@ -283,7 +288,7 @@ func EditFile(ctx *context.Context) {
// on the "New File" page, we should add an empty path field to make end users could input a new name
prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
prepareEditorCommitFormOptions(ctx, editorAction)
prepareEditorPage(ctx, editorAction)
if ctx.Written() {
return
}
@ -376,15 +381,16 @@ func EditFilePost(ctx *context.Context) {
// DeleteFile render delete file page
func DeleteFile(ctx *context.Context) {
prepareEditorCommitFormOptions(ctx, "_delete")
prepareEditorPage(ctx, "_delete")
if ctx.Written() {
return
}
ctx.Data["PageIsDelete"] = true
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
ctx.HTML(http.StatusOK, tplDeleteFile)
}
// DeleteFilePost response for deleting file
// DeleteFilePost response for deleting file or directory
func DeleteFilePost(ctx *context.Context) {
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
if ctx.Written() {
@ -392,17 +398,37 @@ func DeleteFilePost(ctx *context.Context) {
}
treePath := ctx.Repo.TreePath
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
if treePath == "" {
ctx.JSONError("cannot delete root directory") // it should not happen unless someone is trying to be malicious
return
}
// Check if the path is a directory
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
if err != nil {
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
var commitMessage string
if entry.IsDir() {
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
} else {
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath))
}
_, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Files: []*files_service.ChangeRepoFile{
{
Operation: "delete",
TreePath: treePath,
Operation: "delete",
TreePath: treePath,
DeleteRecursively: true,
},
},
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
Message: commitMessage,
Signoff: parsed.form.Signoff,
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
@ -412,7 +438,11 @@ func DeleteFilePost(ctx *context.Context) {
return
}
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
if entry.IsDir() {
ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath))
} else {
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
}
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
redirectForCommitChoice(ctx, parsed, redirectTreePath)
}
@ -420,7 +450,7 @@ func DeleteFilePost(ctx *context.Context) {
func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
opts := prepareEditorCommitFormOptions(ctx, "_upload")
opts := prepareEditorPage(ctx, "_upload")
if ctx.Written() {
return
}

@ -14,7 +14,7 @@ import (
)
func NewDiffPatch(ctx *context.Context) {
prepareEditorCommitFormOptions(ctx, "_diffpatch")
prepareEditorPage(ctx, "_diffpatch")
if ctx.Written() {
return
}

@ -16,7 +16,7 @@ import (
)
func CherryPick(ctx *context.Context) {
prepareEditorCommitFormOptions(ctx, "_cherrypick")
prepareEditorPage(ctx, "_cherrypick")
if ctx.Written() {
return
}

@ -13,6 +13,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@ -102,8 +103,7 @@ func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) st
}
func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
Remote: targetRepo.RepoPath(),
return gitrepo.Push(ctx, baseRepo, targetRepo, git.PushOptions{
Branch: baseBranchName + ":" + targetBranchName,
Env: repo_module.PushingEnvironment(doer, targetRepo),
})

@ -1,24 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
const (
tplFindFiles templates.TplName = "repo/find/files"
)
// FindFiles render the page to find repository files
func FindFiles(ctx *context.Context) {
path := ctx.PathParam("*")
ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
ctx.HTML(http.StatusOK, tplFindFiles)
}

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
@ -141,8 +142,7 @@ func NewComment(ctx *context.Context) {
if prHeadCommitID != headBranchCommitID {
// force push to base repo
err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
Remote: pull.BaseRepo.RepoPath(),
err := gitrepo.Push(ctx, pull.HeadRepo, pull.BaseRepo, git.PushOptions{
Branch: pull.HeadBranch + ":" + prHeadRef,
Force: true,
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),

@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/services/task"
)
@ -237,7 +238,7 @@ func MigratePost(ctx *context.Context) {
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
}
err = repo_model.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
err = repo_service.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
if err != nil {
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
return

@ -331,7 +331,7 @@ func UpdateViewedFiles(ctx *context.Context) {
updatedFiles[file] = state
}
if err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil {
if _, err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil {
ctx.ServerError("UpdateReview", err)
}
}

@ -5,6 +5,7 @@
package repo
import (
stdCtx "context"
"errors"
"fmt"
"net/http"
@ -18,6 +19,7 @@ import (
"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/gitrepo"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@ -39,7 +41,7 @@ const (
)
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
func calReleaseNumCommitsBehind(ctx stdCtx.Context, repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
target := release.Target
if target == "" {
target = repoCtx.Repository.DefaultBranch
@ -59,7 +61,7 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model
return fmt.Errorf("GetBranchCommit(DefaultBranch): %w", err)
}
}
countCache[target], err = commit.CommitsCount()
countCache[target], err = gitrepo.CommitsCountOfCommit(ctx, repoCtx.Repository, commit.ID.String())
if err != nil {
return fmt.Errorf("CommitsCount: %w", err)
}
@ -122,7 +124,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
}
if !r.IsDraft {
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
if err := calReleaseNumCommitsBehind(ctx, ctx.Repo, r, countCache); err != nil {
return nil, err
}
}

@ -158,7 +158,7 @@ func TestCalReleaseNumCommitsBehind(t *testing.T) {
countCache := make(map[string]int64)
for _, release := range releases {
err := calReleaseNumCommitsBehind(ctx.Repo, release, countCache)
err := calReleaseNumCommitsBehind(ctx, ctx.Repo, release, countCache)
assert.NoError(t, err)
}

@ -70,7 +70,7 @@ func CommitInfoCache(ctx *context.Context) {
ctx.ServerError("GetBranchCommit", err)
return
}
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@ -112,7 +113,7 @@ func LFSLocks(ctx *context.Context) {
}
defer cleanup()
if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
if err := gitrepo.CloneRepoToLocal(ctx, ctx.Repo.Repository, tmpBasePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
}); err != nil {

@ -33,7 +33,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
})
mockIconForFile := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
}
assert.Equal(t, WebDiffFileTree{
TreeRoot: WebDiffFileItem{

@ -245,27 +245,17 @@ func LastCommit(ctx *context.Context) {
return
}
// The "/lastcommit/" endpoint is used to render the embedded HTML content for the directory file listing with latest commit info
// It needs to construct correct links to the file items, but the route only accepts a commit ID, not a full ref name (branch or tag).
// So we need to get the ref name from the query parameter "refSubUrl".
// TODO: LAST-COMMIT-ASYNC-LOADING: it needs more tests to cover this
refSubURL := path.Clean(ctx.FormString("refSubUrl"))
prepareRepoViewContent(ctx, util.IfZero(refSubURL, ctx.Repo.RefTypeNameSubURL()))
renderDirectoryFiles(ctx, 0)
if ctx.Written() {
return
}
var treeNames []string
paths := make([]string, 0, 5)
if len(ctx.Repo.TreePath) > 0 {
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["BranchLink"] = branchLink
ctx.HTML(http.StatusOK, tplRepoViewList)
}
@ -289,7 +279,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
return nil
}
ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
// TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details
lastCommitLoaderURL := ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL())
// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
@ -322,6 +314,21 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
ctx.ServerError("GetCommitsInfo", err)
return nil
}
{
if timeout != 0 && !setting.IsProd && !setting.IsInTesting {
log.Debug("first call to get directory file commit info")
clearFilesCommitInfo := func() {
log.Warn("clear directory file commit info to force async loading on frontend")
for i := range files {
files[i].Commit = nil
}
}
_ = clearFilesCommitInfo
// clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests
}
}
ctx.Data["Files"] = files
prepareDirectoryFileIcons(ctx, files)
for _, f := range files {
@ -334,16 +341,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
if !loadLatestCommitData(ctx, latestCommit) {
return nil
}
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
treeLink := branchLink
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Data["TreeLink"] = treeLink
return allEntries
}

@ -362,6 +362,32 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b
return false
}
func prepareRepoViewContent(ctx *context.Context, refTypeNameSubURL string) {
// for: home, file list, file view, blame
ctx.Data["PageIsViewCode"] = true
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show Upload File button or menu item
// prepare the tree path navigation
var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + refTypeNameSubURL
treeLink := branchLink
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink
}
// Home render repository home page
func Home(ctx *context.Context) {
if handleRepoHomeFeed(ctx) {
@ -383,8 +409,7 @@ func Home(ctx *context.Context) {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
ctx.Data["PageIsViewCode"] = true
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
// empty or broken repositories need to be handled differently
@ -405,26 +430,6 @@ func Home(ctx *context.Context) {
return
}
// prepare the tree path
var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
treeLink := branchLink
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink
// some UI components are only shown when the tree path is root
isTreePathRoot := ctx.Repo.TreePath == ""
@ -455,7 +460,7 @@ func Home(ctx *context.Context) {
if isViewHomeOnlyContent(ctx) {
ctx.HTML(http.StatusOK, tplRepoViewContent)
} else if len(treeNames) != 0 {
} else if ctx.Repo.TreePath != "" {
ctx.HTML(http.StatusOK, tplRepoView)
} else {
ctx.HTML(http.StatusOK, tplRepoHome)

@ -310,7 +310,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
}
// get commit count - wiki revisions
commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
return wikiGitRepo, entry
@ -350,7 +350,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
}
// get commit count - wiki revisions
commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page

@ -227,6 +227,8 @@ func ctxDataSet(args ...any) func(ctx *context.Context) {
}
}
const RouterMockPointBeforeWebRoutes = "before-web-routes"
// Routes returns all web routes
func Routes() *web.Router {
routes := web.NewRouter()
@ -267,11 +269,7 @@ func Routes() *web.Router {
routes.Get("/ssh_info", misc.SSHInfo)
routes.Get("/api/healthz", healthcheck.Check)
if sessionMid, err := common.Sessioner(); err == nil && sessionMid != nil {
mid = append(mid, sessionMid, context.Contexter())
} else {
log.Fatal("common.Sessioner failed: %v", err)
}
mid = append(mid, common.MustInitSessioner(), context.Contexter())
// Get user from session if logged in.
mid = append(mid, webAuth(buildAuthGroup()))
@ -289,7 +287,7 @@ func Routes() *web.Router {
webRoutes := web.NewRouter()
webRoutes.Use(mid...)
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS())
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
routes.Mount("", webRoutes)
return routes
}
@ -1188,7 +1186,6 @@ func registerWebRoutes(m *web.Router) {
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
m.Group("/{username}/{reponame}", func() {
m.Get("/find/*", repo.FindFiles)
m.Group("/tree-list", func() {
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList)
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)

@ -22,9 +22,6 @@ import (
var gothRWMutex = sync.RWMutex{}
// UsersStoreKey is the key for the store
const UsersStoreKey = "gitea-oauth2-sessions"
// ProviderHeaderKey is the HTTP header key
const ProviderHeaderKey = "gitea-oauth2-provider"
@ -33,7 +30,7 @@ func Init(ctx context.Context) error {
// Lock our mutex
gothRWMutex.Lock()
gob.Register(&sessions.Session{})
gob.Register(&sessions.Session{}) // TODO: CHI-SESSION-GOB-REGISTER. FIXME: it seems to be an abuse, why the Session struct itself is stored in session store again?
gothic.Store = &SessionsStore{
maxLength: int64(setting.OAuth2.MaxTokenLength),

@ -35,9 +35,9 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
// Allow PAM sources with `@` in their name, like from Active Directory
username := pamLogin
email := pamLogin
idx := strings.Index(pamLogin, "@")
if idx > -1 {
username = pamLogin[:idx]
before, _, ok := strings.Cut(pamLogin, "@")
if ok {
username = before
}
if user_model.ValidateEmail(email) != nil {
if source.EmailDomain != "" {

@ -21,10 +21,10 @@ import (
func (source *Source) Authenticate(ctx context.Context, user *user_model.User, userName, password string) (*user_model.User, error) {
// Verify allowed domains.
if len(source.AllowedDomains) > 0 {
idx := strings.Index(userName, "@")
if idx == -1 {
_, after, ok := strings.Cut(userName, "@")
if !ok {
return nil, user_model.ErrUserNotExist{Name: userName}
} else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), userName[idx+1:], true) {
} else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), after, true) {
return nil, user_model.ErrUserNotExist{Name: userName}
}
}
@ -61,9 +61,9 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
}
username := userName
idx := strings.Index(userName, "@")
if idx > -1 {
username = userName[:idx]
before, _, ok := strings.Cut(userName, "@")
if ok {
username = before
}
user = &user_model.User{

@ -43,8 +43,10 @@ type Base struct {
Locale translation.Locale
}
var ParseMultipartFormMaxMemory = int64(32 << 20)
func (b *Base) ParseMultipartForm() bool {
err := b.Req.ParseMultipartForm(32 << 20)
err := b.Req.ParseMultipartForm(ParseMultipartFormMaxMemory)
if err != nil {
// TODO: all errors caused by client side should be ignored (connection closed).
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {

@ -118,7 +118,7 @@ func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
if uidChanged {
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
} else if cookieToken != "" {
// If cookie token presents, re-use existing unexpired token, else generate a new one.
// If cookie token present, re-use existing unexpired token, else generate a new one.
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {

@ -202,14 +202,14 @@ func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_
}
// GetCommitsCount returns cached commit count for current view
func (r *Repository) GetCommitsCount() (int64, error) {
func (r *Repository) GetCommitsCount(ctx context.Context) (int64, error) {
if r.Commit == nil {
return 0, nil
}
contextName := r.RefFullName.ShortName()
isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag()
return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) {
return r.Commit.CommitsCount()
return gitrepo.CommitsCountOfCommit(ctx, r.Repository, r.Commit.ID.String())
})
}
@ -219,11 +219,10 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool,
return cache.GetInt64(cacheKey, func() (int64, error) {
if len(branches) == 0 {
return git.AllCommitsCount(ctx, r.Repository.RepoPath(), hidePRRefs, files...)
return gitrepo.AllCommitsCount(ctx, r.Repository, hidePRRefs, files...)
}
return git.CommitsCount(ctx,
git.CommitsCountOptions{
RepoPath: r.Repository.RepoPath(),
return gitrepo.CommitsCount(ctx, r.Repository,
gitrepo.CommitsCountOptions{
Revision: branches,
RelPath: files,
})
@ -821,7 +820,7 @@ func RepoRefByDefaultBranch() func(*Context) {
ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount(ctx)
ctx.Data["RefFullName"] = ctx.Repo.RefFullName
ctx.Data["BranchName"] = ctx.Repo.BranchName
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
@ -858,7 +857,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
if err == nil && len(brs) != 0 {
refShortName = brs[0]
} else if len(brs) == 0 {
log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
log.Error("No branches in non-empty repository %s", ctx.Repo.Repository.RelativePath())
} else {
log.Error("GetBranches error: %v", err)
}
@ -970,7 +969,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return

@ -11,6 +11,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@ -190,7 +191,7 @@ func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep
// Retrieve files affected by the commit
if opts.Files {
fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String())
fileStatus, err := gitrepo.GetCommitFileStatus(ctx, repo, commit.ID.String())
if err != nil {
return nil, err
}

@ -215,7 +215,7 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro
if !isExist {
numNeedUpdate++
if autofix {
if err := git.WriteCommitGraph(ctx, repo.RepoPath()); err != nil {
if err := gitrepo.WriteCommitGraph(ctx, repo); err != nil {
logger.Error("Unable to write commit-graph in %s. Error: %v", repo.FullName(), err)
return err
}

@ -1434,15 +1434,17 @@ outer:
}
}
// Explicitly store files that have changed in the database, if any is present at all.
// This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed.
// On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed.
if len(filesChangedSinceLastDiff) > 0 {
err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
// Explicitly store files that have changed in the database, if any is present at all.
// This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed.
// On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed.
updatedReview, err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
if err != nil {
log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err)
return nil, err
}
// Update the local review to reflect the changes immediately
review = updatedReview
}
return review, nil

@ -6,6 +6,7 @@ package incoming
import (
"context"
"crypto/tls"
"errors"
"fmt"
net_mail "net/mail"
"regexp"
@ -221,7 +222,7 @@ loop:
err := func() error {
r := msg.GetBody(section)
if r == nil {
return fmt.Errorf("could not get body from message: %w", err)
return errors.New("could not get body from message")
}
env, err := enmime.ReadEnvelope(r)

@ -33,7 +33,13 @@ func (s *SendmailSender) Send(from string, to []string, msg io.WriterTo) error {
args := []string{"-f", envelopeFrom, "-i"}
args = append(args, setting.MailService.SendmailArgs...)
args = append(args, to...)
for _, recipient := range to {
smtpTo, err := sanitizeEmailAddress(recipient)
if err != nil {
return fmt.Errorf("invalid recipient address %q: %w", recipient, err)
}
args = append(args, smtpTo)
}
log.Trace("Sending with: %s %v", setting.MailService.SendmailPath, args)
desc := fmt.Sprintf("SendMail: %s %v", setting.MailService.SendmailPath, args)

@ -9,13 +9,13 @@ import (
"fmt"
"io"
"net"
"net/mail"
"net/smtp"
"os"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/wneessen/go-mail/smtp"
)
// SMTPSender Sender SMTP mail sender
@ -108,7 +108,7 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error {
if strings.Contains(options, "CRAM-MD5") {
auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd)
} else if strings.Contains(options, "PLAIN") {
auth = smtp.PlainAuth("", opts.User, opts.Passwd, host, false)
auth = smtp.PlainAuth("", opts.User, opts.Passwd, host)
} else if strings.Contains(options, "LOGIN") {
// Patch for AUTH LOGIN
auth = LoginAuth(opts.User, opts.Passwd)
@ -123,18 +123,24 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error {
}
}
if opts.OverrideEnvelopeFrom {
if err = client.Mail(opts.EnvelopeFrom); err != nil {
return fmt.Errorf("failed to issue MAIL command: %w", err)
}
} else {
if err = client.Mail(fmt.Sprintf("<%s>", from)); err != nil {
return fmt.Errorf("failed to issue MAIL command: %w", err)
}
fromAddr := from
if opts.OverrideEnvelopeFrom && opts.EnvelopeFrom != "" {
fromAddr = opts.EnvelopeFrom
}
smtpFrom, err := sanitizeEmailAddress(fromAddr)
if err != nil {
return fmt.Errorf("invalid envelope from address: %w", err)
}
if err = client.Mail(smtpFrom); err != nil {
return fmt.Errorf("failed to issue MAIL command: %w", err)
}
for _, rec := range to {
if err = client.Rcpt(rec); err != nil {
smtpTo, err := sanitizeEmailAddress(rec)
if err != nil {
return fmt.Errorf("invalid recipient address %q: %w", rec, err)
}
if err = client.Rcpt(smtpTo); err != nil {
return fmt.Errorf("failed to issue RCPT command: %w", err)
}
}
@ -155,3 +161,11 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error {
return nil
}
func sanitizeEmailAddress(raw string) (string, error) {
addr, err := mail.ParseAddress(strings.TrimSpace(strings.Trim(raw, "<>")))
if err != nil {
return "", err
}
return addr.Address, nil
}

@ -6,9 +6,9 @@ package sender
import (
"errors"
"fmt"
"net/smtp"
"github.com/Azure/go-ntlmssp"
"github.com/wneessen/go-mail/smtp"
)
type loginAuth struct {

Some files were not shown because too many files have changed in this diff Show More