mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-02 08:48:53 +02:00
feat: Merge the main branch
This commit is contained in:
@@ -1,65 +1,29 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// IsArtifactV4 detects whether the artifact is likely from v4.
|
||||
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
|
||||
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.
|
||||
func IsArtifactV4(art *actions_model.ActionArtifact) bool {
|
||||
return strings.Contains(art.ContentEncodingOrType, "/")
|
||||
}
|
||||
type tagType string
|
||||
|
||||
func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) {
|
||||
contentType := art.ContentEncodingOrType
|
||||
u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType})
|
||||
if err != nil {
|
||||
return "", err
|
||||
// BuildSignature builds a hmac signature for the input values.
|
||||
// "tag" is an internal pre-defined static string to distinguish the signatures for different purpose.
|
||||
func BuildSignature(tag tagType, vals ...string) []byte {
|
||||
m := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
||||
_, _ = io.WriteString(m, string(tag))
|
||||
var buf8 [8]byte
|
||||
for _, v := range vals {
|
||||
binary.LittleEndian.PutUint64(buf8[:], uint64(len(v)))
|
||||
_, _ = m.Write(buf8[:])
|
||||
_, _ = io.WriteString(m, v)
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool {
|
||||
if !setting.Actions.ArtifactStorage.ServeDirect() {
|
||||
return false
|
||||
}
|
||||
u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method)
|
||||
if err != nil {
|
||||
log.Error("GetArtifactV4ServeDirectURL: %v", err)
|
||||
return false
|
||||
}
|
||||
ctx.Redirect(u, http.StatusFound)
|
||||
return true
|
||||
}
|
||||
|
||||
func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{
|
||||
Filename: art.ArtifactPath,
|
||||
ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||
if DownloadArtifactV4ServeDirect(ctx, art) {
|
||||
return nil
|
||||
}
|
||||
return DownloadArtifactV4ReadStorage(ctx, art)
|
||||
return m.Sum(nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildSignature(t *testing.T) {
|
||||
a := BuildSignature("v0", "x")
|
||||
b := BuildSignature("v0", "x")
|
||||
assert.Equal(t, a, b)
|
||||
|
||||
a = BuildSignature("v0", "x", "yz")
|
||||
b = BuildSignature("v0", "xy", "z")
|
||||
assert.NotEqual(t, a, b)
|
||||
|
||||
a = BuildSignature("v1", "x")
|
||||
b = BuildSignature("v2", "x")
|
||||
assert.NotEqual(t, a, b)
|
||||
|
||||
a = BuildSignature("v0", "x")
|
||||
b = BuildSignature("v0x")
|
||||
assert.NotEqual(t, a, b)
|
||||
|
||||
a = BuildSignature("v0", "", "x")
|
||||
b = BuildSignature("v0", "x", "")
|
||||
assert.NotEqual(t, a, b)
|
||||
|
||||
a = BuildSignature("v0")
|
||||
b = BuildSignature("v0")
|
||||
assert.Equal(t, a, b)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// CommitActionsStatusMap maps CommitStatus.ID to the live ActionRunJob status
|
||||
// for Gitea Actions rows.
|
||||
type CommitActionsStatusMap map[int64]actions_model.Status
|
||||
|
||||
// IconStatus returns the action status name to route the icon through
|
||||
// repo/icons/action_status, or "" when the row isn't from Gitea Actions.
|
||||
func (m CommitActionsStatusMap) IconStatus(s *git_model.CommitStatus) string {
|
||||
if status, ok := m[s.ID]; ok {
|
||||
return status.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetCommitActionsStatusMap resolves the live ActionRunJob.Status for every
|
||||
// CommitStatus row backed by Gitea Actions. Rows from other sources (external
|
||||
// CIs, API) are left untouched and rendered from their stored State.
|
||||
func GetCommitActionsStatusMap(ctx context.Context, statuses []*git_model.CommitStatus) CommitActionsStatusMap {
|
||||
if len(statuses) == 0 {
|
||||
return nil
|
||||
}
|
||||
statusByJobID := make(map[int64]*git_model.CommitStatus)
|
||||
repoByID := make(map[int64]*repo_model.Repository)
|
||||
for _, status := range statuses {
|
||||
if status == nil || status.TargetURL == "" {
|
||||
continue
|
||||
}
|
||||
if status.Repo == nil {
|
||||
status.Repo = repoByID[status.RepoID]
|
||||
}
|
||||
// ParseGiteaActionsTargetURL lazy-loads status.Repo on miss; cache the
|
||||
// outcome so later entries with the same RepoID skip that load.
|
||||
_, jobID, ok := status.ParseGiteaActionsTargetURL(ctx)
|
||||
repoByID[status.RepoID] = status.Repo
|
||||
if ok {
|
||||
statusByJobID[jobID] = status
|
||||
}
|
||||
}
|
||||
if len(statusByJobID) == 0 {
|
||||
return nil
|
||||
}
|
||||
jobs := make(map[int64]*actions_model.ActionRunJob, len(statusByJobID))
|
||||
if err := db.GetEngine(ctx).In("id", slices.Collect(maps.Keys(statusByJobID))).Cols("id", "status").Find(&jobs); err != nil {
|
||||
log.Error("db.Find: failed to find action run jobs: %v", err)
|
||||
return nil
|
||||
}
|
||||
info := make(CommitActionsStatusMap, len(jobs))
|
||||
for jobID, status := range statusByJobID {
|
||||
if job, ok := jobs[jobID]; ok {
|
||||
info[status.ID] = job.Status
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/exprparser"
|
||||
"gitea.com/gitea/runner/act/exprparser"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"github.com/nektos/act/pkg/exprparser"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gitea.com/gitea/runner/act/exprparser"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/exprparser"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gitea.com/gitea/runner/act/exprparser"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -48,6 +48,9 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||
}
|
||||
results := map[string]*JobResult{}
|
||||
for id, job := range origin.Jobs {
|
||||
if job == nil {
|
||||
return nil, fmt.Errorf("needed job not found: %q", id)
|
||||
}
|
||||
outputs := pc.jobOutputs[id]
|
||||
results[id] = &JobResult{
|
||||
Needs: job.Needs(),
|
||||
|
||||
@@ -75,6 +75,13 @@ func TestParse(t *testing.T) {
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
invalidFileTests := []struct {
|
||||
name string
|
||||
}{
|
||||
{name: "null_job_implicit"},
|
||||
{name: "null_job_explicit"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
content := ReadTestdata(t, tt.name+".in.yaml")
|
||||
@@ -100,6 +107,16 @@ func TestParse(t *testing.T) {
|
||||
assert.Equal(t, string(want), builder.String())
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range invalidFileTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
content := ReadTestdata(t, tt.name+".in.yaml")
|
||||
require.NotPanics(t, func() {
|
||||
_, err := Parse(content)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepCopyYamlNode(t *testing.T) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# null_job_explicit.in.yaml
|
||||
on: push
|
||||
jobs:
|
||||
empty: null
|
||||
notempty:
|
||||
needs: empty
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo ok
|
||||
@@ -0,0 +1,9 @@
|
||||
# null_job_implicit.in.yaml
|
||||
on: push
|
||||
jobs:
|
||||
empty:
|
||||
notempty:
|
||||
needs: empty
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo ok
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflowpattern
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
)
|
||||
|
||||
type WorkflowPattern struct {
|
||||
negative bool
|
||||
glob glob.Glob
|
||||
}
|
||||
|
||||
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
|
||||
ret := make([]*WorkflowPattern, 0, len(patterns))
|
||||
for _, pattern := range patterns {
|
||||
cp, err := glob.CompileWorkflow(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, &WorkflowPattern{glob: cp, negative: strings.HasPrefix(pattern, "!")})
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Skip returns true if the workflow should be skipped per paths/branches semantics.
|
||||
func Skip(sequence []*WorkflowPattern, input []string) bool {
|
||||
allSkipped := true
|
||||
for _, file := range input {
|
||||
shouldSkip := true
|
||||
for _, item := range sequence {
|
||||
if item.negative {
|
||||
// "!README.md" doesn't match "README.md", so "README.md" should be skipped
|
||||
// "!README.md" matches "help.md" but it shouldn't affect "skip or not", because "help.md" might have been skipped by other rules like "docs/*.md"
|
||||
if !item.glob.Match(file) {
|
||||
shouldSkip = true
|
||||
}
|
||||
} else if item.glob.Match(file) {
|
||||
// if "*.md" matches "help.md" so it shouldn't be skipped
|
||||
shouldSkip = false
|
||||
}
|
||||
}
|
||||
allSkipped = allSkipped && shouldSkip
|
||||
}
|
||||
return len(sequence) > 0 && allSkipped
|
||||
}
|
||||
|
||||
// Filter returns true if the workflow should be skipped per paths-ignore/branches-ignore semantics.
|
||||
func Filter(sequence []*WorkflowPattern, input []string) bool {
|
||||
for _, file := range input {
|
||||
anyMatched := false
|
||||
for _, item := range sequence {
|
||||
if anyMatched = item.glob.Match(file); anyMatched {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !anyMatched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(sequence) != 0
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflowpattern
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMatchPattern(t *testing.T) {
|
||||
kases := []struct {
|
||||
inputs []string
|
||||
patterns []string
|
||||
skipResult bool
|
||||
filterResult bool
|
||||
}{
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"path/with/slash"},
|
||||
skipResult: true,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"meta", "path/b", "otherfile"},
|
||||
skipResult: false,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/b"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/c", "path/b"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/c", "path/b", "path/a"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/c", "path/b", "path/d", "path/a"},
|
||||
skipResult: false,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{},
|
||||
inputs: []string{},
|
||||
skipResult: false,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{"\\!file"},
|
||||
inputs: []string{"!file"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"escape\\\\backslash"},
|
||||
inputs: []string{"escape\\backslash"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{".yml"},
|
||||
inputs: []string{"fyml"},
|
||||
skipResult: true,
|
||||
filterResult: false,
|
||||
},
|
||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags
|
||||
{
|
||||
patterns: []string{"feature/*"},
|
||||
inputs: []string{"feature/my-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/*"},
|
||||
inputs: []string{"feature/your-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/**"},
|
||||
inputs: []string{"feature/beta-a/my-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/**"},
|
||||
inputs: []string{"feature/beta-a/my-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/**"},
|
||||
inputs: []string{"feature/mona/the/octocat"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"main", "releases/mona-the-octocat"},
|
||||
inputs: []string{"main"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"main", "releases/mona-the-octocat"},
|
||||
inputs: []string{"releases/mona-the-octocat"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"main"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"releases"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**"},
|
||||
inputs: []string{"all/the/branches"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**"},
|
||||
inputs: []string{"every/tag"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*feature"},
|
||||
inputs: []string{"mona-feature"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*feature"},
|
||||
inputs: []string{"feature"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*feature"},
|
||||
inputs: []string{"ver-10-feature"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v2*"},
|
||||
inputs: []string{"v2"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v2*"},
|
||||
inputs: []string{"v2.0"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v2*"},
|
||||
inputs: []string{"v2.9"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
||||
inputs: []string{"v1.10.1"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
||||
inputs: []string{"v2.0.0"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"server.rb"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.jsx?"},
|
||||
inputs: []string{"page.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.jsx?"},
|
||||
inputs: []string{"page.jsx"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**"},
|
||||
inputs: []string{"all/the/files.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.js"},
|
||||
inputs: []string{"app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.js"},
|
||||
inputs: []string{"index.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**.js"},
|
||||
inputs: []string{"index.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**.js"},
|
||||
inputs: []string{"js/index.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**.js"},
|
||||
inputs: []string{"src/js/app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/*"},
|
||||
inputs: []string{"docs/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/*"},
|
||||
inputs: []string{"docs/file.txt"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**"},
|
||||
inputs: []string{"docs/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**"},
|
||||
inputs: []string{"docs/mona/octocat.txt"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**/*.md"},
|
||||
inputs: []string{"docs/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**/*.md"},
|
||||
inputs: []string{"docs/mona/hello-world.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**/*.md"},
|
||||
inputs: []string{"docs/a/markdown/file.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/docs/**"},
|
||||
inputs: []string{"docs/hello.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/docs/**"},
|
||||
inputs: []string{"dir/docs/my-file.txt"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/docs/**"},
|
||||
inputs: []string{"space/docs/plan/space.doc"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/README.md"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/README.md"},
|
||||
inputs: []string{"js/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*src/**"},
|
||||
inputs: []string{"a/src/app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*src/**"},
|
||||
inputs: []string{"my-src/code/js/app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*-post.md"},
|
||||
inputs: []string{"my-post.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*-post.md"},
|
||||
inputs: []string{"path/their-post.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/migrate-*.sql"},
|
||||
inputs: []string{"migrate-10909.sql"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/migrate-*.sql"},
|
||||
inputs: []string{"db/migrate-v1.0.sql"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/migrate-*.sql"},
|
||||
inputs: []string{"db/sept/migrate-v1.sql"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md"},
|
||||
inputs: []string{"hello.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: true,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md"},
|
||||
inputs: []string{"docs/hello.md"},
|
||||
skipResult: true,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md", "README*"},
|
||||
inputs: []string{"hello.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md", "README*"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md", "README*"},
|
||||
inputs: []string{"README.doc"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
msg := fmt.Sprintf("patterns=%s, input=%s", strings.Join(kase.patterns, ","), strings.Join(kase.inputs, ","))
|
||||
patterns, err := CompilePatterns(kase.patterns...)
|
||||
assert.NoError(t, err, "compile error: %s", msg)
|
||||
assert.Equal(t, kase.skipResult, Skip(patterns, kase.inputs), "unexpected skip result: %s", msg)
|
||||
assert.Equal(t, kase.filterResult, Filter(patterns, kase.inputs), "unexpected filter result: %s", msg)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||
"code.gitea.io/gitea/modules/actions/workflowpattern"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -17,8 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/workflowpattern"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -297,7 +297,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{refName.BranchName()}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, []string{refName.BranchName()}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "branches-ignore":
|
||||
@@ -309,7 +309,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, []string{refName.BranchName()}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Filter(patterns, []string{refName.BranchName()}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "tags":
|
||||
@@ -321,7 +321,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{refName.TagName()}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, []string{refName.TagName()}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "tags-ignore":
|
||||
@@ -333,7 +333,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, []string{refName.TagName()}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Filter(patterns, []string{refName.TagName()}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
@@ -349,7 +349,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
}
|
||||
@@ -366,7 +366,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
}
|
||||
@@ -492,7 +492,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, []string{refName.ShortName()}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "branches-ignore":
|
||||
@@ -501,7 +501,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Filter(patterns, []string{refName.ShortName()}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
@@ -513,7 +513,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
}
|
||||
@@ -526,7 +526,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||
matchTimes++
|
||||
}
|
||||
}
|
||||
@@ -747,7 +747,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, []string{workflow.Name}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "branches":
|
||||
@@ -755,7 +755,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "branches-ignore":
|
||||
@@ -763,7 +763,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}) {
|
||||
matchTimes++
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//go:build pam
|
||||
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build pam
|
||||
|
||||
package pam
|
||||
|
||||
import (
|
||||
|
||||
@@ -89,7 +89,10 @@ func ToUTF8(content []byte, opts ConvertOpts) []byte {
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
if encoding == nil {
|
||||
setting.PanicInDevOrTesting("unsupported detected charset %q, it shouldn't happen", charsetLabel)
|
||||
return content
|
||||
if opts.ErrorReturnOrigin {
|
||||
return content
|
||||
}
|
||||
return bytes.ToValidUTF8(content, opts.ErrorReplacement)
|
||||
}
|
||||
|
||||
var decoded []byte
|
||||
|
||||
+34
-16
@@ -11,19 +11,28 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type CommitMessage struct {
|
||||
MessageRaw string
|
||||
messageUTF8 *string
|
||||
messageTitle *string
|
||||
messageBody *string
|
||||
}
|
||||
|
||||
// Commit represents a git commit.
|
||||
type Commit struct {
|
||||
Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache"
|
||||
|
||||
ID ObjectID
|
||||
Author *Signature // never nil
|
||||
Committer *Signature // never nil
|
||||
CommitMessage string
|
||||
Signature *CommitSignature
|
||||
CommitMessage
|
||||
|
||||
ID ObjectID
|
||||
Author *Signature // never nil
|
||||
Committer *Signature // never nil
|
||||
Signature *CommitSignature
|
||||
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache[*SubModule]
|
||||
@@ -35,19 +44,28 @@ type CommitSignature struct {
|
||||
Payload string
|
||||
}
|
||||
|
||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
||||
func (c *Commit) Message() string {
|
||||
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right
|
||||
// * When need to use commit message in templates/database, it should be valid UTF-8
|
||||
// * When need to get the original commit message, it should just use "c.CommitMessage"
|
||||
// It's not easy to refactor at the moment, many templates need to be updated and tested
|
||||
return c.CommitMessage
|
||||
func (c *CommitMessage) MessageUTF8() string {
|
||||
if c.messageUTF8 == nil {
|
||||
bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}})
|
||||
c.messageUTF8 = new(util.UnsafeBytesToString(bs))
|
||||
}
|
||||
return *c.messageUTF8
|
||||
}
|
||||
|
||||
// Summary returns first line of commit message.
|
||||
// The string is forced to be valid UTF8
|
||||
func (c *Commit) Summary() string {
|
||||
return strings.ToValidUTF8(strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0], "?")
|
||||
func (c *CommitMessage) MessageTitle() string {
|
||||
if c.messageTitle == nil {
|
||||
s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n")
|
||||
c.messageTitle = new(strings.TrimSpace(s))
|
||||
}
|
||||
return *c.messageTitle
|
||||
}
|
||||
|
||||
func (c *CommitMessage) MessageBody() string {
|
||||
if c.messageBody == nil {
|
||||
_, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n")
|
||||
c.messageBody = new(strings.TrimSpace(s))
|
||||
}
|
||||
return *c.messageBody
|
||||
}
|
||||
|
||||
// ParentID returns oid of n-th parent (0-based index).
|
||||
|
||||
@@ -66,7 +66,7 @@ func convertPGPSignature(c *object.Commit) *CommitSignature {
|
||||
func convertCommit(c *object.Commit) *Commit {
|
||||
return &Commit{
|
||||
ID: ParseGogitHash(c.Hash),
|
||||
CommitMessage: c.Message,
|
||||
CommitMessage: CommitMessage{MessageRaw: c.Message},
|
||||
Committer: &c.Committer,
|
||||
Author: &c.Author,
|
||||
Signature: convertPGPSignature(c),
|
||||
|
||||
@@ -7,6 +7,7 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"path"
|
||||
|
||||
"github.com/emirpasic/gods/trees/binaryheap"
|
||||
@@ -47,9 +48,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for k, v := range revs2 {
|
||||
revs[k] = v
|
||||
}
|
||||
maps.Copy(revs, revs2)
|
||||
}
|
||||
} else {
|
||||
revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths)
|
||||
@@ -201,7 +200,7 @@ heaploop:
|
||||
// Load the parent commits for the one we are currently examining
|
||||
numParents := current.commit.NumParents()
|
||||
var parents []cgobject.CommitNode
|
||||
for i := 0; i < numParents; i++ {
|
||||
for i := range numParents {
|
||||
parent, err := current.commit.ParentNode(i)
|
||||
if err != nil {
|
||||
break
|
||||
@@ -273,9 +272,8 @@ heaploop:
|
||||
|
||||
if len(newRemainingPaths) == 0 {
|
||||
break
|
||||
} else {
|
||||
remainingPaths = newRemainingPaths
|
||||
}
|
||||
remainingPaths = newRemainingPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader)
|
||||
}
|
||||
}
|
||||
|
||||
commit.CommitMessage = messageSB.String()
|
||||
commit.MessageRaw = messageSB.String()
|
||||
if commit.Signature != nil {
|
||||
commit.Signature.Payload = payloadSB.String()
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ signed commit`, commitFromReader.Signature.Payload)
|
||||
|
||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
||||
assert.NoError(t, err)
|
||||
commitFromReader.CommitMessage += "\n\n"
|
||||
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
||||
commitFromReader.Signature.Payload += "\n\n"
|
||||
assert.Equal(t, commitFromReader, commitFromReader2)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ empty commit`, commitFromReader.Signature.Payload)
|
||||
|
||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
||||
assert.NoError(t, err)
|
||||
commitFromReader.CommitMessage += "\n\n"
|
||||
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
||||
commitFromReader.Signature.Payload += "\n\n"
|
||||
assert.Equal(t, commitFromReader, commitFromReader2)
|
||||
}
|
||||
@@ -154,11 +154,20 @@ ISO-8859-1`, commitFromReader.Signature.Payload)
|
||||
|
||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
||||
assert.NoError(t, err)
|
||||
commitFromReader.CommitMessage += "\n\n"
|
||||
commitFromReader.CommitMessage.MessageRaw += "\n\n"
|
||||
commitFromReader.Signature.Payload += "\n\n"
|
||||
assert.Equal(t, commitFromReader, commitFromReader2)
|
||||
}
|
||||
|
||||
func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) {
|
||||
commit := &Commit{
|
||||
CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"},
|
||||
}
|
||||
assert.Equal(t, "title ÿ", commit.MessageTitle())
|
||||
assert.Equal(t, "body ÿ", commit.MessageBody())
|
||||
assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8())
|
||||
}
|
||||
|
||||
func TestHasPreviousCommit(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
|
||||
|
||||
@@ -111,8 +111,6 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) {
|
||||
len(fields), len(p.format.fieldNames))
|
||||
}
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
|
||||
var fieldKey string
|
||||
var fieldVal string
|
||||
before, after, ok := strings.Cut(field, " ")
|
||||
|
||||
@@ -116,12 +116,12 @@ func TestParser(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"refname:short": "v0.0.2",
|
||||
"contents": "Update CI config (#651)",
|
||||
"contents": "Update CI config (#651)\n\n",
|
||||
"author": "John Doe <john.doe@foo.com> 1521643174 +0000",
|
||||
},
|
||||
{
|
||||
"refname:short": "v0.0.3",
|
||||
"contents": "Fixed code sample for bash completion (#687)",
|
||||
"contents": "Fixed code sample for bash completion (#687)\n\n",
|
||||
"author": "Foo Baz <foo@baz.com> 1524836750 +0200",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
@@ -30,7 +31,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
||||
}
|
||||
|
||||
remainingCommitID := commitID
|
||||
path := ""
|
||||
var path strings.Builder
|
||||
currentTree, err := notes.Tree.gogitTreeObject()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err)
|
||||
@@ -41,17 +42,17 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
||||
for len(remainingCommitID) > 2 {
|
||||
file, err = currentTree.File(remainingCommitID)
|
||||
if err == nil {
|
||||
path += remainingCommitID
|
||||
path.WriteString(remainingCommitID)
|
||||
break
|
||||
}
|
||||
if err == object.ErrFileNotFound {
|
||||
currentTree, err = currentTree.Tree(remainingCommitID[0:2])
|
||||
path += remainingCommitID[0:2] + "/"
|
||||
path.WriteString(remainingCommitID[0:2] + "/")
|
||||
remainingCommitID = remainingCommitID[2:]
|
||||
}
|
||||
if err != nil {
|
||||
if err == object.ErrDirectoryNotFound {
|
||||
return ErrNotExist{ID: remainingCommitID, RelPath: path}
|
||||
return ErrNotExist{ID: remainingCommitID, RelPath: path.String()}
|
||||
}
|
||||
log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err)
|
||||
return err
|
||||
@@ -83,12 +84,12 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
||||
return err
|
||||
}
|
||||
|
||||
lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path})
|
||||
lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path.String()})
|
||||
if err != nil {
|
||||
log.Error("Unable to get the commit for the path %q. Error: %v", path, err)
|
||||
log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err)
|
||||
return err
|
||||
}
|
||||
note.Commit = lastCommits[path]
|
||||
note.Commit = lastCommits[path.String()]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
var _ = catBatchParseTreeEntries // bypass "unused" lint because it is only used by "nogogit"
|
||||
|
||||
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) {
|
||||
entries := make([]*TreeEntry, 0, 10)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
@@ -97,7 +96,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader
|
||||
result := LFSResult{
|
||||
Name: curPath + fname,
|
||||
SHA: curCommit.ID.String(),
|
||||
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
|
||||
Summary: curCommit.MessageTitle(),
|
||||
When: curCommit.Author.When,
|
||||
ParentHashes: curCommit.Parents,
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestGetTagCommitWithSignature(t *testing.T) {
|
||||
assert.NotNil(t, commit)
|
||||
assert.NotNil(t, commit.Signature)
|
||||
// test that signature is not in message
|
||||
assert.Equal(t, "signed-commit\n", commit.CommitMessage)
|
||||
assert.Equal(t, "signed-commit\n", commit.CommitMessage.MessageRaw)
|
||||
}
|
||||
|
||||
func TestGetCommitWithBadCommitID(t *testing.T) {
|
||||
|
||||
@@ -17,21 +17,21 @@ import (
|
||||
)
|
||||
|
||||
// CommitNodeIndex returns the index for walking commit graph
|
||||
func (r *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) {
|
||||
indexPath := filepath.Join(r.Path, "objects", "info", "commit-graph")
|
||||
func (repo *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) {
|
||||
indexPath := filepath.Join(repo.Path, "objects", "info", "commit-graph")
|
||||
|
||||
file, err := os.Open(indexPath)
|
||||
if err == nil {
|
||||
var index commitgraph.Index
|
||||
index, err = commitgraph.OpenFileIndex(file)
|
||||
if err == nil {
|
||||
return cgobject.NewGraphCommitNodeIndex(index, r.gogitRepo.Storer), file
|
||||
return cgobject.NewGraphCommitNodeIndex(index, repo.gogitRepo.Storer), file
|
||||
}
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
gitealog.Warn("Unable to read commit-graph for %s: %v", r.Path, err)
|
||||
gitealog.Warn("Unable to read commit-graph for %s: %v", repo.Path, err)
|
||||
}
|
||||
|
||||
return cgobject.NewObjectCommitNodeIndex(r.gogitRepo.Storer), nil
|
||||
return cgobject.NewObjectCommitNodeIndex(repo.gogitRepo.Storer), nil
|
||||
}
|
||||
|
||||
@@ -176,15 +176,14 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
|
||||
}
|
||||
|
||||
tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
|
||||
tag.Message = ref["contents"]
|
||||
tag.MessageRaw = ref["contents"]
|
||||
|
||||
// strip any signature if present in contents field
|
||||
_, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0)
|
||||
_, tag.MessageRaw, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.MessageRaw), 0)
|
||||
|
||||
// annotated tag with GPG signature
|
||||
if tag.Type == "tag" && ref["contents:signature"] != "" {
|
||||
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
|
||||
tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
|
||||
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s", tag.Object, tag.Name, ref["creator"], tag.MessageRaw)
|
||||
tag.Signature = &CommitSignature{
|
||||
Signature: ref["contents:signature"],
|
||||
Payload: payload,
|
||||
|
||||
@@ -64,12 +64,12 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
||||
return nil, err
|
||||
}
|
||||
tag := &Tag{
|
||||
Name: name,
|
||||
ID: tagID,
|
||||
Object: commitID,
|
||||
Type: tp,
|
||||
Tagger: commit.Committer,
|
||||
Message: commit.Message(),
|
||||
Name: name,
|
||||
ID: tagID,
|
||||
Object: commitID,
|
||||
Type: tp,
|
||||
Tagger: commit.Committer,
|
||||
CommitMessage: CommitMessage{MessageRaw: commit.CommitMessage.MessageRaw},
|
||||
}
|
||||
|
||||
repo.tagCache.Set(tagID.String(), tag)
|
||||
@@ -86,12 +86,12 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
||||
}
|
||||
|
||||
tag := &Tag{
|
||||
Name: name,
|
||||
ID: tagID,
|
||||
Object: commitID.Type().MustID(gogitTag.Target[:]),
|
||||
Type: tp,
|
||||
Tagger: &gogitTag.Tagger,
|
||||
Message: gogitTag.Message,
|
||||
Name: name,
|
||||
ID: tagID,
|
||||
Object: commitID.Type().MustID(gogitTag.Target[:]),
|
||||
Type: tp,
|
||||
Tagger: &gogitTag.Tagger,
|
||||
CommitMessage: CommitMessage{MessageRaw: gogitTag.Message},
|
||||
}
|
||||
|
||||
repo.tagCache.Set(tagID.String(), tag)
|
||||
|
||||
@@ -71,12 +71,12 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
||||
return nil, err
|
||||
}
|
||||
tag := &Tag{
|
||||
Name: name,
|
||||
ID: tagID,
|
||||
Object: commitID,
|
||||
Type: tp,
|
||||
Tagger: commit.Committer,
|
||||
Message: commit.Message(),
|
||||
Name: name,
|
||||
ID: tagID,
|
||||
Object: commitID,
|
||||
Type: tp,
|
||||
Tagger: commit.Committer,
|
||||
CommitMessage: commit.CommitMessage,
|
||||
}
|
||||
|
||||
repo.tagCache.Set(tagID.String(), tag)
|
||||
|
||||
@@ -211,13 +211,13 @@ func TestRepository_parseTagRef(t *testing.T) {
|
||||
},
|
||||
|
||||
want: &Tag{
|
||||
Name: "v1.9.1",
|
||||
ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
|
||||
Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
|
||||
Type: "commit",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n",
|
||||
Signature: nil,
|
||||
Name: "v1.9.1",
|
||||
ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
|
||||
Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
|
||||
Type: "commit",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"},
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -240,13 +240,13 @@ func TestRepository_parseTagRef(t *testing.T) {
|
||||
},
|
||||
|
||||
want: &Tag{
|
||||
Name: "v0.0.1",
|
||||
ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
|
||||
Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
|
||||
Type: "tag",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n",
|
||||
Signature: nil,
|
||||
Name: "v0.0.1",
|
||||
ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
|
||||
Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
|
||||
Type: "tag",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"},
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -263,6 +263,7 @@ func TestRepository_parseTagRef(t *testing.T) {
|
||||
|
||||
* add changelog of v1.9.1
|
||||
* Update CHANGELOG.md
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3
|
||||
@@ -298,12 +299,12 @@ qbHDASXl
|
||||
},
|
||||
|
||||
want: &Tag{
|
||||
Name: "v0.0.1",
|
||||
ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
|
||||
Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
|
||||
Type: "tag",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
|
||||
Name: "v0.0.1",
|
||||
ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
|
||||
Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
|
||||
Type: "tag",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"},
|
||||
Signature: &CommitSignature{
|
||||
Signature: `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
|
||||
+3
-2
@@ -12,12 +12,13 @@ import (
|
||||
|
||||
// Tag represents a Git tag.
|
||||
type Tag struct {
|
||||
CommitMessage
|
||||
|
||||
Name string
|
||||
ID ObjectID
|
||||
Object ObjectID // The id of this commit object
|
||||
Type string
|
||||
Tagger *Signature
|
||||
Message string
|
||||
Signature *CommitSignature
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) {
|
||||
pos += eol + 1
|
||||
}
|
||||
payload, msg, sign := parsePayloadSignature(data, pos)
|
||||
tag.Message = msg
|
||||
tag.MessageRaw = msg
|
||||
if len(sign) > 0 {
|
||||
tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
|
||||
}
|
||||
|
||||
+14
-15
@@ -28,7 +28,6 @@ tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100
|
||||
Object: MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "",
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
@@ -43,13 +42,13 @@ o
|
||||
|
||||
ono`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "test message\no\n\nono",
|
||||
Signature: nil,
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
|
||||
CommitMessage: CommitMessage{MessageRaw: "test message\no\n\nono"},
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -64,12 +63,12 @@ dummy signature
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "dummy message",
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
CommitMessage: CommitMessage{MessageRaw: "dummy message"},
|
||||
Signature: &CommitSignature{
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
dummy signature
|
||||
@@ -93,5 +92,5 @@ dummy message`,
|
||||
|
||||
tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted..."))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message)
|
||||
assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.CommitMessage.MessageRaw)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestEntryGogit(t *testing.T) {
|
||||
EntryModeTree: filemode.Dir,
|
||||
}
|
||||
for emode, fmode := range cases {
|
||||
assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode))
|
||||
assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode))
|
||||
assert.Equal(t, fmode, entryModeToGogitFileMode(emode))
|
||||
assert.Equal(t, emode, gogitFileModeToEntryMode(fmode))
|
||||
}
|
||||
}
|
||||
|
||||
+99
-40
@@ -7,6 +7,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
@@ -18,19 +20,27 @@ type Glob interface {
|
||||
}
|
||||
|
||||
type globCompiler struct {
|
||||
regexpQuestion bool
|
||||
regexpPlus bool
|
||||
superWildcardRight bool
|
||||
supportNegative bool
|
||||
|
||||
separators []rune
|
||||
nonSeparatorChars string
|
||||
globPattern []rune
|
||||
regexpPattern string
|
||||
regexp *regexp.Regexp
|
||||
builder *strings.Builder
|
||||
pos int
|
||||
negativeFlip bool
|
||||
}
|
||||
|
||||
// compileChars compiles character class patterns like [abc] or [!abc]
|
||||
func (g *globCompiler) compileChars() (string, error) {
|
||||
result := ""
|
||||
func (g *globCompiler) compileChars() error {
|
||||
g.builder.WriteByte('[')
|
||||
if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '!' {
|
||||
g.pos++
|
||||
result += "^"
|
||||
g.builder.WriteByte('^')
|
||||
}
|
||||
|
||||
for g.pos < len(g.globPattern) {
|
||||
@@ -38,81 +48,111 @@ func (g *globCompiler) compileChars() (string, error) {
|
||||
g.pos++
|
||||
|
||||
if c == ']' {
|
||||
return "[" + result + "]", nil
|
||||
g.builder.WriteByte(']')
|
||||
return nil
|
||||
}
|
||||
|
||||
if c == '\\' {
|
||||
if g.pos >= len(g.globPattern) {
|
||||
return "", errors.New("unterminated character class escape")
|
||||
return errors.New("unterminated character class escape")
|
||||
}
|
||||
result += "\\" + string(g.globPattern[g.pos])
|
||||
g.builder.WriteByte('\\')
|
||||
g.builder.WriteRune(g.globPattern[g.pos])
|
||||
g.pos++
|
||||
} else {
|
||||
result += string(c)
|
||||
g.builder.WriteRune(c)
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("unterminated character class")
|
||||
return errors.New("unterminated character class")
|
||||
}
|
||||
|
||||
// compile compiles the glob pattern into a regular expression
|
||||
func (g *globCompiler) compile(subPattern bool) (string, error) {
|
||||
result := ""
|
||||
|
||||
func (g *globCompiler) compile(subPattern bool) error {
|
||||
for g.pos < len(g.globPattern) {
|
||||
c := g.globPattern[g.pos]
|
||||
g.pos++
|
||||
|
||||
if subPattern && c == '}' {
|
||||
return "(" + result + ")", nil
|
||||
g.builder.WriteByte(')')
|
||||
return nil
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '*':
|
||||
if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '*' {
|
||||
g.pos++
|
||||
result += ".*" // match any sequence of characters
|
||||
var matchRightSep bool
|
||||
if g.superWildcardRight {
|
||||
// check "**/" pattern, then the wildcards should also match the right separator
|
||||
// e.g.: "**/docs" should match "docs"
|
||||
var rightRune rune
|
||||
if g.pos+1 < len(g.globPattern) {
|
||||
rightRune = g.globPattern[g.pos+1]
|
||||
}
|
||||
if slices.Contains(g.separators, rightRune) {
|
||||
matchRightSep = g.pos-2 < 0 || g.globPattern[g.pos-2] == rightRune
|
||||
}
|
||||
}
|
||||
if matchRightSep {
|
||||
g.pos += 2
|
||||
} else {
|
||||
g.pos++
|
||||
}
|
||||
g.builder.WriteString(".*") // match any sequence of characters
|
||||
} else {
|
||||
result += g.nonSeparatorChars + "*" // match any sequence of non-separator characters
|
||||
g.builder.WriteString(g.nonSeparatorChars)
|
||||
g.builder.WriteByte('*') // match any sequence of non-separator characters
|
||||
}
|
||||
case '?':
|
||||
result += g.nonSeparatorChars // match any single non-separator character
|
||||
if g.regexpQuestion {
|
||||
g.builder.WriteByte('?')
|
||||
} else {
|
||||
g.builder.WriteString(g.nonSeparatorChars) // match any single non-separator character
|
||||
}
|
||||
case '+':
|
||||
if g.regexpPlus {
|
||||
g.builder.WriteByte('+')
|
||||
} else {
|
||||
g.builder.WriteByte('\\')
|
||||
g.builder.WriteRune(c)
|
||||
}
|
||||
case '[':
|
||||
chars, err := g.compileChars()
|
||||
if err != nil {
|
||||
return "", err
|
||||
if err := g.compileChars(); err != nil {
|
||||
return err
|
||||
}
|
||||
result += chars
|
||||
case '{':
|
||||
subResult, err := g.compile(true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
g.builder.WriteByte('(')
|
||||
if err := g.compile(true); err != nil {
|
||||
return err
|
||||
}
|
||||
result += subResult
|
||||
case ',':
|
||||
if subPattern {
|
||||
result += "|"
|
||||
g.builder.WriteByte('|')
|
||||
} else {
|
||||
result += ","
|
||||
g.builder.WriteByte(',')
|
||||
}
|
||||
case '\\':
|
||||
if g.pos >= len(g.globPattern) {
|
||||
return "", errors.New("no character to escape")
|
||||
return errors.New("no character to escape")
|
||||
}
|
||||
result += "\\" + string(g.globPattern[g.pos])
|
||||
g.builder.WriteByte('\\')
|
||||
g.builder.WriteRune(g.globPattern[g.pos])
|
||||
g.pos++
|
||||
case '.', '+', '^', '$', '(', ')', '|':
|
||||
result += "\\" + string(c) // escape regexp special characters
|
||||
case '.', '^', '$', '(', ')', '|':
|
||||
g.builder.WriteByte('\\')
|
||||
g.builder.WriteRune(c) // escape regexp special characters
|
||||
default:
|
||||
result += string(c)
|
||||
g.builder.WriteRune(c)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
|
||||
g := &globCompiler{globPattern: []rune(pattern)}
|
||||
func initGlobCompiler(g *globCompiler, pattern string, separators []rune) (Glob, error) {
|
||||
g.globPattern = []rune(pattern)
|
||||
g.separators = separators
|
||||
g.builder = new(strings.Builder)
|
||||
|
||||
// Escape separators for use in character class
|
||||
escapedSeparators := regexp.QuoteMeta(string(separators))
|
||||
@@ -122,12 +162,18 @@ func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
|
||||
g.nonSeparatorChars = "."
|
||||
}
|
||||
|
||||
compiled, err := g.compile(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if g.supportNegative && len(g.globPattern) > 0 && g.globPattern[0] == '!' {
|
||||
g.negativeFlip = true
|
||||
g.pos++
|
||||
}
|
||||
|
||||
g.regexpPattern = "^" + compiled + "$"
|
||||
g.builder.WriteByte('^')
|
||||
if err := g.compile(false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.builder.WriteByte('$')
|
||||
g.regexpPattern = g.builder.String()
|
||||
g.builder = nil
|
||||
|
||||
regex, err := regexp.Compile(g.regexpPattern)
|
||||
if err != nil {
|
||||
@@ -139,11 +185,24 @@ func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
|
||||
}
|
||||
|
||||
func (g *globCompiler) Match(s string) bool {
|
||||
return g.regexp.MatchString(s)
|
||||
ret := g.regexp.MatchString(s)
|
||||
if g.negativeFlip {
|
||||
ret = !ret
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func Compile(pattern string, separators ...rune) (Glob, error) {
|
||||
return newGlobCompiler(pattern, separators...)
|
||||
return initGlobCompiler(&globCompiler{}, pattern, separators)
|
||||
}
|
||||
|
||||
func CompileWorkflow(pattern string) (Glob, error) {
|
||||
return initGlobCompiler(&globCompiler{
|
||||
regexpQuestion: true,
|
||||
regexpPlus: true,
|
||||
superWildcardRight: true,
|
||||
supportNegative: true,
|
||||
}, pattern, []rune{'/'})
|
||||
}
|
||||
|
||||
func MustCompile(pattern string, separators ...rune) Glob {
|
||||
|
||||
@@ -6,31 +6,39 @@ package globallock
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLocker Locker
|
||||
initOnce sync.Once
|
||||
initFunc = func() {
|
||||
switch setting.GlobalLock.ServiceType {
|
||||
case "redis":
|
||||
defaultLocker = NewRedisLocker(setting.GlobalLock.ServiceConnStr)
|
||||
case "memory":
|
||||
fallthrough
|
||||
default:
|
||||
defaultLocker = NewMemoryLocker()
|
||||
}
|
||||
} // define initFunc as a variable to make it possible to change it in tests
|
||||
defaultLocker atomic.Pointer[Locker]
|
||||
defaultMutex sync.Mutex
|
||||
)
|
||||
|
||||
func initDefaultLocker() Locker {
|
||||
switch setting.GlobalLock.ServiceType {
|
||||
case "redis":
|
||||
return NewRedisLocker(setting.GlobalLock.ServiceConnStr)
|
||||
default: // "memory"
|
||||
return NewMemoryLocker()
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultLocker returns the default locker.
|
||||
func DefaultLocker() Locker {
|
||||
initOnce.Do(func() {
|
||||
initFunc()
|
||||
})
|
||||
return defaultLocker
|
||||
ptr := defaultLocker.Load()
|
||||
if ptr == nil {
|
||||
defaultMutex.Lock()
|
||||
ptr = defaultLocker.Load()
|
||||
if ptr == nil {
|
||||
ptr = new(initDefaultLocker())
|
||||
defaultLocker.Store(ptr)
|
||||
}
|
||||
defaultMutex.Unlock()
|
||||
ptr = defaultLocker.Load()
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
|
||||
// Lock tries to acquire a lock for the given key, it uses the default locker.
|
||||
|
||||
@@ -5,7 +5,6 @@ package globallock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -15,50 +14,13 @@ import (
|
||||
|
||||
func TestLockAndDo(t *testing.T) {
|
||||
t.Run("redis", func(t *testing.T) {
|
||||
url := "redis://127.0.0.1:6379/0"
|
||||
if os.Getenv("CI") == "" {
|
||||
// Make it possible to run tests against a local redis instance
|
||||
url = os.Getenv("TEST_REDIS_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_REDIS_URL not set and not running in CI")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
oldDefaultLocker := defaultLocker
|
||||
oldInitFunc := initFunc
|
||||
defer func() {
|
||||
defaultLocker = oldDefaultLocker
|
||||
initFunc = oldInitFunc
|
||||
if defaultLocker == nil {
|
||||
initOnce = sync.Once{}
|
||||
}
|
||||
}()
|
||||
|
||||
initOnce = sync.Once{}
|
||||
initFunc = func() {
|
||||
defaultLocker = NewRedisLocker(url)
|
||||
}
|
||||
|
||||
locker := newTestRedisLocker(t)
|
||||
defaultLocker.Store(new(locker))
|
||||
testLockAndDo(t)
|
||||
require.NoError(t, defaultLocker.(*redisLocker).Close())
|
||||
require.NoError(t, locker.(*redisLocker).Close())
|
||||
})
|
||||
t.Run("memory", func(t *testing.T) {
|
||||
oldDefaultLocker := defaultLocker
|
||||
oldInitFunc := initFunc
|
||||
defer func() {
|
||||
defaultLocker = oldDefaultLocker
|
||||
initFunc = oldInitFunc
|
||||
if defaultLocker == nil {
|
||||
initOnce = sync.Once{}
|
||||
}
|
||||
}()
|
||||
|
||||
initOnce = sync.Once{}
|
||||
initFunc = func() {
|
||||
defaultLocker = NewMemoryLocker()
|
||||
}
|
||||
|
||||
defaultLocker.Store(new(NewMemoryLocker()))
|
||||
testLockAndDo(t)
|
||||
})
|
||||
}
|
||||
@@ -69,13 +31,10 @@ func testLockAndDo(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
count := 0
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(concurrency)
|
||||
for range concurrency {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
err := LockAndDo(ctx, "test", func(ctx context.Context) error {
|
||||
count++
|
||||
|
||||
// It's impossible to acquire the lock inner the function
|
||||
ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error {
|
||||
assert.Fail(t, "should not acquire the lock")
|
||||
@@ -83,13 +42,11 @@ func testLockAndDo(t *testing.T) {
|
||||
})
|
||||
assert.False(t, ok)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
assert.Equal(t, concurrency, count)
|
||||
|
||||
@@ -10,29 +10,30 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestRedisLocker(t *testing.T) Locker {
|
||||
t.Helper()
|
||||
redisURL := util.IfZero(os.Getenv("TEST_REDIS_URL"), "redis://127.0.0.1:6379/0")
|
||||
rl := NewRedisLocker(redisURL).(*redisLocker)
|
||||
err := rl.conn.Ping(t.Context()).Err()
|
||||
if err != nil && test.AllowSkipExternalService() {
|
||||
t.Skip("no redis server for testing, skipped")
|
||||
}
|
||||
require.NoError(t, err, "redis error for testing: %v", err)
|
||||
return rl
|
||||
}
|
||||
|
||||
func TestLocker(t *testing.T) {
|
||||
t.Run("redis", func(t *testing.T) {
|
||||
url := "redis://127.0.0.1:6379/0"
|
||||
if os.Getenv("CI") == "" {
|
||||
// Make it possible to run tests against a local redis instance
|
||||
url = os.Getenv("TEST_REDIS_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_REDIS_URL not set and not running in CI")
|
||||
return
|
||||
}
|
||||
}
|
||||
oldExpiry := redisLockExpiry
|
||||
redisLockExpiry = 5 * time.Second // make it shorter for testing
|
||||
defer func() {
|
||||
redisLockExpiry = oldExpiry
|
||||
}()
|
||||
|
||||
locker := NewRedisLocker(url)
|
||||
defer test.MockVariableValue(&redisLockExpiry, 5*time.Second)() // make it shorter for testing
|
||||
locker := newTestRedisLocker(t)
|
||||
testLocker(t, locker)
|
||||
testRedisLocker(t, locker.(*redisLocker))
|
||||
require.NoError(t, locker.(*redisLocker).Close())
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const redisLockKeyPrefix = "gitea:globallock:"
|
||||
@@ -23,7 +24,8 @@ const redisLockKeyPrefix = "gitea:globallock:"
|
||||
var redisLockExpiry = 30 * time.Second
|
||||
|
||||
type redisLocker struct {
|
||||
rs *redsync.Redsync
|
||||
conn redis.UniversalClient
|
||||
rs *redsync.Redsync
|
||||
|
||||
mutexM sync.Map
|
||||
closed atomic.Bool
|
||||
@@ -33,17 +35,13 @@ type redisLocker struct {
|
||||
var _ Locker = &redisLocker{}
|
||||
|
||||
func NewRedisLocker(connection string) Locker {
|
||||
conn := nosql.GetManager().GetRedisClient(connection)
|
||||
l := &redisLocker{
|
||||
rs: redsync.New(
|
||||
goredis.NewPool(
|
||||
nosql.GetManager().GetRedisClient(connection),
|
||||
),
|
||||
),
|
||||
conn: conn,
|
||||
rs: redsync.New(goredis.NewPool(conn)),
|
||||
}
|
||||
|
||||
l.extendWg.Add(1)
|
||||
l.startExtend()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func (g *Manager) RunAtTerminate(terminate func()) {
|
||||
defer g.terminateWaitGroup.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Critical("PANIC during RunAtTerminate: %v\nStacktrace: %s", err, log.Stack(2))
|
||||
log.Error("PANIC during RunAtTerminate: %v\nStacktrace: %s", err, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
terminate()
|
||||
@@ -113,7 +113,7 @@ func (g *Manager) RunAtShutdown(ctx context.Context, shutdown func()) {
|
||||
func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Critical("PANIC during RunAtShutdown: %v\nStacktrace: %s", err, log.Stack(2))
|
||||
log.Error("PANIC during RunAtShutdown: %v\nStacktrace: %s", err, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
select {
|
||||
|
||||
@@ -83,3 +83,34 @@ func HTMLPrintTag(w io.Writer, tag template.HTML, attrs map[string]string) (writ
|
||||
written += n
|
||||
return written, err
|
||||
}
|
||||
|
||||
func EscapeString(s string) template.HTML {
|
||||
return template.HTML(template.HTMLEscapeString(s))
|
||||
}
|
||||
|
||||
type HTMLBuilder struct {
|
||||
sb strings.Builder
|
||||
}
|
||||
|
||||
func (b *HTMLBuilder) WriteString(s string) *HTMLBuilder {
|
||||
b.sb.WriteString(template.HTMLEscapeString(s))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *HTMLBuilder) WriteHTML(s template.HTML) *HTMLBuilder {
|
||||
b.sb.WriteString(string(s))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *HTMLBuilder) WriteFormat(fmt template.HTML, args ...any) *HTMLBuilder {
|
||||
_, _ = HTMLPrintf(&b.sb, fmt, args...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *HTMLBuilder) HTMLString() template.HTML {
|
||||
return template.HTML(b.sb.String())
|
||||
}
|
||||
|
||||
func (b *HTMLBuilder) String() string {
|
||||
return b.sb.String()
|
||||
}
|
||||
|
||||
@@ -22,3 +22,10 @@ func TestHTMLFormat(t *testing.T) {
|
||||
assert.Equal(t, template.HTML("<>"), HTMLFormat("%s", template.URL("<>")))
|
||||
assert.Equal(t, template.HTML("&StringMethod &StringMethod"), HTMLFormat("%s %s", testStringer{}, &testStringer{}))
|
||||
}
|
||||
|
||||
func TestHTMLBuilder(t *testing.T) {
|
||||
b := &HTMLBuilder{}
|
||||
b.WriteString("<").WriteHTML("<hr>").WriteFormat("<span>%s%s</span>", ">", EscapeString(">"))
|
||||
assert.Equal(t, "<<hr><span>>></span>", b.String())
|
||||
assert.Equal(t, template.HTML("<<hr><span>>></span>"), b.HTMLString())
|
||||
}
|
||||
|
||||
@@ -87,8 +87,9 @@ func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
|
||||
if opts.ContentLength != nil {
|
||||
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
|
||||
}
|
||||
if opts.Filename != "" && opts.ContentDisposition != "" {
|
||||
header.Set("Content-Disposition", encodeContentDisposition(opts.ContentDisposition, path.Base(opts.Filename)))
|
||||
if opts.Filename != "" {
|
||||
contentDisposition := util.IfZero(opts.ContentDisposition, ContentDispositionAttachment)
|
||||
header.Set("Content-Disposition", encodeContentDisposition(contentDisposition, path.Base(opts.Filename)))
|
||||
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
}
|
||||
|
||||
|
||||
@@ -133,3 +133,11 @@ func TestServeSetHeaderContentRelated(t *testing.T) {
|
||||
// make sure sandboxed
|
||||
require.Contains(t, serveHeaderCspDefault, "; sandbox")
|
||||
}
|
||||
|
||||
func TestServeSetHeaders(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip"})
|
||||
assert.Equal(t, "attachment; filename=foo.zip", w.Header().Get("Content-Disposition"))
|
||||
ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip", ContentDisposition: ContentDispositionInline})
|
||||
assert.Equal(t, "inline; filename=foo.zip", w.Header().Get("Content-Disposition"))
|
||||
}
|
||||
|
||||
@@ -4,24 +4,19 @@
|
||||
package elasticsearch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newRealIndexer(t *testing.T) *Indexer {
|
||||
t.Helper()
|
||||
url := "http://elasticsearch:9200"
|
||||
if os.Getenv("CI") == "" {
|
||||
url = os.Getenv("TEST_ELASTICSEARCH_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_ELASTICSEARCH_URL not set and not running in CI")
|
||||
}
|
||||
}
|
||||
esURL := test.ExternalServiceHTTP(t, "TEST_ELASTICSEARCH_URL", "http://elasticsearch:9200")
|
||||
indexName := "gitea_test_" + strings.ReplaceAll(strings.ToLower(t.Name()), "/", "_")
|
||||
ix := NewIndexer(url, indexName, 1, `{"mappings":{"properties":{"x":{"type":"keyword"}}}}`)
|
||||
ix := NewIndexer(esURL, indexName, 1, `{"mappings":{"properties":{"x":{"type":"keyword"}}}}`)
|
||||
_, err := ix.Init(t.Context())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(ix.Close)
|
||||
|
||||
@@ -7,27 +7,18 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal/tests"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestElasticsearchIndexer(t *testing.T) {
|
||||
// The elasticsearch instance started by pull-db-tests.yml > test-unit > services > elasticsearch
|
||||
rawURL := "http://elastic:changeme@elasticsearch:9200"
|
||||
|
||||
if os.Getenv("CI") == "" {
|
||||
// Make it possible to run tests against a local elasticsearch instance
|
||||
rawURL = os.Getenv("TEST_ELASTICSEARCH_URL")
|
||||
if rawURL == "" {
|
||||
t.Skip("TEST_ELASTICSEARCH_URL not set and not running in CI")
|
||||
return
|
||||
}
|
||||
}
|
||||
rawURL := test.ExternalServiceHTTP(t, "TEST_ELASTICSEARCH_URL", "http://elastic:changeme@elasticsearch:9200")
|
||||
|
||||
// Go's net/http does not auto-attach URL userinfo as Basic Auth, so extract
|
||||
// it and set the header explicitly; otherwise auth-enforced clusters answer
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal/tests"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -21,18 +22,8 @@ import (
|
||||
|
||||
func TestMeilisearchIndexer(t *testing.T) {
|
||||
// The meilisearch instance started by pull-db-tests.yml > test-unit > services > meilisearch
|
||||
url := "http://meilisearch:7700"
|
||||
key := "" // auth has been disabled in test environment
|
||||
|
||||
if os.Getenv("CI") == "" {
|
||||
// Make it possible to run tests against a local meilisearch instance
|
||||
url = os.Getenv("TEST_MEILISEARCH_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_MEILISEARCH_URL not set and not running in CI")
|
||||
return
|
||||
}
|
||||
key = os.Getenv("TEST_MEILISEARCH_KEY")
|
||||
}
|
||||
url := test.ExternalServiceHTTP(t, "TEST_MEILISEARCH_URL", "http://meilisearch:7700")
|
||||
key := os.Getenv("TEST_MEILISEARCH_KEY")
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
resp, err := http.Get(url)
|
||||
|
||||
@@ -54,10 +54,6 @@ func ErrorWithSkip(skip int, format string, v ...any) {
|
||||
Log(skip+1, ERROR, format, v...)
|
||||
}
|
||||
|
||||
func Critical(format string, v ...any) {
|
||||
Log(1, ERROR, format, v...)
|
||||
}
|
||||
|
||||
var OsExiter = os.Exit
|
||||
|
||||
// Fatal records fatal log and exit process
|
||||
|
||||
@@ -5,6 +5,7 @@ package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
|
||||
}
|
||||
} else if ls := asLogStringer(v); ls != nil {
|
||||
msgArgs[i] = logStringFormatter{v: ls}
|
||||
} else if str, ok := v.(string); ok {
|
||||
msgArgs[i] = protectSensitiveInfo(str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +238,24 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
|
||||
l.SendLogEvent(event)
|
||||
}
|
||||
|
||||
func protectSensitiveInfo(s string) string {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
return s
|
||||
}
|
||||
q := u.Query()
|
||||
for _, vals := range q {
|
||||
for i := range vals {
|
||||
vals[i] = "_"
|
||||
}
|
||||
}
|
||||
masked := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: q.Encode()}
|
||||
if u.User != nil {
|
||||
masked.User = url.User("_masked_")
|
||||
}
|
||||
return masked.String()
|
||||
}
|
||||
|
||||
func (l *LoggerImpl) GetLevel() Level {
|
||||
return Level(l.level.Load())
|
||||
}
|
||||
|
||||
@@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) {
|
||||
|
||||
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs())
|
||||
}
|
||||
|
||||
func TestProtectSensitiveInfo(t *testing.T) {
|
||||
assert.Empty(t, protectSensitiveInfo(""))
|
||||
assert.Equal(t, "mailto:user@example.com", protectSensitiveInfo("mailto:user@example.com"))
|
||||
assert.Equal(t, "https://example.com", protectSensitiveInfo("https://example.com"))
|
||||
assert.Equal(t, "https://_masked_@example.com/path?k=_", protectSensitiveInfo("https://u:p@example.com/path?k=v#hash"))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestRenderCodePreview(t *testing.T) {
|
||||
},
|
||||
})
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithMarkupType(markdown.MarkupName), input)
|
||||
buffer, err := testRenderString(markup.NewTestRenderContext().WithMarkupType(markdown.MarkupName), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
@@ -24,10 +24,16 @@ var (
|
||||
localMetas = map[string]string{"user": testRepoOwnerName, "repo": testRepoName}
|
||||
)
|
||||
|
||||
func testRenderString(ctx *markup.RenderContext, content string) (string, error) {
|
||||
var buf strings.Builder
|
||||
err := markup.Render(ctx, strings.NewReader(content), &buf)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestRender_Commits(t *testing.T) {
|
||||
test := func(input, expected string) {
|
||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
|
||||
buffer, err := markup.RenderString(rctx, input)
|
||||
buffer, err := testRenderString(rctx, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@@ -75,7 +81,7 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
|
||||
buffer, err := markup.RenderString(rctx, input)
|
||||
buffer, err := testRenderString(rctx, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@@ -119,7 +125,7 @@ func TestRender_links(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
buffer, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@@ -234,7 +240,7 @@ func TestRender_email(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
res, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res), "input: %s", input)
|
||||
}
|
||||
@@ -321,7 +327,7 @@ func TestRender_emoji(t *testing.T) {
|
||||
|
||||
test := func(input, expected string) {
|
||||
expected = strings.ReplaceAll(expected, "&", "&")
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
buffer, err := testRenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ func (r *RenderInternal) ProtectSafeAttrs(content template.HTML) template.HTML {
|
||||
}
|
||||
|
||||
func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt template.HTML, a ...any) error {
|
||||
_, err := w.Write([]byte(r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...))))
|
||||
htmlStr := r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...))
|
||||
_, err := io.WriteString(w, string(htmlStr))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -119,12 +119,33 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(KindDetails, r.renderDetails)
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(KindAttention, r.renderAttention)
|
||||
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
reg.Register(KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
// renderCodeBlock wraps indented code blocks like the fenced renderer
|
||||
func (r *HTMLRenderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
opening := r.renderInternal.ProtectSafeAttrs(`<div class="code-block-container code-overflow-scroll"><pre class="code-block"><code>`)
|
||||
if _, err := w.WriteString(string(opening)); err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
lines := n.Lines()
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
r.Writer.RawWrite(w, line.Value(source))
|
||||
}
|
||||
} else {
|
||||
if _, err := w.WriteString("</code></pre></div>"); err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Document)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/common"
|
||||
@@ -266,7 +267,9 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
|
||||
var buf strings.Builder
|
||||
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
|
||||
return "", err
|
||||
log.Warn("Unable to RenderString: %v, content: %s", err, giteautil.TruncateRunes(content, 200))
|
||||
err = nil
|
||||
return htmlutil.EscapeString(content), err
|
||||
}
|
||||
return template.HTML(buf.String()), nil
|
||||
}
|
||||
|
||||
@@ -601,7 +601,7 @@ func TestMarkdownUlDir(t *testing.T) {
|
||||
`, string(result))
|
||||
}
|
||||
|
||||
func TestMarkdownFencedCodeBlock(t *testing.T) {
|
||||
func TestMarkdownCodeBlock(t *testing.T) {
|
||||
testRender := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
@@ -618,4 +618,6 @@ func TestMarkdownFencedCodeBlock(t *testing.T) {
|
||||
testRender("```js:app.ts\ncode\n```", jsCommon)
|
||||
testRender("```js,ignore\ncode\n```", jsCommon)
|
||||
testRender("```js ignore\ncode\n```", jsCommon)
|
||||
testRender(" code\n", prefix+`<code>code`+nl+`</code>`+suffix)
|
||||
testRender(" <script>alert(1)</script>\n", prefix+`<code><script>alert(1)</script>`+nl+`</code>`+suffix)
|
||||
}
|
||||
|
||||
@@ -106,31 +106,27 @@ func (r *orgWriter) resolveLink(link string) string {
|
||||
// WriteRegularLink renders images, links or videos
|
||||
func (r *orgWriter) WriteRegularLink(l org.RegularLink) {
|
||||
link := r.resolveLink(l.URL)
|
||||
|
||||
printHTML := func(html template.HTML, a ...any) {
|
||||
_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...))
|
||||
}
|
||||
// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
|
||||
switch l.Kind() {
|
||||
case "image":
|
||||
if l.Description == nil {
|
||||
printHTML(`<img src="%s" alt="%s">`, link, link)
|
||||
_, _ = htmlutil.HTMLPrintf(r, `<img src="%s" alt="%s">`, link, link)
|
||||
} else {
|
||||
imageSrc := r.resolveLink(org.String(l.Description...))
|
||||
printHTML(`<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc)
|
||||
_, _ = htmlutil.HTMLPrintf(r, `<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc)
|
||||
}
|
||||
case "video":
|
||||
if l.Description == nil {
|
||||
printHTML(`<video src="%s">%s</video>`, link, link)
|
||||
_, _ = htmlutil.HTMLPrintf(r, `<video src="%s">%s</video>`, link, link)
|
||||
} else {
|
||||
videoSrc := r.resolveLink(org.String(l.Description...))
|
||||
printHTML(`<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
|
||||
_, _ = htmlutil.HTMLPrintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
|
||||
}
|
||||
default:
|
||||
var description any = link
|
||||
if l.Description != nil {
|
||||
description = template.HTML(r.WriteNodesAsString(l.Description...)) // orgmode HTMLWriter outputs HTML content
|
||||
}
|
||||
printHTML(`<a href="%s">%s</a>`, link, description)
|
||||
_, _ = htmlutil.HTMLPrintf(r, `<a href="%s">%s</a>`, link, description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,15 +198,6 @@ func Render(rctx *RenderContext, origInput io.Reader, output io.Writer) error {
|
||||
return RenderWithRenderer(rctx, renderer, input, output)
|
||||
}
|
||||
|
||||
// RenderString renders Markup string to HTML with all specific handling stuff and return string
|
||||
func RenderString(ctx *RenderContext, content string) (string, error) {
|
||||
var buf strings.Builder
|
||||
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func RenderIFrame(ctx *RenderContext, opts *ExternalRendererOptions, output io.Writer) error {
|
||||
ownerName, repoName := ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"]
|
||||
refSubURL := ctx.RenderOptions.Metas["RefTypeNameSubURL"]
|
||||
|
||||
@@ -40,5 +40,7 @@ type MigrateOptions struct {
|
||||
MirrorInterval string `json:"mirror_interval"`
|
||||
|
||||
AWSAccessKeyID string
|
||||
AWSSecretAccessKey string
|
||||
AWSSecretAccessKey string `json:",omitempty"`
|
||||
|
||||
AWSSecretAccessKeyEncrypted string `json:"aws_secret_access_key_encrypted,omitempty"`
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (m *Manager) GetLevelDB(connection string) (db *leveldb.DB, err error) {
|
||||
defer func() {
|
||||
recovered = recover()
|
||||
if recovered != nil {
|
||||
log.Critical("PANIC during GetLevelDB: %v\nStacktrace: %s", recovered, log.Stack(2))
|
||||
log.Error("PANIC during GetLevelDB: %v\nStacktrace: %s", recovered, log.Stack(2))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m *Manager) GetRedisClient(connection string) (client redis.UniversalClien
|
||||
defer func() {
|
||||
recovered = recover()
|
||||
if recovered != nil {
|
||||
log.Critical("PANIC during GetRedisClient: %v\nStacktrace: %s", recovered, log.Stack(2))
|
||||
log.Error("PANIC during GetRedisClient: %v\nStacktrace: %s", recovered, log.Stack(2))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
@@ -91,7 +91,7 @@ func (e *MarshalEncoder) marshal(v any) error {
|
||||
val := reflect.ValueOf(v)
|
||||
typ := reflect.TypeOf(v)
|
||||
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
if typ.Kind() == reflect.Pointer {
|
||||
val = val.Elem()
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
@@ -192,12 +192,13 @@ func isViteDevRequest(req *http.Request) bool {
|
||||
|
||||
// Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports:
|
||||
// - {WebSite}/public/assets/... (e.g. SVG icons from "{RepoRoot}/public/assets/img/svg/")
|
||||
// - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS code, but:
|
||||
// - {WebSite}/assets/<file>.json: exception for frontend-imported repo-root assets:
|
||||
// - KEEP IN MIND: all static frontend assets are served from "{AssetFS}/assets" to "{WebSite}/assets" by Gitea Web Server
|
||||
// - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public"
|
||||
// - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets
|
||||
// - "{RepoRoot}/assets/*.json" just happens to live under the dir name "assets"; it is not related to frontend assets
|
||||
// - BAD DESIGN: indeed it is a "conflicted and polluted name" sample
|
||||
if path == "/assets/emoji.json" {
|
||||
switch path {
|
||||
case "/assets/emoji.json", "/assets/codemirror-languages.json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/nosql"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -57,10 +58,10 @@ func TestBaseRedis(t *testing.T) {
|
||||
}()
|
||||
if !waitRedisReady("redis://127.0.0.1:6379/0", 0) {
|
||||
redisServer = redisServerCmd(t)
|
||||
if redisServer == nil && os.Getenv("CI") == "" {
|
||||
t.Skip("redis-server not found")
|
||||
return
|
||||
if redisServer == nil && test.AllowSkipExternalService() {
|
||||
t.Skip("redis server command not found, skipped")
|
||||
}
|
||||
require.NotNil(t, redisServer)
|
||||
assert.NoError(t, redisServer.Start())
|
||||
require.True(t, waitRedisReady("redis://127.0.0.1:6379/0", 5*time.Second), "start redis-server")
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
RepoID: repo.ID,
|
||||
Name: branch,
|
||||
CommitID: commit.ID.String(),
|
||||
CommitMessage: commit.Summary(),
|
||||
CommitMessage: commit.MessageTitle(),
|
||||
PusherID: doerID,
|
||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||
})
|
||||
@@ -112,7 +112,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
RepoID: repo.ID,
|
||||
Name: branch,
|
||||
CommitID: commit.ID.String(),
|
||||
CommitMessage: commit.Summary(),
|
||||
CommitMessage: commit.MessageTitle(),
|
||||
PusherID: doerID,
|
||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||
})
|
||||
|
||||
@@ -152,7 +152,7 @@ func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
|
||||
func CommitToPushCommit(commit *git.Commit) *PushCommit {
|
||||
return &PushCommit{
|
||||
Sha1: commit.ID.String(),
|
||||
Message: commit.Message(),
|
||||
Message: commit.MessageUTF8(),
|
||||
AuthorEmail: commit.Author.Email,
|
||||
AuthorName: commit.Author.Name,
|
||||
CommitterEmail: commit.Committer.Email,
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestCommitToPushCommit(t *testing.T) {
|
||||
ID: sha1,
|
||||
Author: sig,
|
||||
Committer: sig,
|
||||
CommitMessage: "Commit Message",
|
||||
CommitMessage: git.CommitMessage{MessageRaw: "Commit Message"},
|
||||
})
|
||||
assert.Equal(t, hexString, pushCommit.Sha1)
|
||||
assert.Equal(t, "Commit Message", pushCommit.Message)
|
||||
@@ -176,13 +176,13 @@ func TestListToPushCommits(t *testing.T) {
|
||||
ID: hash1,
|
||||
Author: sig,
|
||||
Committer: sig,
|
||||
CommitMessage: "Message1",
|
||||
CommitMessage: git.CommitMessage{MessageRaw: "Message1"},
|
||||
},
|
||||
{
|
||||
ID: hash2,
|
||||
Author: sig,
|
||||
Committer: sig,
|
||||
CommitMessage: "Message2",
|
||||
CommitMessage: git.CommitMessage{MessageRaw: "Message2"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultSQLiteBusyTimeout = 20 * 1000
|
||||
|
||||
var (
|
||||
// SupportedDatabaseTypes includes all XORM supported databases type, sqlite3 maybe added by `database_sqlite3.go`
|
||||
// SupportedDatabaseTypes includes all XORM supported databases type, sqlite3 maybe added by the tag-controlled drivers
|
||||
SupportedDatabaseTypes = []string{"mysql", "postgres", "mssql"}
|
||||
// DatabaseTypeNames contains the friendly names for all database types
|
||||
DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", "sqlite3": "SQLite3"}
|
||||
DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", DatabaseTypeSQLite3: "SQLite3"}
|
||||
|
||||
// Database holds the database settings
|
||||
Database = struct {
|
||||
@@ -39,8 +41,8 @@ var (
|
||||
AutoMigration bool
|
||||
SlowQueryThreshold time.Duration
|
||||
}{
|
||||
SQLiteBusyTimeout: 500,
|
||||
IterateBufferSize: 50,
|
||||
IterateBufferSize: 50,
|
||||
SlowQueryThreshold: 5 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -63,7 +65,13 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||
Database.CharsetCollation = sec.Key("CHARSET_COLLATION").String()
|
||||
|
||||
Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db"))
|
||||
Database.SQLiteBusyTimeout = sec.Key("SQLITE_TIMEOUT").MustInt(500)
|
||||
|
||||
Database.SQLiteBusyTimeout = sec.Key("SQLITE_TIMEOUT").MustInt(defaultSQLiteBusyTimeout)
|
||||
// mattn driver isn't really affected by this timeout, but other drivers are affected
|
||||
// the default value was 500 (0.5s), to avoid breaking existing users, make sure the timeout is long enough (at least, 5 seconds)
|
||||
if Database.SQLiteBusyTimeout < 5000 {
|
||||
Database.SQLiteBusyTimeout = defaultSQLiteBusyTimeout
|
||||
}
|
||||
Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("")
|
||||
|
||||
Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2)
|
||||
@@ -79,14 +87,16 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
|
||||
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(Database.SlowQueryThreshold)
|
||||
}
|
||||
|
||||
// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent
|
||||
type DatabaseType string
|
||||
|
||||
const DatabaseTypeSQLite3 = "sqlite3"
|
||||
|
||||
func (t DatabaseType) IsSQLite3() bool {
|
||||
return t == "sqlite3"
|
||||
return t == DatabaseTypeSQLite3
|
||||
}
|
||||
|
||||
func (t DatabaseType) IsMySQL() bool {
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
const IncomingEmailTokenPlaceholder = "%{token}"
|
||||
|
||||
var IncomingEmail = struct {
|
||||
Enabled bool
|
||||
ReplyToAddress string
|
||||
TokenPlaceholder string `ini:"-"`
|
||||
Host string
|
||||
Port int
|
||||
UseTLS bool `ini:"USE_TLS"`
|
||||
@@ -28,7 +29,6 @@ var IncomingEmail = struct {
|
||||
}{
|
||||
Mailbox: "INBOX",
|
||||
DeleteHandledMessage: true,
|
||||
TokenPlaceholder: "%{token}",
|
||||
MaximumMessageSize: 10485760,
|
||||
}
|
||||
|
||||
@@ -54,19 +54,10 @@ func checkReplyToAddress() error {
|
||||
return errors.New("name must not be set")
|
||||
}
|
||||
|
||||
c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
|
||||
switch c {
|
||||
case 0:
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
||||
case 1:
|
||||
default:
|
||||
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
|
||||
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
|
||||
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
|
||||
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
|
||||
}
|
||||
|
||||
parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
|
||||
if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ var (
|
||||
AddCoCommitterTrailers: true,
|
||||
RetargetChildrenOnMerge: true,
|
||||
DelayCheckForInactiveDays: 7,
|
||||
DefaultTitleSource: RepoPRTitleSourceFirstCommit,
|
||||
DefaultTitleSource: RepoPRTitleSourceAuto,
|
||||
},
|
||||
|
||||
// Issue settings
|
||||
|
||||
+27
-20
@@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// settings
|
||||
@@ -163,32 +164,38 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
|
||||
func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
rootSec := rootCfg.Section("")
|
||||
mustNotRunAsRoot(rootSec)
|
||||
|
||||
runModeValue := os.Getenv("GITEA_RUN_MODE")
|
||||
runModeValue = util.IfZero(runModeValue, rootSec.Key("RUN_MODE").String())
|
||||
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
|
||||
IsProd = !strings.EqualFold(runModeValue, "dev") // TODO: can use case-sensitive comparing in the future
|
||||
RunMode = util.Iif(IsProd, "prod", "dev")
|
||||
|
||||
// there is a separate check: mustCurrentRunUserMatch (IsRunUserMatchCurrentUser)
|
||||
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
|
||||
}
|
||||
|
||||
func mustNotRunAsRoot(rootSec ConfigSection) {
|
||||
if os.Getuid() != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mustRunAsRoot := os.Getenv("SNAP") != "" && os.Getenv("SNAP_NAME") != "" // snap container runs the app as uid=0
|
||||
if mustRunAsRoot {
|
||||
return
|
||||
}
|
||||
|
||||
// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
|
||||
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
|
||||
unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
|
||||
unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
|
||||
RunMode = os.Getenv("GITEA_RUN_MODE")
|
||||
if RunMode == "" {
|
||||
RunMode = rootSec.Key("RUN_MODE").MustString("prod")
|
||||
}
|
||||
allowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") || // check gitea config
|
||||
optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() // check gitea env var
|
||||
|
||||
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
|
||||
RunMode = strings.ToLower(RunMode)
|
||||
if RunMode != "dev" {
|
||||
RunMode = "prod"
|
||||
}
|
||||
IsProd = RunMode != "dev"
|
||||
|
||||
// check if we run as root
|
||||
if os.Getuid() == 0 {
|
||||
if !unsafeAllowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
|
||||
}
|
||||
log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
if !allowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission.")
|
||||
}
|
||||
log.Warn("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
}
|
||||
|
||||
// HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet.
|
||||
|
||||
+4
-7
@@ -155,7 +155,6 @@ func sessionHandler(session ssh.Session) {
|
||||
process.SetSysProcAttribute(cmd)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
log.Error("SSH: Start: %v", err)
|
||||
@@ -169,21 +168,19 @@ func sessionHandler(session ssh.Session) {
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
defer stdout.Close()
|
||||
if _, err := io.Copy(session, stdout); err != nil {
|
||||
log.Error("Failed to write stdout to session. %s", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
defer stderr.Close()
|
||||
if _, err := io.Copy(session.Stderr(), stderr); err != nil {
|
||||
log.Error("Failed to write stderr to session. %s", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// Ensure all the output has been written before we wait on the command
|
||||
// to exit.
|
||||
|
||||
@@ -20,7 +20,7 @@ func listen(server *ssh.Server) {
|
||||
if err != nil {
|
||||
select {
|
||||
case <-graceful.GetManager().IsShutdown():
|
||||
log.Critical("Failed to start SSH server: %v", err)
|
||||
log.Error("Failed to start SSH server: %v", err)
|
||||
default:
|
||||
log.Fatal("Failed to start SSH server: %v", err)
|
||||
}
|
||||
|
||||
@@ -5,25 +5,22 @@ package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAzureBlobStorage(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("azureBlobStorage not present outside of CI")
|
||||
return
|
||||
}
|
||||
endpoint := test.ExternalServiceHTTP(t, "TEST_AZURESTORAGE_ENDPOINT", "http://devstoreaccount1.azurite.local:10000")
|
||||
storageType := setting.AzureBlobStorageType
|
||||
config := &setting.Storage{
|
||||
AzureBlobConfig: setting.AzureBlobStorageConfig{
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url
|
||||
Endpoint: "http://devstoreaccount1.azurite.local:10000",
|
||||
Endpoint: endpoint,
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key
|
||||
AccountName: "devstoreaccount1",
|
||||
AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
|
||||
@@ -77,15 +74,11 @@ func TestAzureBlobStoragePath(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_azureBlobObject(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("azureBlobStorage not present outside of CI")
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := test.ExternalServiceHTTP(t, "TEST_AZURESTORAGE_ENDPOINT", "http://devstoreaccount1.azurite.local:10000")
|
||||
s, err := NewStorage(setting.AzureBlobStorageType, &setting.Storage{
|
||||
AzureBlobConfig: setting.AzureBlobStorageConfig{
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url
|
||||
Endpoint: "http://devstoreaccount1.azurite.local:10000",
|
||||
Endpoint: endpoint,
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key
|
||||
AccountName: "devstoreaccount1",
|
||||
AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
|
||||
|
||||
@@ -11,20 +11,18 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMinioStorage(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("minioStorage not present outside of CI")
|
||||
return
|
||||
}
|
||||
endpoint := test.ExternalServiceHTTP(t, "TEST_MINIO_ENDPOINT", "minio:9000")
|
||||
storageType := setting.MinioStorageType
|
||||
config := &setting.Storage{
|
||||
MinioConfig: setting.MinioStorageConfig{
|
||||
Endpoint: "minio:9000",
|
||||
Endpoint: endpoint,
|
||||
AccessKeyID: "123456",
|
||||
SecretAccessKey: "12345678",
|
||||
Bucket: "gitea",
|
||||
|
||||
+12
-4
@@ -113,11 +113,13 @@ type Repository struct {
|
||||
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
|
||||
AllowSquash bool `json:"allow_squash_merge"`
|
||||
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
||||
AllowMergeUpdate bool `json:"allow_merge_update"`
|
||||
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
||||
AllowManualMerge bool `json:"allow_manual_merge"`
|
||||
AutodetectManualMerge bool `json:"autodetect_manual_merge"`
|
||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||
DefaultMergeStyle string `json:"default_merge_style"`
|
||||
DefaultUpdateStyle string `json:"default_update_style"`
|
||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Internal bool `json:"internal"`
|
||||
@@ -125,10 +127,12 @@ type Repository struct {
|
||||
// ObjectFormatName of the underlying git repository
|
||||
ObjectFormatName ObjectFormatName `json:"object_format_name"`
|
||||
// swagger:strfmt date-time
|
||||
MirrorUpdated time.Time `json:"mirror_updated"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
|
||||
Topics []string `json:"topics"`
|
||||
Licenses []string `json:"licenses"`
|
||||
MirrorUpdated time.Time `json:"mirror_updated"`
|
||||
// swagger:strfmt date-time
|
||||
MirrorLastSyncAt time.Time `json:"mirror_last_sync_at"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
|
||||
Topics []string `json:"topics"`
|
||||
Licenses []string `json:"licenses"`
|
||||
}
|
||||
|
||||
// CreateRepoOption options when creating repository
|
||||
@@ -222,12 +226,16 @@ type EditRepoOption struct {
|
||||
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
|
||||
// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
|
||||
AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"`
|
||||
// either `true` to allow updating pull request branch by merge, or `false` to prevent it.
|
||||
AllowMergeUpdate *bool `json:"allow_merge_update,omitempty"`
|
||||
// either `true` to allow updating pull request branch by rebase, or `false` to prevent it.
|
||||
AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
|
||||
// set to `true` to delete pr branch after merge by default
|
||||
DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
|
||||
// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
|
||||
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
|
||||
// set to an update style to be used by this repository: "merge" or "rebase".
|
||||
DefaultUpdateStyle *string `json:"default_update_style,omitempty"`
|
||||
// set to `true` to allow edits from maintainers by default
|
||||
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
||||
// set to `true` to archive this repository.
|
||||
|
||||
@@ -50,6 +50,9 @@ type BranchProtection struct {
|
||||
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
EnableBypassAllowlist bool `json:"enable_bypass_allowlist"`
|
||||
BypassAllowlistUsernames []string `json:"bypass_allowlist_usernames"`
|
||||
BypassAllowlistTeams []string `json:"bypass_allowlist_teams"`
|
||||
EnableStatusCheck bool `json:"enable_status_check"`
|
||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||
RequiredApprovals int64 `json:"required_approvals"`
|
||||
@@ -90,6 +93,9 @@ type CreateBranchProtectionOption struct {
|
||||
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
EnableBypassAllowlist bool `json:"enable_bypass_allowlist"`
|
||||
BypassAllowlistUsernames []string `json:"bypass_allowlist_usernames"`
|
||||
BypassAllowlistTeams []string `json:"bypass_allowlist_teams"`
|
||||
EnableStatusCheck bool `json:"enable_status_check"`
|
||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||
RequiredApprovals int64 `json:"required_approvals"`
|
||||
@@ -123,6 +129,9 @@ type EditBranchProtectionOption struct {
|
||||
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
EnableBypassAllowlist *bool `json:"enable_bypass_allowlist"`
|
||||
BypassAllowlistUsernames []string `json:"bypass_allowlist_usernames"`
|
||||
BypassAllowlistTeams []string `json:"bypass_allowlist_teams"`
|
||||
EnableStatusCheck *bool `json:"enable_status_check"`
|
||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||
RequiredApprovals *int64 `json:"required_approvals"`
|
||||
|
||||
@@ -132,10 +132,9 @@ func newFuncMapWebPage() template.FuncMap {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// misc (TODO: move them to MiscUtils to avoid bloating the main func map)
|
||||
"ActionContent2Commits": ActionContent2Commits,
|
||||
"IsMultilineCommitMessage": isMultilineCommitMessage,
|
||||
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
||||
"MirrorRemoteAddress": mirrorRemoteAddress,
|
||||
"ActionContent2Commits": ActionContent2Commits,
|
||||
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
||||
"MirrorRemoteAddress": mirrorRemoteAddress,
|
||||
|
||||
"FilenameIsImage": filenameIsImage,
|
||||
"TabSizeClass": tabSizeClass,
|
||||
|
||||
@@ -54,17 +54,14 @@ func TestScopedTemplateSetFuncMap(t *testing.T) {
|
||||
out1 := bytes.Buffer{}
|
||||
out2 := bytes.Buffer{}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
wg.Go(func() {
|
||||
err := ts.newExecutor(funcMap1).Execute(&out1, nil)
|
||||
assert.NoError(t, err)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
})
|
||||
wg.Go(func() {
|
||||
err := ts.newExecutor(funcMap2).Execute(&out2, nil)
|
||||
assert.NoError(t, err)
|
||||
wg.Done()
|
||||
}()
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Equal(t, "base1\ntest1\nbase1\ntest1", out1.String())
|
||||
assert.Equal(t, "base2\ntest2\nbase2\ntest2", out2.String())
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
)
|
||||
|
||||
type ActionsUtils struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewActionsUtils(ctx context.Context) *ActionsUtils {
|
||||
return &ActionsUtils{ctx: ctx}
|
||||
}
|
||||
|
||||
func (a *ActionsUtils) CommitStatusesToActionsStatuses(statuses []*git_model.CommitStatus) actions_module.CommitActionsStatusMap {
|
||||
return actions_module.GetCommitActionsStatusMap(a.ctx, statuses)
|
||||
}
|
||||
@@ -52,11 +52,6 @@ func sortArrow(normSort, revSort, urlSort string, isDefault bool) template.HTML
|
||||
return ""
|
||||
}
|
||||
|
||||
// isMultilineCommitMessage checks to see if a commit message contains multiple lines.
|
||||
func isMultilineCommitMessage(msg string) bool {
|
||||
return strings.Count(strings.TrimSpace(msg), "\n") >= 1
|
||||
}
|
||||
|
||||
// Actioner describes an action
|
||||
type Actioner interface {
|
||||
GetOpType() activities_model.ActionType
|
||||
|
||||
@@ -79,20 +79,14 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, re
|
||||
|
||||
// RenderCommitBody extracts the body of a commit message without its title.
|
||||
func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML {
|
||||
msgLine := strings.TrimSpace(msg)
|
||||
lineEnd := strings.IndexByte(msgLine, '\n')
|
||||
if lineEnd > 0 {
|
||||
msgLine = msgLine[lineEnd+1:]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
|
||||
if len(msgLine) == 0 {
|
||||
_, body, _ := strings.Cut(strings.TrimSpace(msg), "\n")
|
||||
body = strings.TrimFunc(body, unicode.IsSpace)
|
||||
if body == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo)
|
||||
htmlContent := template.HTML(template.HTMLEscapeString(msgLine))
|
||||
htmlContent := template.HTML(template.HTMLEscapeString(body))
|
||||
renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent)
|
||||
if err != nil {
|
||||
log.Error("PostProcessCommitMessage: %v", err)
|
||||
@@ -127,23 +121,7 @@ func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ut *RenderUtils) RenderLabelWithLink(label *issues_model.Label, link any) template.HTML {
|
||||
var attrHref template.HTML
|
||||
switch link.(type) {
|
||||
case template.URL, string:
|
||||
attrHref = htmlutil.HTMLFormat(`href="%s"`, link)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected type %T for link", link))
|
||||
}
|
||||
return ut.renderLabelWithTag(label, "a", attrHref)
|
||||
}
|
||||
|
||||
func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
|
||||
return ut.renderLabelWithTag(label, "span", "")
|
||||
}
|
||||
|
||||
// RenderLabel renders a label
|
||||
func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, tagAttrs template.HTML) template.HTML {
|
||||
locale := ut.ctx.Value(translation.ContextKey).(translation.Locale)
|
||||
var extraCSSClasses string
|
||||
textColor := util.ContrastColor(label.Color)
|
||||
@@ -157,8 +135,8 @@ func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, ta
|
||||
|
||||
if labelScope == "" {
|
||||
// Regular label
|
||||
return htmlutil.HTMLFormat(`<%s %s class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></%s>`,
|
||||
tagName, tagAttrs, extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name), tagName)
|
||||
return htmlutil.HTMLFormat(`<span class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></span>`,
|
||||
extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name))
|
||||
}
|
||||
|
||||
// Scoped label
|
||||
@@ -193,29 +171,25 @@ func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, ta
|
||||
|
||||
if label.ExclusiveOrder > 0 {
|
||||
// <scope> | <label> | <order>
|
||||
return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`<div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`<div class="ui label scope-right">%d</div>`+
|
||||
`</%s>`,
|
||||
tagName, tagAttrs,
|
||||
`</span>`,
|
||||
extraCSSClasses, descriptionText,
|
||||
textColor, scopeColor, scopeHTML,
|
||||
textColor, itemColor, itemHTML,
|
||||
label.ExclusiveOrder,
|
||||
tagName)
|
||||
label.ExclusiveOrder)
|
||||
}
|
||||
|
||||
// <scope> | <label>
|
||||
return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`</%s>`,
|
||||
tagName, tagAttrs,
|
||||
`</span>`,
|
||||
extraCSSClasses, descriptionText,
|
||||
textColor, scopeColor, scopeHTML,
|
||||
textColor, itemColor, itemHTML,
|
||||
tagName)
|
||||
textColor, itemColor, itemHTML)
|
||||
}
|
||||
|
||||
// RenderEmoji renders html text with emoji post processors
|
||||
@@ -252,18 +226,19 @@ func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:rev
|
||||
func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
|
||||
isPullRequest := issue != nil && issue.IsPull
|
||||
baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues"))
|
||||
var htmlCode strings.Builder
|
||||
htmlCode.WriteString(`<span class="labels-list">`)
|
||||
var htmlCode htmlutil.HTMLBuilder
|
||||
htmlCode.WriteHTML(`<span class="labels-list">`)
|
||||
for _, label := range labels {
|
||||
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
||||
if label == nil {
|
||||
continue
|
||||
}
|
||||
link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID)
|
||||
htmlCode.WriteString(string(ut.RenderLabelWithLink(label, template.URL(link))))
|
||||
htmlCode.WriteFormat(`<a class="item" href="%s?labels=%d">`, baseLink, label.ID)
|
||||
htmlCode.WriteHTML(ut.RenderLabel(label))
|
||||
htmlCode.WriteHTML("</a>")
|
||||
}
|
||||
htmlCode.WriteString("</span>")
|
||||
return template.HTML(htmlCode.String())
|
||||
htmlCode.WriteHTML("</span>")
|
||||
return htmlCode.HTMLString()
|
||||
}
|
||||
|
||||
func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML {
|
||||
|
||||
@@ -206,16 +206,15 @@ func TestRenderLabels(t *testing.T) {
|
||||
expected = `/owner/repo/pulls?labels=123`
|
||||
assert.Contains(t, ut.RenderLabels([]*issues.Label{label}, "/owner/repo", issue), expected)
|
||||
|
||||
expectedLabel := `<a href="<>" class="ui label " style="color: #fff !important; background-color: label-color !important;" data-tooltip-content title=""><span class="gt-ellipsis">label-name</span></a>`
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "<>")))
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, template.URL("<>"))))
|
||||
expectedLabel := `<span class="ui label " style="color: #fff !important; background-color: label-color !important;" data-tooltip-content title=""><span class="gt-ellipsis">label-name</span></span>`
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabel(label)))
|
||||
|
||||
label = &issues.Label{ID: 123, Name: "</>", Exclusive: true}
|
||||
expectedLabel = `<a href="" class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important"><</div><div class="ui label scope-right" style="color: #fff !important; background-color: #000000 !important">></div></a>`
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "")))
|
||||
expectedLabel = `<span class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important"><</div><div class="ui label scope-right" style="color: #fff !important; background-color: #000000 !important">></div></span>`
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabel(label)))
|
||||
label = &issues.Label{ID: 123, Name: "</>", Exclusive: true, ExclusiveOrder: 1}
|
||||
expectedLabel = `<a href="" class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important"><</div><div class="ui label scope-middle" style="color: #fff !important; background-color: #000000 !important">></div><div class="ui label scope-right">1</div></a>`
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "")))
|
||||
expectedLabel = `<span class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important"><</div><div class="ui label scope-middle" style="color: #fff !important; background-color: #000000 !important">></div><div class="ui label scope-right">1</div></span>`
|
||||
assert.Equal(t, expectedLabel, string(ut.RenderLabel(label)))
|
||||
}
|
||||
|
||||
func TestUserMention(t *testing.T) {
|
||||
|
||||
@@ -11,7 +11,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -141,3 +144,41 @@ func CompressGzip(content string) *bytes.Buffer {
|
||||
_ = cw.Close()
|
||||
return buf
|
||||
}
|
||||
|
||||
var AllowSkipExternalService = sync.OnceValue(func() bool {
|
||||
isLocalTesting := os.Getenv("CI") == ""
|
||||
ciSkipExternal, _ := strconv.ParseBool(os.Getenv("GITEA_TEST_CI_SKIP_EXTERNAL"))
|
||||
return isLocalTesting || ciSkipExternal
|
||||
})
|
||||
|
||||
type TestingT interface {
|
||||
Helper()
|
||||
Skipf(format string, args ...any)
|
||||
Errorf(format string, args ...any)
|
||||
Fatalf(format string, args ...any)
|
||||
}
|
||||
|
||||
func ExternalServiceHTTP(t TestingT, envVarName, def string) string {
|
||||
t.Helper()
|
||||
val := util.IfZero(os.Getenv(envVarName), def)
|
||||
if val == "" {
|
||||
if AllowSkipExternalService() {
|
||||
t.Skipf("skipping test because %s is not set", envVarName)
|
||||
} else {
|
||||
t.Fatalf("%s is not set, but skipping is not allowed in CI", envVarName)
|
||||
}
|
||||
}
|
||||
// minio's endpoint is "host:port" pattern
|
||||
testURL := util.Iif(strings.Contains(val, "://"), val, "http://"+val)
|
||||
resp, err := http.Get(testURL)
|
||||
if err != nil {
|
||||
if AllowSkipExternalService() {
|
||||
t.Skipf("skipping test because %s is not ready", val)
|
||||
} else {
|
||||
t.Fatalf("%s is not ready, but skipping is not allowed in CI", val)
|
||||
}
|
||||
} else {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
@@ -131,8 +132,8 @@ type middlewareProvider = func(next http.Handler) http.Handler
|
||||
|
||||
func executeMiddlewaresHandler(w http.ResponseWriter, r *http.Request, middlewares []middlewareProvider, endpoint http.HandlerFunc) {
|
||||
handler := endpoint
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
handler = middlewares[i](handler).ServeHTTP
|
||||
for _, middleware := range slices.Backward(middlewares) {
|
||||
handler = middleware(handler).ServeHTTP
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func AssignForm(form any, data map[string]any) {
|
||||
typ := reflect.TypeOf(form)
|
||||
val := reflect.ValueOf(form)
|
||||
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
for typ.Kind() == reflect.Pointer {
|
||||
typ = typ.Elem()
|
||||
val = val.Elem()
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
|
||||
data["ErrorMsg"] = l.TrString("form.unknown_error")
|
||||
|
||||
typ := reflect.TypeOf(f)
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
if typ.Kind() == reflect.Pointer {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
|
||||
// do not respond to other requests, to simulate a real sub-path environment
|
||||
resp.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
resp.WriteHeader(http.StatusNotFound)
|
||||
_, _ = resp.Write([]byte(htmlutil.HTMLFormat(`404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)))
|
||||
_, _ = htmlutil.HTMLPrintf(resp, `404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)
|
||||
return
|
||||
}
|
||||
normalized = true
|
||||
|
||||
@@ -11,19 +11,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/web/types"
|
||||
)
|
||||
|
||||
// NewLoggerHandler is a handler that will log routing to the router log taking account of
|
||||
// routing information
|
||||
func NewLoggerHandler() func(next http.Handler) http.Handler {
|
||||
manager := requestRecordsManager{
|
||||
requestRecords: map[uint64]*requestRecord{},
|
||||
}
|
||||
manager.startSlowQueryDetector(3 * time.Second)
|
||||
|
||||
logger := log.GetLogger("router")
|
||||
manager.print = logPrinter(logger)
|
||||
return manager.handler
|
||||
}
|
||||
|
||||
var (
|
||||
startMessage = log.NewColoredValue("started ", log.DEBUG.ColorAttributes()...)
|
||||
slowMessage = log.NewColoredValue("slow ", log.WARN.ColorAttributes()...)
|
||||
@@ -89,7 +76,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
|
||||
}
|
||||
|
||||
var status int
|
||||
if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok {
|
||||
if v, ok := record.respWriter.(types.ResponseStatusProvider); ok {
|
||||
status = v.WrittenStatus()
|
||||
}
|
||||
logLevel := record.logLevel
|
||||
|
||||
@@ -6,7 +6,6 @@ package routing
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,26 +28,21 @@ const (
|
||||
EndEvent
|
||||
)
|
||||
|
||||
// Printer is used to output the log for a request
|
||||
type Printer func(trigger Event, record *requestRecord)
|
||||
// logPrinterFunc is used to output the log for a request
|
||||
type logPrinterFunc func(trigger Event, record *requestRecord)
|
||||
|
||||
type requestRecordsManager struct {
|
||||
print Printer
|
||||
|
||||
lock sync.Mutex
|
||||
|
||||
requestRecords map[uint64]*requestRecord
|
||||
count uint64
|
||||
type loggerRequestManager struct {
|
||||
logPrint logPrinterFunc
|
||||
reqRecords sync.Map // it only contains the active requests which haven't been detected as "slow"
|
||||
}
|
||||
|
||||
func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Duration) {
|
||||
func (manager *loggerRequestManager) startSlowQueryDetector(threshold time.Duration) {
|
||||
go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Service: SlowQueryDetector", process.SystemProcessType, true)
|
||||
defer finished()
|
||||
// This go-routine checks all active requests every second.
|
||||
// If a request has been running for a long time (eg: /user/events), we also print a log with "still-executing" message
|
||||
// After the "still-executing" log is printed, the record will be removed from the map to prevent from duplicated logs in future
|
||||
|
||||
// We do not care about accurate duration here. It just does the check periodically, 0.5s or 1.5s are all OK.
|
||||
t := time.NewTicker(time.Second)
|
||||
for {
|
||||
@@ -58,69 +52,39 @@ func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Dura
|
||||
case <-t.C:
|
||||
now := time.Now()
|
||||
|
||||
var slowRequests []*requestRecord
|
||||
|
||||
// find all slow requests with lock
|
||||
manager.lock.Lock()
|
||||
for index, record := range manager.requestRecords {
|
||||
if now.Sub(record.startTime) < threshold {
|
||||
continue
|
||||
}
|
||||
|
||||
slowRequests = append(slowRequests, record)
|
||||
delete(manager.requestRecords, index)
|
||||
}
|
||||
manager.lock.Unlock()
|
||||
|
||||
// print logs for slow requests
|
||||
for _, record := range slowRequests {
|
||||
manager.print(StillExecutingEvent, record)
|
||||
}
|
||||
manager.reqRecords.Range(func(key, value any) bool {
|
||||
index, record := key.(uint64), value.(*requestRecord)
|
||||
if now.Sub(record.startTime) >= threshold {
|
||||
manager.logPrint(StillExecutingEvent, record)
|
||||
manager.reqRecords.Delete(index)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *requestRecordsManager) handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
record := &requestRecord{
|
||||
startTime: time.Now(),
|
||||
request: req,
|
||||
responseWriter: w,
|
||||
func (manager *loggerRequestManager) handleRequestRecord(record *requestRecord) func() {
|
||||
manager.reqRecords.Store(record.index, record)
|
||||
manager.logPrint(StartEvent, record)
|
||||
|
||||
return func() {
|
||||
// just in case there is a panic. now the panics are all recovered in middleware.go
|
||||
localPanicErr := recover()
|
||||
if localPanicErr != nil {
|
||||
record.lock.Lock()
|
||||
record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2))
|
||||
record.lock.Unlock()
|
||||
}
|
||||
|
||||
// generate a record index an insert into the map
|
||||
manager.lock.Lock()
|
||||
record.index = manager.count
|
||||
manager.count++
|
||||
manager.requestRecords[record.index] = record
|
||||
manager.lock.Unlock()
|
||||
manager.reqRecords.Delete(record.index)
|
||||
manager.logPrint(EndEvent, record)
|
||||
|
||||
defer func() {
|
||||
// just in case there is a panic. now the panics are all recovered in middleware.go
|
||||
localPanicErr := recover()
|
||||
if localPanicErr != nil {
|
||||
record.lock.Lock()
|
||||
record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2))
|
||||
record.lock.Unlock()
|
||||
}
|
||||
|
||||
// remove from the record map
|
||||
manager.lock.Lock()
|
||||
delete(manager.requestRecords, record.index)
|
||||
manager.lock.Unlock()
|
||||
|
||||
// log the end of the request
|
||||
manager.print(EndEvent, record)
|
||||
|
||||
if localPanicErr != nil {
|
||||
// the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error
|
||||
panic(localPanicErr)
|
||||
}
|
||||
}()
|
||||
|
||||
req = req.WithContext(context.WithValue(req.Context(), contextKey, record))
|
||||
manager.print(StartEvent, record)
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
if localPanicErr != nil {
|
||||
// the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error
|
||||
panic(localPanicErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// NewRequestInfoHandler is a handler that saves request info into request context.
|
||||
// If router logger is enabled, it will also print request logs and detect slow requests.
|
||||
func NewRequestInfoHandler() func(next http.Handler) http.Handler {
|
||||
var reqLogger *loggerRequestManager
|
||||
if setting.IsRouteLogEnabled() {
|
||||
reqLogger = &loggerRequestManager{
|
||||
logPrint: logPrinter(log.GetLogger("router")),
|
||||
}
|
||||
reqLogger.startSlowQueryDetector(3 * time.Second)
|
||||
}
|
||||
var requestCounter atomic.Uint64
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
record := &requestRecord{
|
||||
index: requestCounter.Add(1),
|
||||
startTime: time.Now(),
|
||||
respWriter: w,
|
||||
}
|
||||
req = req.WithContext(context.WithValue(req.Context(), contextKey, record))
|
||||
record.request = req
|
||||
if reqLogger != nil {
|
||||
end := reqLogger.handleRequestRecord(record)
|
||||
defer end()
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,20 +12,20 @@ import (
|
||||
)
|
||||
|
||||
type requestRecord struct {
|
||||
// index of the record in the records map
|
||||
index uint64
|
||||
|
||||
// immutable fields
|
||||
startTime time.Time
|
||||
request *http.Request
|
||||
responseWriter http.ResponseWriter
|
||||
index uint64 // unique number (per process) for the request
|
||||
startTime time.Time
|
||||
request *http.Request
|
||||
respWriter http.ResponseWriter
|
||||
|
||||
// mutex
|
||||
lock sync.RWMutex
|
||||
|
||||
// mutable fields
|
||||
// below are mutable fields
|
||||
funcInfo *FuncInfo
|
||||
// * for "mark as long polling"
|
||||
isLongPolling bool
|
||||
logLevel log.Level
|
||||
funcInfo *FuncInfo
|
||||
panicError error
|
||||
// * for router logger
|
||||
logLevel log.Level
|
||||
panicError error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user