mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-09 03:09:17 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
)
|
||||
|
||||
const (
|
||||
githubEventPullRequest = "pull_request"
|
||||
githubEventPullRequestTarget = "pull_request_target"
|
||||
githubEventPullRequestReviewComment = "pull_request_review_comment"
|
||||
githubEventPullRequestReview = "pull_request_review"
|
||||
githubEventRegistryPackage = "registry_package"
|
||||
githubEventCreate = "create"
|
||||
githubEventDelete = "delete"
|
||||
githubEventFork = "fork"
|
||||
githubEventPush = "push"
|
||||
githubEventIssues = "issues"
|
||||
githubEventIssueComment = "issue_comment"
|
||||
githubEventRelease = "release"
|
||||
githubEventPullRequestComment = "pull_request_comment"
|
||||
)
|
||||
|
||||
func convertFromGithubEvent(evt *jobparser.Event) string {
|
||||
switch evt.Name {
|
||||
case githubEventPullRequest, githubEventPullRequestTarget, githubEventPullRequestReview,
|
||||
githubEventPullRequestReviewComment:
|
||||
return string(webhook_module.HookEventPullRequest)
|
||||
case githubEventRegistryPackage:
|
||||
return string(webhook_module.HookEventPackage)
|
||||
case githubEventCreate, githubEventDelete, githubEventFork, githubEventPush,
|
||||
githubEventIssues, githubEventIssueComment, githubEventRelease, githubEventPullRequestComment:
|
||||
fallthrough
|
||||
default:
|
||||
return evt.Name
|
||||
}
|
||||
}
|
||||
+207
-134
@@ -44,6 +44,32 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
||||
f, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := io.ReadAll(f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := jobparser.ParseRawOn(&workflow.RawOn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) (map[string][]byte, error) {
|
||||
entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
@@ -52,29 +78,17 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy
|
||||
|
||||
workflows := make(map[string][]byte, len(entries))
|
||||
for _, entry := range entries {
|
||||
f, err := entry.Blob().DataAsync()
|
||||
content, err := GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := io.ReadAll(f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
events, err := jobparser.ParseRawOn(&workflow.RawOn)
|
||||
events, err := GetEventsFromContent(content)
|
||||
if err != nil {
|
||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
for _, evt := range events {
|
||||
if evt.Name != triggedEvent.Event() {
|
||||
continue
|
||||
}
|
||||
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
||||
if detectMatched(commit, triggedEvent, payload, evt) {
|
||||
workflows[entry.Name()] = content
|
||||
}
|
||||
@@ -85,138 +99,197 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy
|
||||
}
|
||||
|
||||
func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
||||
if convertFromGithubEvent(evt) != string(triggedEvent) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch triggedEvent {
|
||||
case webhook_module.HookEventCreate,
|
||||
webhook_module.HookEventDelete,
|
||||
webhook_module.HookEventFork,
|
||||
webhook_module.HookEventIssueAssign,
|
||||
webhook_module.HookEventIssueLabel,
|
||||
webhook_module.HookEventIssueMilestone,
|
||||
webhook_module.HookEventPullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestMilestone,
|
||||
webhook_module.HookEventPullRequestComment,
|
||||
webhook_module.HookEventPullRequestReviewApproved,
|
||||
webhook_module.HookEventPullRequestReviewRejected,
|
||||
webhook_module.HookEventPullRequestReviewComment,
|
||||
webhook_module.HookEventWiki,
|
||||
webhook_module.HookEventRepository,
|
||||
webhook_module.HookEventRelease,
|
||||
webhook_module.HookEventPackage:
|
||||
if len(evt.Acts) != 0 {
|
||||
log.Warn("Ignore unsupported %s event arguments %q", triggedEvent, evt.Acts)
|
||||
}
|
||||
// no special filter parameters for these events, just return true if name matched
|
||||
return true
|
||||
|
||||
case webhook_module.HookEventPush:
|
||||
return matchPushEvent(commit, payload.(*api.PushPayload), evt)
|
||||
|
||||
case webhook_module.HookEventIssues:
|
||||
return matchIssuesEvent(commit, payload.(*api.IssuePayload), evt)
|
||||
|
||||
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync:
|
||||
return matchPullRequestEvent(commit, payload.(*api.PullRequestPayload), evt)
|
||||
|
||||
case webhook_module.HookEventIssueComment:
|
||||
return matchIssueCommentEvent(commit, payload.(*api.IssueCommentPayload), evt)
|
||||
|
||||
default:
|
||||
log.Warn("unsupported event %q", triggedEvent)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool {
|
||||
// with no special filter parameters
|
||||
if len(evt.Acts) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
switch triggedEvent {
|
||||
case webhook_module.HookEventCreate:
|
||||
fallthrough
|
||||
case webhook_module.HookEventDelete:
|
||||
fallthrough
|
||||
case webhook_module.HookEventFork:
|
||||
log.Warn("unsupported event %q", triggedEvent.Event())
|
||||
return false
|
||||
case webhook_module.HookEventPush:
|
||||
pushPayload := payload.(*api.PushPayload)
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "branches", "tags":
|
||||
refShortName := git.RefName(pushPayload.Ref).ShortName()
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(refShortName) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "branches", "tags":
|
||||
refShortName := git.RefName(pushPayload.Ref).ShortName()
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(refShortName) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||
} else {
|
||||
for _, val := range vals {
|
||||
matched := false
|
||||
for _, file := range filesChanged {
|
||||
if glob.MustCompile(val, '/').Match(file) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||
} else {
|
||||
for _, val := range vals {
|
||||
matched := false
|
||||
for _, file := range filesChanged {
|
||||
if glob.MustCompile(val, '/').Match(file) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Warn("unsupported condition %q", cond)
|
||||
}
|
||||
}
|
||||
return matchTimes == len(evt.Acts)
|
||||
|
||||
case webhook_module.HookEventIssues:
|
||||
fallthrough
|
||||
case webhook_module.HookEventIssueAssign:
|
||||
fallthrough
|
||||
case webhook_module.HookEventIssueLabel:
|
||||
fallthrough
|
||||
case webhook_module.HookEventIssueMilestone:
|
||||
fallthrough
|
||||
case webhook_module.HookEventIssueComment:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequest:
|
||||
prPayload := payload.(*api.PullRequestPayload)
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "types":
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(prPayload.Action)) {
|
||||
if matched {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
case "branches":
|
||||
refShortName := git.RefName(prPayload.PullRequest.Base.Ref).ShortName()
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(refShortName) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||
} else {
|
||||
for _, val := range vals {
|
||||
matched := false
|
||||
for _, file := range filesChanged {
|
||||
if glob.MustCompile(val, '/').Match(file) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Warn("unsupported condition %q", cond)
|
||||
}
|
||||
default:
|
||||
log.Warn("push event unsupported condition %q", cond)
|
||||
}
|
||||
return matchTimes == len(evt.Acts)
|
||||
case webhook_module.HookEventPullRequestAssign:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestLabel:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestMilestone:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestComment:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestReviewApproved:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestReviewRejected:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestReviewComment:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPullRequestSync:
|
||||
fallthrough
|
||||
case webhook_module.HookEventWiki:
|
||||
fallthrough
|
||||
case webhook_module.HookEventRepository:
|
||||
fallthrough
|
||||
case webhook_module.HookEventRelease:
|
||||
fallthrough
|
||||
case webhook_module.HookEventPackage:
|
||||
fallthrough
|
||||
default:
|
||||
log.Warn("unsupported event %q", triggedEvent.Event())
|
||||
}
|
||||
return false
|
||||
return matchTimes == len(evt.Acts)
|
||||
}
|
||||
|
||||
func matchIssuesEvent(commit *git.Commit, issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
|
||||
// with no special filter parameters
|
||||
if len(evt.Acts) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "types":
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(issuePayload.Action)) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Warn("issue event unsupported condition %q", cond)
|
||||
}
|
||||
}
|
||||
return matchTimes == len(evt.Acts)
|
||||
}
|
||||
|
||||
func matchPullRequestEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
|
||||
// with no special filter parameters
|
||||
if len(evt.Acts) == 0 {
|
||||
// defaultly, only pull request opened and synchronized will trigger workflow
|
||||
return prPayload.Action == api.HookIssueSynchronized || prPayload.Action == api.HookIssueOpened
|
||||
}
|
||||
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "types":
|
||||
action := prPayload.Action
|
||||
if prPayload.Action == api.HookIssueSynchronized {
|
||||
action = "synchronize"
|
||||
}
|
||||
log.Trace("matching pull_request %s with %v", action, vals)
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
case "branches":
|
||||
refShortName := git.RefName(prPayload.PullRequest.Base.Ref).ShortName()
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(refShortName) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||
} else {
|
||||
for _, val := range vals {
|
||||
matched := false
|
||||
for _, file := range filesChanged {
|
||||
if glob.MustCompile(val, '/').Match(file) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Warn("pull request event unsupported condition %q", cond)
|
||||
}
|
||||
}
|
||||
return matchTimes == len(evt.Acts)
|
||||
}
|
||||
|
||||
func matchIssueCommentEvent(commit *git.Commit, issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {
|
||||
// with no special filter parameters
|
||||
if len(evt.Acts) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "types":
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(issueCommentPayload.Action)) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Warn("issue comment unsupported condition %q", cond)
|
||||
}
|
||||
}
|
||||
return matchTimes == len(evt.Acts)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
texttemplate "text/template"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -213,6 +215,8 @@ func (ctx *Context) RedirectToFirst(location ...string) {
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
}
|
||||
|
||||
var templateExecutingErr = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): executing (?:"(.*)" at <(.*)>: )?`)
|
||||
|
||||
// HTML calls Context.HTML and renders the template to HTTP response
|
||||
func (ctx *Context) HTML(status int, name base.TplName) {
|
||||
log.Debug("Template: %s", name)
|
||||
@@ -228,6 +232,34 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||
ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template")
|
||||
return
|
||||
}
|
||||
if execErr, ok := err.(texttemplate.ExecError); ok {
|
||||
if groups := templateExecutingErr.FindStringSubmatch(err.Error()); len(groups) > 0 {
|
||||
errorTemplateName, lineStr, posStr := groups[1], groups[2], groups[3]
|
||||
target := ""
|
||||
if len(groups) == 6 {
|
||||
target = groups[5]
|
||||
}
|
||||
line, _ := strconv.Atoi(lineStr) // Cannot error out as groups[2] is [1-9][0-9]*
|
||||
pos, _ := strconv.Atoi(posStr) // Cannot error out as groups[3] is [1-9][0-9]*
|
||||
filename, filenameErr := templates.GetAssetFilename("templates/" + errorTemplateName + ".tmpl")
|
||||
if filenameErr != nil {
|
||||
filename = "(template) " + errorTemplateName
|
||||
}
|
||||
if errorTemplateName != string(name) {
|
||||
filename += " (subtemplate of " + string(name) + ")"
|
||||
}
|
||||
err = fmt.Errorf("%w\nin template file %s:\n%s", err, filename, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
|
||||
} else {
|
||||
filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl")
|
||||
if filenameErr != nil {
|
||||
filename = "(template) " + execErr.Name
|
||||
}
|
||||
if execErr.Name != string(name) {
|
||||
filename += " (subtemplate of " + string(name) + ")"
|
||||
}
|
||||
err = fmt.Errorf("%w\nin template file %s", err, filename)
|
||||
}
|
||||
}
|
||||
ctx.ServerError("Render failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ type Pagination struct {
|
||||
urlParams []string
|
||||
}
|
||||
|
||||
// NewPagination creates a new instance of the Pagination struct
|
||||
func NewPagination(total, page, issueNum, numPages int) *Pagination {
|
||||
// NewPagination creates a new instance of the Pagination struct.
|
||||
// "pagingNum" is "page size" or "limit", "current" is "page"
|
||||
func NewPagination(total, pagingNum, current, numPages int) *Pagination {
|
||||
p := &Pagination{}
|
||||
p.Paginater = paginator.New(total, page, issueNum, numPages)
|
||||
p.Paginater = paginator.New(total, pagingNum, current, numPages)
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
+2
-13
@@ -660,20 +660,9 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := ctx.Repo.GitRepo.GetTags(0, 0)
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: not a git repository ") {
|
||||
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
|
||||
ctx.Repo.Repository.Status = repo_model.RepositoryBroken
|
||||
ctx.Repo.Repository.IsEmpty = true
|
||||
ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
|
||||
// Only allow access to base of repo or settings
|
||||
if !isHomeOrSettings {
|
||||
ctx.Redirect(ctx.Repo.RepoLink)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetTags", err)
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
|
||||
@@ -31,7 +31,7 @@ func commonCheckStorage(ctx context.Context, logger log.Logger, autofix bool, op
|
||||
totalSize, orphanedSize := int64(0), int64(0)
|
||||
|
||||
var pathsToDelete []string
|
||||
if err := opts.storer.IterateObjects(func(p string, obj storage.Object) error {
|
||||
if err := opts.storer.IterateObjects("", func(p string, obj storage.Object) error {
|
||||
defer obj.Close()
|
||||
|
||||
totalCount++
|
||||
|
||||
@@ -56,6 +56,10 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p
|
||||
} else if treepath != "" {
|
||||
files = append(files, treepath)
|
||||
}
|
||||
// Use the :(literal) pathspec magic to handle edge cases with files named like ":file.txt" or "*.jpg"
|
||||
for i, file := range files {
|
||||
files[i] = ":(literal)" + file
|
||||
}
|
||||
cmd.AddDashesAndList(files...)
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -36,6 +36,8 @@ var (
|
||||
once sync.Once
|
||||
|
||||
cache *lru.TwoQueueCache
|
||||
|
||||
githubStyles = styles.Get("github")
|
||||
)
|
||||
|
||||
// NewContext loads custom highlight map from local config
|
||||
@@ -121,7 +123,7 @@ func CodeFromLexer(lexer chroma.Lexer, code string) string {
|
||||
return code
|
||||
}
|
||||
// style not used for live site but need to pass something
|
||||
err = formatter.Format(htmlw, styles.GitHub, iterator)
|
||||
err = formatter.Format(htmlw, githubStyles, iterator)
|
||||
if err != nil {
|
||||
log.Error("Can't format code: %v", err)
|
||||
return code
|
||||
@@ -184,7 +186,7 @@ func File(fileName, language string, code []byte) ([]string, string, error) {
|
||||
lines := make([]string, 0, len(tokensLines))
|
||||
for _, tokens := range tokensLines {
|
||||
iterator = chroma.Literator(tokens...)
|
||||
err = formatter.Format(htmlBuf, styles.GitHub, iterator)
|
||||
err = formatter.Format(htmlBuf, githubStyles, iterator)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("can't format code: %w", err)
|
||||
}
|
||||
|
||||
+53
-14
@@ -7,36 +7,38 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var directories = make(directorySet)
|
||||
|
||||
// Locale reads the content of a specific locale from static/bindata or custom path.
|
||||
func Locale(name string) ([]byte, error) {
|
||||
return fileFromDir(path.Join("locale", util.CleanPath(name)))
|
||||
return fileFromOptionsDir("locale", name)
|
||||
}
|
||||
|
||||
// Readme reads the content of a specific readme from static/bindata or custom path.
|
||||
func Readme(name string) ([]byte, error) {
|
||||
return fileFromDir(path.Join("readme", util.CleanPath(name)))
|
||||
return fileFromOptionsDir("readme", name)
|
||||
}
|
||||
|
||||
// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
|
||||
func Gitignore(name string) ([]byte, error) {
|
||||
return fileFromDir(path.Join("gitignore", util.CleanPath(name)))
|
||||
return fileFromOptionsDir("gitignore", name)
|
||||
}
|
||||
|
||||
// License reads the content of a specific license from static/bindata or custom path.
|
||||
func License(name string) ([]byte, error) {
|
||||
return fileFromDir(path.Join("license", util.CleanPath(name)))
|
||||
return fileFromOptionsDir("license", name)
|
||||
}
|
||||
|
||||
// Labels reads the content of a specific labels from static/bindata or custom path.
|
||||
func Labels(name string) ([]byte, error) {
|
||||
return fileFromDir(path.Join("label", util.CleanPath(name)))
|
||||
return fileFromOptionsDir("label", name)
|
||||
}
|
||||
|
||||
// WalkLocales reads the content of a specific locale
|
||||
@@ -79,17 +81,54 @@ func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, e
|
||||
return nil
|
||||
}
|
||||
|
||||
func statDirIfExist(dir string) ([]string, error) {
|
||||
isDir, err := util.IsDir(dir)
|
||||
// mustLocalPathAbs coverts a path to absolute path
|
||||
// FIXME: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
|
||||
func mustLocalPathAbs(s string) string {
|
||||
abs, err := filepath.Abs(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check if static directory %s is a directory. %w", dir, err)
|
||||
// This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
|
||||
log.Fatal("Unable to get absolute path for %q: %v", s, err)
|
||||
}
|
||||
if !isDir {
|
||||
return nil, nil
|
||||
return abs
|
||||
}
|
||||
|
||||
func joinLocalPaths(baseDirs []string, subDir string, elems ...string) (paths []string) {
|
||||
abs := make([]string, len(elems)+2)
|
||||
abs[1] = subDir
|
||||
copy(abs[2:], elems)
|
||||
for _, baseDir := range baseDirs {
|
||||
abs[0] = mustLocalPathAbs(baseDir)
|
||||
paths = append(paths, util.FilePathJoinAbs(abs...))
|
||||
}
|
||||
files, err := util.StatDir(dir, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read directory %q. %w", dir, err)
|
||||
return paths
|
||||
}
|
||||
|
||||
func listLocalDirIfExist(baseDirs []string, subDir string, elems ...string) (files []string, err error) {
|
||||
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
|
||||
isDir, err := util.IsDir(localPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check if path %q is a directory. %w", localPath, err)
|
||||
} else if !isDir {
|
||||
continue
|
||||
}
|
||||
|
||||
dirFiles, err := util.StatDir(localPath, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read directory %q. %w", localPath, err)
|
||||
}
|
||||
files = append(files, dirFiles...)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func readLocalFile(baseDirs []string, subDir string, elems ...string) ([]byte, error) {
|
||||
for _, localPath := range joinLocalPaths(baseDirs, subDir, elems...) {
|
||||
data, err := os.ReadFile(localPath)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
log.Error("Unable to read file %q. Error: %v", localPath, err)
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
@@ -6,62 +6,26 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var directories = make(directorySet)
|
||||
|
||||
// Dir returns all files from static or custom directory.
|
||||
func Dir(name string) ([]string, error) {
|
||||
if directories.Filled(name) {
|
||||
return directories.Get(name), nil
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
for _, dir := range []string{
|
||||
path.Join(setting.CustomPath, "options", name), // custom dir
|
||||
path.Join(setting.StaticRootPath, "options", name), // static dir
|
||||
} {
|
||||
files, err := statDirIfExist(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, files...)
|
||||
result, err := listLocalDirIfExist([]string{setting.CustomPath, setting.StaticRootPath}, "options", name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return directories.AddAndGet(name, result), nil
|
||||
}
|
||||
|
||||
// fileFromDir is a helper to read files from static or custom path.
|
||||
func fileFromDir(name string) ([]byte, error) {
|
||||
customPath := path.Join(setting.CustomPath, "options", name)
|
||||
|
||||
isFile, err := util.IsFile(customPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
|
||||
}
|
||||
if isFile {
|
||||
return os.ReadFile(customPath)
|
||||
}
|
||||
|
||||
staticPath := path.Join(setting.StaticRootPath, "options", name)
|
||||
|
||||
isFile, err = util.IsFile(staticPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s is a file. Error: %v", staticPath, err)
|
||||
}
|
||||
if isFile {
|
||||
return os.ReadFile(staticPath)
|
||||
}
|
||||
|
||||
return []byte{}, fmt.Errorf("Asset file does not exist: %s", name)
|
||||
// fileFromOptionsDir is a helper to read files from custom or static path.
|
||||
func fileFromOptionsDir(elems ...string) ([]byte, error) {
|
||||
return readLocalFile([]string{setting.CustomPath, setting.StaticRootPath}, "options", elems...)
|
||||
}
|
||||
|
||||
// IsDynamic will return false when using embedded data (-tags bindata)
|
||||
|
||||
+10
-29
@@ -8,33 +8,20 @@ package options
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var directories = make(directorySet)
|
||||
|
||||
// Dir returns all files from bindata or custom directory.
|
||||
// Dir returns all files from custom directory or bindata.
|
||||
func Dir(name string) ([]string, error) {
|
||||
if directories.Filled(name) {
|
||||
return directories.Get(name), nil
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
for _, dir := range []string{
|
||||
path.Join(setting.CustomPath, "options", name), // custom dir
|
||||
// no static dir
|
||||
} {
|
||||
files, err := statDirIfExist(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, files...)
|
||||
result, err := listLocalDirIfExist([]string{setting.CustomPath}, "options", name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := AssetDir(name)
|
||||
@@ -64,24 +51,18 @@ func AssetDir(dirName string) ([]string, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// fileFromDir is a helper to read files from bindata or custom path.
|
||||
func fileFromDir(name string) ([]byte, error) {
|
||||
customPath := path.Join(setting.CustomPath, "options", name)
|
||||
|
||||
isFile, err := util.IsFile(customPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
|
||||
}
|
||||
if isFile {
|
||||
return os.ReadFile(customPath)
|
||||
// fileFromOptionsDir is a helper to read files from custom path or bindata.
|
||||
func fileFromOptionsDir(elems ...string) ([]byte, error) {
|
||||
// only try custom dir, no static dir
|
||||
if data, err := readLocalFile([]string{setting.CustomPath}, "options", elems...); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
f, err := Assets.Open(name)
|
||||
f, err := Assets.Open(util.PathJoinRelX(elems...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
|
||||
OptionalDependencies: meta.OptionalDependencies,
|
||||
Bin: meta.Bin,
|
||||
Readme: meta.Readme,
|
||||
Repository: meta.Repository,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ func TestParsePackage(t *testing.T) {
|
||||
packageDescription := "Test Description"
|
||||
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
|
||||
integrity := "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg=="
|
||||
repository := Repository{
|
||||
Type: "gitea",
|
||||
URL: "http://localhost:3000/gitea/test.git",
|
||||
}
|
||||
|
||||
t.Run("InvalidUpload", func(t *testing.T) {
|
||||
p, err := ParsePackage(bytes.NewReader([]byte{0}))
|
||||
@@ -242,6 +246,7 @@ func TestParsePackage(t *testing.T) {
|
||||
Dist: PackageDistribution{
|
||||
Integrity: integrity,
|
||||
},
|
||||
Repository: repository,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -272,5 +277,7 @@ func TestParsePackage(t *testing.T) {
|
||||
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
|
||||
assert.Contains(t, p.Metadata.Dependencies, "package")
|
||||
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
|
||||
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
|
||||
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ type Metadata struct {
|
||||
OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"`
|
||||
Bin map[string]string `json:"bin,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
Repository Repository `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package swift
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingManifestFile = util.NewInvalidArgumentErrorf("Package.swift file is missing")
|
||||
ErrManifestFileTooLarge = util.NewInvalidArgumentErrorf("Package.swift file is too large")
|
||||
ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
|
||||
|
||||
manifestPattern = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
|
||||
toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
|
||||
)
|
||||
|
||||
const (
|
||||
maxManifestFileSize = 128 * 1024
|
||||
|
||||
PropertyScope = "swift.scope"
|
||||
PropertyName = "swift.name"
|
||||
PropertyRepositoryURL = "swift.repository_url"
|
||||
)
|
||||
|
||||
// Package represents a Swift package
|
||||
type Package struct {
|
||||
RepositoryURLs []string
|
||||
Metadata *Metadata
|
||||
}
|
||||
|
||||
// Metadata represents the metadata of a Swift package
|
||||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Author Person `json:"author,omitempty"`
|
||||
Manifests map[string]*Manifest `json:"manifests,omitempty"`
|
||||
}
|
||||
|
||||
// Manifest represents a Package.swift file
|
||||
type Manifest struct {
|
||||
Content string `json:"content"`
|
||||
ToolsVersion string `json:"tools_version,omitempty"`
|
||||
}
|
||||
|
||||
// https://schema.org/SoftwareSourceCode
|
||||
type SoftwareSourceCode struct {
|
||||
Context []string `json:"@context"`
|
||||
Type string `json:"@type"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
CodeRepository string `json:"codeRepository,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Author Person `json:"author"`
|
||||
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
|
||||
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
|
||||
}
|
||||
|
||||
// https://schema.org/ProgrammingLanguage
|
||||
type ProgrammingLanguage struct {
|
||||
Type string `json:"@type"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// https://schema.org/Person
|
||||
type Person struct {
|
||||
Type string `json:"@type,omitempty"`
|
||||
GivenName string `json:"givenName,omitempty"`
|
||||
MiddleName string `json:"middleName,omitempty"`
|
||||
FamilyName string `json:"familyName,omitempty"`
|
||||
}
|
||||
|
||||
func (p Person) String() string {
|
||||
var sb strings.Builder
|
||||
if p.GivenName != "" {
|
||||
sb.WriteString(p.GivenName)
|
||||
}
|
||||
if p.MiddleName != "" {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
sb.WriteString(p.MiddleName)
|
||||
}
|
||||
if p.FamilyName != "" {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
sb.WriteString(p.FamilyName)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ParsePackage parses the Swift package upload
|
||||
func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
|
||||
zr, err := zip.NewReader(sr, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Package{
|
||||
Metadata: &Metadata{
|
||||
Manifests: make(map[string]*Manifest),
|
||||
},
|
||||
}
|
||||
|
||||
for _, file := range zr.File {
|
||||
manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
|
||||
if len(manifestMatch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if file.UncompressedSize64 > maxManifestFileSize {
|
||||
return nil, ErrManifestFileTooLarge
|
||||
}
|
||||
|
||||
f, err := zr.Open(file.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(f)
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swiftVersion := ""
|
||||
if len(manifestMatch) == 2 && manifestMatch[1] != "" {
|
||||
v, err := version.NewSemver(manifestMatch[1])
|
||||
if err != nil {
|
||||
return nil, ErrInvalidManifestVersion
|
||||
}
|
||||
swiftVersion = TrimmedVersionString(v)
|
||||
}
|
||||
|
||||
manifest := &Manifest{
|
||||
Content: string(content),
|
||||
}
|
||||
|
||||
toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content)
|
||||
if len(toolsMatch) == 2 {
|
||||
v, err := version.NewSemver(toolsMatch[1])
|
||||
if err != nil {
|
||||
return nil, ErrInvalidManifestVersion
|
||||
}
|
||||
|
||||
manifest.ToolsVersion = TrimmedVersionString(v)
|
||||
}
|
||||
|
||||
p.Metadata.Manifests[swiftVersion] = manifest
|
||||
}
|
||||
|
||||
if _, found := p.Metadata.Manifests[""]; !found {
|
||||
return nil, ErrMissingManifestFile
|
||||
}
|
||||
|
||||
if mr != nil {
|
||||
var ssc *SoftwareSourceCode
|
||||
if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.Metadata.Description = ssc.Description
|
||||
p.Metadata.Keywords = ssc.Keywords
|
||||
p.Metadata.License = ssc.License
|
||||
p.Metadata.Author = Person{
|
||||
GivenName: ssc.Author.GivenName,
|
||||
MiddleName: ssc.Author.MiddleName,
|
||||
FamilyName: ssc.Author.FamilyName,
|
||||
}
|
||||
|
||||
p.Metadata.RepositoryURL = ssc.CodeRepository
|
||||
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
|
||||
p.Metadata.RepositoryURL = ""
|
||||
}
|
||||
|
||||
p.RepositoryURLs = ssc.RepositoryURLs
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// TrimmedVersionString returns the version string without the patch segment if it is zero
|
||||
func TrimmedVersionString(v *version.Version) string {
|
||||
segments := v.Segments64()
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%d.%d", segments[0], segments[1])
|
||||
if segments[2] != 0 {
|
||||
fmt.Fprintf(&b, ".%d", segments[2])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package swift
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
packageName = "gitea"
|
||||
packageVersion = "1.0.1"
|
||||
packageDescription = "Package Description"
|
||||
packageRepositoryURL = "https://gitea.io/gitea/gitea"
|
||||
packageAuthor = "KN4CK3R"
|
||||
packageLicense = "MIT"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
createArchive := func(files map[string][]byte) *bytes.Reader {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
for filename, content := range files {
|
||||
w, _ := zw.Create(filename)
|
||||
w.Write(content)
|
||||
}
|
||||
zw.Close()
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
t.Run("MissingManifestFile", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{"dummy.txt": {}})
|
||||
|
||||
p, err := ParsePackage(data, data.Size(), nil)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrMissingManifestFile)
|
||||
})
|
||||
|
||||
t.Run("ManifestFileTooLarge", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": make([]byte, maxManifestFileSize+1),
|
||||
})
|
||||
|
||||
p, err := ParsePackage(data, data.Size(), nil)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
|
||||
})
|
||||
|
||||
t.Run("WithoutMetadata", func(t *testing.T) {
|
||||
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
||||
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte(content1),
|
||||
"Package@swift-5.5.swift": []byte(content2),
|
||||
})
|
||||
|
||||
p, err := ParsePackage(data, data.Size(), nil)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, p.Metadata)
|
||||
assert.Empty(t, p.RepositoryURLs)
|
||||
assert.Len(t, p.Metadata.Manifests, 2)
|
||||
m := p.Metadata.Manifests[""]
|
||||
assert.Equal(t, "5.7", m.ToolsVersion)
|
||||
assert.Equal(t, content1, m.Content)
|
||||
m = p.Metadata.Manifests["5.5"]
|
||||
assert.Equal(t, "5.6", m.ToolsVersion)
|
||||
assert.Equal(t, content2, m.Content)
|
||||
})
|
||||
|
||||
t.Run("WithMetadata", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
||||
})
|
||||
|
||||
p, err := ParsePackage(
|
||||
data,
|
||||
data.Size(),
|
||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||
)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, p.Metadata)
|
||||
assert.Len(t, p.Metadata.Manifests, 1)
|
||||
m := p.Metadata.Manifests[""]
|
||||
assert.Equal(t, "5.7", m.ToolsVersion)
|
||||
|
||||
assert.Equal(t, packageDescription, p.Metadata.Description)
|
||||
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
|
||||
assert.Equal(t, packageLicense, p.Metadata.License)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
|
||||
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
|
||||
assert.ElementsMatch(t, []string{packageRepositoryURL}, p.RepositoryURLs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTrimmedVersionString(t *testing.T) {
|
||||
cases := []struct {
|
||||
Version *version.Version
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1")),
|
||||
Expected: "1.0",
|
||||
},
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1.0")),
|
||||
Expected: "1.0",
|
||||
},
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1.0.0")),
|
||||
Expected: "1.0",
|
||||
},
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1.0.1")),
|
||||
Expected: "1.0.1",
|
||||
},
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1.0+meta")),
|
||||
Expected: "1.0",
|
||||
},
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1.0.0+meta")),
|
||||
Expected: "1.0",
|
||||
},
|
||||
{
|
||||
Version: version.Must(version.NewVersion("1.0.1+meta")),
|
||||
Expected: "1.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.Expected, TrimmedVersionString(c.Version))
|
||||
}
|
||||
}
|
||||
@@ -45,29 +45,19 @@ func AssetsHandlerFunc(opts *Options) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
file := req.URL.Path
|
||||
file = file[len(opts.Prefix):]
|
||||
if len(file) == 0 {
|
||||
resp.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if strings.Contains(file, "\\") {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
file = "/" + file
|
||||
|
||||
var written bool
|
||||
var corsSent bool
|
||||
if opts.CorsHandler != nil {
|
||||
written = true
|
||||
opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
written = false
|
||||
corsSent = true
|
||||
})).ServeHTTP(resp, req)
|
||||
}
|
||||
if written {
|
||||
// If CORS is not sent, the response must have been written by other handlers
|
||||
if !corsSent {
|
||||
return
|
||||
}
|
||||
|
||||
file := req.URL.Path[len(opts.Prefix):]
|
||||
|
||||
// custom files
|
||||
if opts.handle(resp, req, http.Dir(custPath), file) {
|
||||
return
|
||||
@@ -102,8 +92,8 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
|
||||
}
|
||||
|
||||
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
|
||||
// use clean to keep the file is a valid path with no . or ..
|
||||
f, err := fs.Open(util.CleanPath(file))
|
||||
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
|
||||
f, err := fs.Open(util.PathJoinRelX(file))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
|
||||
@@ -26,7 +26,7 @@ func loadAttachmentFrom(rootCfg ConfigProvider) {
|
||||
|
||||
Attachment.Storage = getStorage(rootCfg, "attachments", storageType, sec)
|
||||
|
||||
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
|
||||
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
|
||||
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(4)
|
||||
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
|
||||
Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
|
||||
|
||||
@@ -54,7 +54,11 @@ var (
|
||||
|
||||
// LoadDBSetting loads the database settings
|
||||
func LoadDBSetting() {
|
||||
sec := CfgProvider.Section("database")
|
||||
loadDBSetting(CfgProvider)
|
||||
}
|
||||
|
||||
func loadDBSetting(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("database")
|
||||
Database.Type = DatabaseType(sec.Key("DB_TYPE").String())
|
||||
defaultCharset := "utf8"
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ var (
|
||||
LimitSizePub int64
|
||||
LimitSizePyPI int64
|
||||
LimitSizeRubyGems int64
|
||||
LimitSizeSwift int64
|
||||
LimitSizeVagrant int64
|
||||
}{
|
||||
Enabled: true,
|
||||
@@ -81,6 +82,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
|
||||
Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
|
||||
Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
|
||||
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
|
||||
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
|
||||
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
|
||||
}
|
||||
|
||||
|
||||
+13
-36
@@ -12,7 +12,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -42,15 +41,13 @@ var (
|
||||
AppWorkPath string
|
||||
|
||||
// Global setting objects
|
||||
CfgProvider ConfigProvider
|
||||
CustomPath string // Custom directory path
|
||||
CustomConf string
|
||||
PIDFile = "/run/gitea.pid"
|
||||
WritePIDFile bool
|
||||
RunMode string
|
||||
RunUser string
|
||||
IsProd bool
|
||||
IsWindows bool
|
||||
CfgProvider ConfigProvider
|
||||
CustomPath string // Custom directory path
|
||||
CustomConf string
|
||||
RunMode string
|
||||
RunUser string
|
||||
IsProd bool
|
||||
IsWindows bool
|
||||
)
|
||||
|
||||
func getAppPath() (string, error) {
|
||||
@@ -141,22 +138,6 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
|
||||
return currentUser, runUser == currentUser
|
||||
}
|
||||
|
||||
func createPIDFile(pidPath string) {
|
||||
currentPid := os.Getpid()
|
||||
if err := os.MkdirAll(filepath.Dir(pidPath), os.ModePerm); err != nil {
|
||||
log.Fatal("Failed to create PID folder: %v", err)
|
||||
}
|
||||
|
||||
file, err := os.Create(pidPath)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create PID file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := file.WriteString(strconv.FormatInt(int64(currentPid), 10)); err != nil {
|
||||
log.Fatal("Failed to write PID information: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetCustomPathAndConf will set CustomPath and CustomConf with reference to the
|
||||
// GITEA_CUSTOM environment variable and with provided overrides before stepping
|
||||
// back to the default
|
||||
@@ -218,17 +199,17 @@ func PrepareAppDataPath() error {
|
||||
|
||||
// InitProviderFromExistingFile initializes config provider from an existing config file (app.ini)
|
||||
func InitProviderFromExistingFile() {
|
||||
CfgProvider = newFileProviderFromConf(CustomConf, WritePIDFile, false, PIDFile, "")
|
||||
CfgProvider = newFileProviderFromConf(CustomConf, false, "")
|
||||
}
|
||||
|
||||
// InitProviderAllowEmpty initializes config provider from file, it's also fine that if the config file (app.ini) doesn't exist
|
||||
func InitProviderAllowEmpty() {
|
||||
CfgProvider = newFileProviderFromConf(CustomConf, WritePIDFile, true, PIDFile, "")
|
||||
CfgProvider = newFileProviderFromConf(CustomConf, true, "")
|
||||
}
|
||||
|
||||
// InitProviderAndLoadCommonSettingsForTest initializes config provider and load common setttings for tests
|
||||
func InitProviderAndLoadCommonSettingsForTest(extraConfigs ...string) {
|
||||
CfgProvider = newFileProviderFromConf(CustomConf, WritePIDFile, true, PIDFile, strings.Join(extraConfigs, "\n"))
|
||||
CfgProvider = newFileProviderFromConf(CustomConf, true, strings.Join(extraConfigs, "\n"))
|
||||
loadCommonSettingsFrom(CfgProvider)
|
||||
if err := PrepareAppDataPath(); err != nil {
|
||||
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
|
||||
@@ -241,13 +222,9 @@ func InitProviderAndLoadCommonSettingsForTest(extraConfigs ...string) {
|
||||
|
||||
// newFileProviderFromConf initializes configuration context.
|
||||
// NOTE: do not print any log except error.
|
||||
func newFileProviderFromConf(customConf string, writePIDFile, allowEmpty bool, pidFile, extraConfig string) *ini.File {
|
||||
func newFileProviderFromConf(customConf string, allowEmpty bool, extraConfig string) *ini.File {
|
||||
cfg := ini.Empty()
|
||||
|
||||
if writePIDFile && len(pidFile) > 0 {
|
||||
createPIDFile(pidFile)
|
||||
}
|
||||
|
||||
isFile, err := util.IsFile(customConf)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s is a file. Error: %v", customConf, err)
|
||||
@@ -380,7 +357,7 @@ func CreateOrAppendToCustomConf(purpose string, callback func(cfg *ini.File)) {
|
||||
|
||||
// LoadSettings initializes the settings for normal start up
|
||||
func LoadSettings() {
|
||||
LoadDBSetting()
|
||||
loadDBSetting(CfgProvider)
|
||||
loadServiceFrom(CfgProvider)
|
||||
loadOAuth2ClientFrom(CfgProvider)
|
||||
InitLogs(false)
|
||||
@@ -401,7 +378,7 @@ func LoadSettings() {
|
||||
|
||||
// LoadSettingsForInstall initializes the settings for install
|
||||
func LoadSettingsForInstall() {
|
||||
LoadDBSetting()
|
||||
loadDBSetting(CfgProvider)
|
||||
loadServiceFrom(CfgProvider)
|
||||
loadMailerFrom(CfgProvider)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,6 @@ func (s discardStorage) URL(_, _ string) (*url.URL, error) {
|
||||
return nil, fmt.Errorf("%s", s)
|
||||
}
|
||||
|
||||
func (s discardStorage) IterateObjects(_ func(string, Object) error) error {
|
||||
func (s discardStorage) IterateObjects(_ string, _ func(string, Object) error) error {
|
||||
return fmt.Errorf("%s", s)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func Test_discardStorage(t *testing.T) {
|
||||
assert.Errorf(t, err, string(tt))
|
||||
}
|
||||
{
|
||||
err := tt.IterateObjects(func(_ string, _ Object) error { return nil })
|
||||
err := tt.IterateObjects("", func(_ string, _ Object) error { return nil })
|
||||
assert.Error(t, err, string(tt))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,11 +5,11 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -41,13 +41,19 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||
}
|
||||
config := configInterface.(LocalStorageConfig)
|
||||
|
||||
if !filepath.IsAbs(config.Path) {
|
||||
return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path)
|
||||
}
|
||||
log.Info("Creating new Local Storage at %s", config.Path)
|
||||
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.TemporaryPath == "" {
|
||||
config.TemporaryPath = config.Path + "/tmp"
|
||||
config.TemporaryPath = filepath.Join(config.Path, "tmp")
|
||||
}
|
||||
if !filepath.IsAbs(config.TemporaryPath) {
|
||||
return nil, fmt.Errorf("LocalStorageConfig.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath)
|
||||
}
|
||||
|
||||
return &LocalStorage{
|
||||
@@ -58,7 +64,7 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||
}
|
||||
|
||||
func (l *LocalStorage) buildLocalPath(p string) string {
|
||||
return filepath.Join(l.dir, util.CleanPath(strings.ReplaceAll(p, "\\", "/")))
|
||||
return util.FilePathJoinAbs(l.dir, p)
|
||||
}
|
||||
|
||||
// Open a file
|
||||
@@ -127,8 +133,9 @@ func (l *LocalStorage) URL(path, name string) (*url.URL, error) {
|
||||
}
|
||||
|
||||
// IterateObjects iterates across the objects in the local storage
|
||||
func (l *LocalStorage) IterateObjects(fn func(path string, obj Object) error) error {
|
||||
return filepath.WalkDir(l.dir, func(path string, d os.DirEntry, err error) error {
|
||||
func (l *LocalStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
|
||||
dir := l.buildLocalPath(prefix)
|
||||
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,29 +20,29 @@ func TestBuildLocalPath(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"a",
|
||||
"/a",
|
||||
"0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
},
|
||||
{
|
||||
"a",
|
||||
"/a",
|
||||
"../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
},
|
||||
{
|
||||
"a",
|
||||
"/a",
|
||||
"0\\a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"/a/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
},
|
||||
{
|
||||
"b",
|
||||
"/b",
|
||||
"a/../0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"/b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
},
|
||||
{
|
||||
"b",
|
||||
"/b",
|
||||
"a\\..\\0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
"/b/0/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,3 +54,41 @@ func TestBuildLocalPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageIterator(t *testing.T) {
|
||||
dir := filepath.Join(os.TempDir(), "TestLocalStorageIteratorTestDir")
|
||||
l, err := NewLocalStorage(context.Background(), LocalStorageConfig{Path: dir})
|
||||
assert.NoError(t, err)
|
||||
|
||||
testFiles := [][]string{
|
||||
{"a/1.txt", "a1"},
|
||||
{"/a/1.txt", "aa1"}, // same as above, but with leading slash that will be trim
|
||||
{"b/1.txt", "b1"},
|
||||
{"b/2.txt", "b2"},
|
||||
{"b/3.txt", "b3"},
|
||||
{"b/x 4.txt", "bx4"},
|
||||
}
|
||||
for _, f := range testFiles {
|
||||
_, err = l.Save(f[0], bytes.NewBufferString(f[1]), -1)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
expectedList := map[string][]string{
|
||||
"a": {"a/1.txt"},
|
||||
"b": {"b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
|
||||
"": {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
|
||||
"/": {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
|
||||
"a/b/../../a": {"a/1.txt"},
|
||||
}
|
||||
for dir, expected := range expectedList {
|
||||
count := 0
|
||||
err = l.IterateObjects(dir, func(path string, f Object) error {
|
||||
defer f.Close()
|
||||
assert.Contains(t, expected, path)
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, count, len(expected))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
|
||||
}
|
||||
|
||||
func (m *MinioStorage) buildMinioPath(p string) string {
|
||||
return strings.TrimPrefix(path.Join(m.basePath, util.CleanPath(strings.ReplaceAll(p, "\\", "/"))), "/")
|
||||
return util.PathJoinRelX(m.basePath, p)
|
||||
}
|
||||
|
||||
// Open open a file
|
||||
@@ -209,12 +209,18 @@ func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
|
||||
}
|
||||
|
||||
// IterateObjects iterates across the objects in the miniostorage
|
||||
func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) error {
|
||||
func (m *MinioStorage) IterateObjects(prefix string, fn func(path string, obj Object) error) error {
|
||||
opts := minio.GetObjectOptions{}
|
||||
lobjectCtx, cancel := context.WithCancel(m.ctx)
|
||||
defer cancel()
|
||||
|
||||
basePath := m.basePath
|
||||
if prefix != "" {
|
||||
basePath = m.buildMinioPath(prefix)
|
||||
}
|
||||
|
||||
for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
|
||||
Prefix: m.basePath,
|
||||
Prefix: basePath,
|
||||
Recursive: true,
|
||||
}) {
|
||||
object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
|
||||
@@ -223,7 +229,7 @@ func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) er
|
||||
}
|
||||
if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
|
||||
defer object.Close()
|
||||
return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
|
||||
return fn(strings.TrimPrefix(mObjInfo.Key, basePath), &minioObject{object})
|
||||
}(object, fn); err != nil {
|
||||
return convertMinioErr(err)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ type ObjectStorage interface {
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
Delete(path string) error
|
||||
URL(path, name string) (*url.URL, error)
|
||||
IterateObjects(func(path string, obj Object) error) error
|
||||
IterateObjects(path string, iterator func(path string, obj Object) error) error
|
||||
}
|
||||
|
||||
// Copy copies a file from source ObjectStorage to dest ObjectStorage
|
||||
@@ -87,7 +87,7 @@ func Copy(dstStorage ObjectStorage, dstPath string, srcStorage ObjectStorage, sr
|
||||
|
||||
// Clean delete all the objects in this storage
|
||||
func Clean(storage ObjectStorage) error {
|
||||
return storage.IterateObjects(func(path string, obj Object) error {
|
||||
return storage.IterateObjects("", func(path string, obj Object) error {
|
||||
_ = obj.Close()
|
||||
return storage.Delete(path)
|
||||
})
|
||||
|
||||
@@ -88,6 +88,9 @@ type Repository struct {
|
||||
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
|
||||
HasPullRequests bool `json:"has_pull_requests"`
|
||||
HasProjects bool `json:"has_projects"`
|
||||
HasReleases bool `json:"has_releases"`
|
||||
HasPackages bool `json:"has_packages"`
|
||||
HasActions bool `json:"has_actions"`
|
||||
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
|
||||
AllowMerge bool `json:"allow_merge_commits"`
|
||||
AllowRebase bool `json:"allow_rebase"`
|
||||
@@ -170,6 +173,12 @@ type EditRepoOption struct {
|
||||
HasPullRequests *bool `json:"has_pull_requests,omitempty"`
|
||||
// either `true` to enable project unit, or `false` to disable them.
|
||||
HasProjects *bool `json:"has_projects,omitempty"`
|
||||
// either `true` to enable releases unit, or `false` to disable them.
|
||||
HasReleases *bool `json:"has_releases,omitempty"`
|
||||
// either `true` to enable packages unit, or `false` to disable them.
|
||||
HasPackages *bool `json:"has_packages,omitempty"`
|
||||
// either `true` to enable actions unit, or `false` to disable them.
|
||||
HasActions *bool `json:"has_actions,omitempty"`
|
||||
// either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace.
|
||||
IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace_conflicts,omitempty"`
|
||||
// either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.
|
||||
|
||||
@@ -93,3 +93,12 @@ type UserSettingsOptions struct {
|
||||
HideEmail *bool `json:"hide_email"`
|
||||
HideActivity *bool `json:"hide_activity"`
|
||||
}
|
||||
|
||||
// RenameUserOption options when renaming a user
|
||||
type RenameUserOption struct {
|
||||
// New username for this user. This name cannot be in use yet by any other user.
|
||||
//
|
||||
// required: true
|
||||
// unique: true
|
||||
NewName string `json:"new_username" binding:"Required"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
@@ -9,6 +10,8 @@ type Email struct {
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"verified"`
|
||||
Primary bool `json:"primary"`
|
||||
UserID int64 `json:"user_id"`
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
// CreateEmailOption options when creating email addresses
|
||||
|
||||
@@ -25,6 +25,12 @@ const defaultSize = 16
|
||||
// Init discovers SVGs and populates the `SVGs` variable
|
||||
func Init() {
|
||||
SVGs = Discover()
|
||||
|
||||
// Remove `xmlns` because inline SVG does not need it
|
||||
r := regexp.MustCompile(`(<svg\b[^>]*?)\s+xmlns="[^"]*"`)
|
||||
for name, svg := range SVGs {
|
||||
SVGs[name] = r.ReplaceAllString(svg, "$1")
|
||||
}
|
||||
}
|
||||
|
||||
// Render render icons - arguments icon name (string), size (int), class (string)
|
||||
|
||||
@@ -109,6 +109,9 @@ func NewFuncMap() []template.FuncMap {
|
||||
"CustomEmojis": func() map[string]string {
|
||||
return setting.UI.CustomEmojisMap
|
||||
},
|
||||
"IsShowFullName": func() bool {
|
||||
return setting.UI.DefaultShowFullName
|
||||
},
|
||||
"Safe": Safe,
|
||||
"SafeJS": SafeJS,
|
||||
"JSEscape": JSEscape,
|
||||
|
||||
@@ -118,7 +118,7 @@ func handleGenericTemplateError(err error) (string, []interface{}) {
|
||||
|
||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||
|
||||
line := getLineFromAsset(templateName, lineNumber, "")
|
||||
line := GetLineFromTemplate(templateName, lineNumber, "", -1)
|
||||
|
||||
return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func handleNotDefinedPanicError(err error) (string, []interface{}) {
|
||||
|
||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||
|
||||
line := getLineFromAsset(templateName, lineNumber, functionName)
|
||||
line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
|
||||
|
||||
return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func handleUnexpected(err error) (string, []interface{}) {
|
||||
|
||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||
|
||||
line := getLineFromAsset(templateName, lineNumber, unexpected)
|
||||
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
|
||||
|
||||
return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
||||
}
|
||||
@@ -181,14 +181,15 @@ func handleExpectedEnd(err error) (string, []interface{}) {
|
||||
|
||||
lineNumber, _ := strconv.Atoi(lineNumberStr)
|
||||
|
||||
line := getLineFromAsset(templateName, lineNumber, unexpected)
|
||||
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
|
||||
|
||||
return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
|
||||
}
|
||||
|
||||
const dashSeparator = "----------------------------------------------------------------------\n"
|
||||
|
||||
func getLineFromAsset(templateName string, targetLineNum int, target string) string {
|
||||
// GetLineFromTemplate returns a line from a template with some context
|
||||
func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
|
||||
bs, err := GetAsset("templates/" + templateName + ".tmpl")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("(unable to read template file: %v)", err)
|
||||
@@ -229,24 +230,30 @@ func getLineFromAsset(templateName string, targetLineNum int, target string) str
|
||||
// If there is a provided target to look for in the line add a pointer to it
|
||||
// e.g. ^^^^^^^
|
||||
if target != "" {
|
||||
idx := bytes.Index(lineBs, []byte(target))
|
||||
|
||||
if idx >= 0 {
|
||||
// take the current line and replace preceding text with whitespace (except for tab)
|
||||
for i := range lineBs[:idx] {
|
||||
if lineBs[i] != '\t' {
|
||||
lineBs[i] = ' '
|
||||
}
|
||||
}
|
||||
|
||||
// write the preceding "space"
|
||||
_, _ = sb.Write(lineBs[:idx])
|
||||
|
||||
// Now write the ^^ pointer
|
||||
_, _ = sb.WriteString(strings.Repeat("^", len(target)))
|
||||
_ = sb.WriteByte('\n')
|
||||
targetPos := bytes.Index(lineBs, []byte(target))
|
||||
if targetPos >= 0 {
|
||||
position = targetPos
|
||||
}
|
||||
}
|
||||
if position >= 0 {
|
||||
// take the current line and replace preceding text with whitespace (except for tab)
|
||||
for i := range lineBs[:position] {
|
||||
if lineBs[i] != '\t' {
|
||||
lineBs[i] = ' '
|
||||
}
|
||||
}
|
||||
|
||||
// write the preceding "space"
|
||||
_, _ = sb.Write(lineBs[:position])
|
||||
|
||||
// Now write the ^^ pointer
|
||||
targetLen := len(target)
|
||||
if targetLen == 0 {
|
||||
targetLen = 1
|
||||
}
|
||||
_, _ = sb.WriteString(strings.Repeat("^", targetLen))
|
||||
_ = sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
// Finally write the footer
|
||||
sb.WriteString(dashSeparator)
|
||||
|
||||
@@ -233,7 +233,7 @@ func TimeSince(then time.Time, lang translation.Locale) template.HTML {
|
||||
}
|
||||
|
||||
func htmlTimeSince(then, now time.Time, lang translation.Locale) template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<span class="time-since tooltip" data-content="%s">%s</span>`,
|
||||
return template.HTML(fmt.Sprintf(`<span class="time-since tooltip" data-content="%s" data-tooltip-interactive="true">%s</span>`,
|
||||
then.In(setting.DefaultUILocation).Format(GetTimeFormat(lang.Language())),
|
||||
timeSince(then, now, lang)))
|
||||
}
|
||||
@@ -244,7 +244,7 @@ func TimeSinceUnix(then TimeStamp, lang translation.Locale) template.HTML {
|
||||
}
|
||||
|
||||
func htmlTimeSinceUnix(then, now TimeStamp, lang translation.Locale) template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<span class="time-since tooltip" data-content="%s">%s</span>`,
|
||||
return template.HTML(fmt.Sprintf(`<span class="time-since tooltip" data-content="%s" data-tooltip-interactive="true">%s</span>`,
|
||||
then.FormatInLocation(GetTimeFormat(lang.Language()), setting.DefaultUILocation),
|
||||
timeSinceUnix(int64(then), int64(now), lang)))
|
||||
}
|
||||
|
||||
+83
-11
@@ -5,6 +5,7 @@ package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -14,21 +15,92 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CleanPath ensure to clean the path
|
||||
func CleanPath(p string) string {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
return path.Clean(p)
|
||||
// PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
|
||||
// It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
|
||||
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
|
||||
//
|
||||
// empty => ``
|
||||
// `` => ``
|
||||
// `..` => `.`
|
||||
// `dir` => `dir`
|
||||
// `/dir/` => `dir`
|
||||
// `foo\..\bar` => `foo\..\bar`
|
||||
// {`foo`, ``, `bar`} => `foo/bar`
|
||||
// {`foo`, `..`, `bar`} => `foo/bar`
|
||||
func PathJoinRel(elem ...string) string {
|
||||
elems := make([]string, len(elem))
|
||||
for i, e := range elem {
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
elems[i] = path.Clean("/" + e)
|
||||
}
|
||||
p := path.Join(elems...)
|
||||
if p == "" {
|
||||
return ""
|
||||
} else if p == "/" {
|
||||
return "."
|
||||
} else {
|
||||
return p[1:]
|
||||
}
|
||||
return path.Clean("/" + p)[1:]
|
||||
}
|
||||
|
||||
// EnsureAbsolutePath ensure that a path is absolute, making it
|
||||
// relative to absoluteBase if necessary
|
||||
func EnsureAbsolutePath(path, absoluteBase string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
// PathJoinRelX joins the path elements into a single path like PathJoinRel,
|
||||
// and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
|
||||
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
|
||||
// It returns similar results as PathJoinRel except:
|
||||
//
|
||||
// `foo\..\bar` => `bar` (because it's processed as `foo/../bar`)
|
||||
//
|
||||
// All backslashes are handled as slashes, the result only contains slashes.
|
||||
func PathJoinRelX(elem ...string) string {
|
||||
elems := make([]string, len(elem))
|
||||
for i, e := range elem {
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
|
||||
}
|
||||
return filepath.Join(absoluteBase, path)
|
||||
return PathJoinRel(elems...)
|
||||
}
|
||||
|
||||
const pathSeparator = string(os.PathSeparator)
|
||||
|
||||
// FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
|
||||
// All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
|
||||
// The first element must be an absolute path, caller should prepare the base path.
|
||||
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
|
||||
// Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
|
||||
//
|
||||
// {`/foo`, ``, `bar`} => `/foo/bar`
|
||||
// {`/foo`, `..`, `bar`} => `/foo/bar`
|
||||
func FilePathJoinAbs(elem ...string) string {
|
||||
elems := make([]string, len(elem))
|
||||
|
||||
// POISX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
|
||||
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
|
||||
if isOSWindows() {
|
||||
elems[0] = filepath.Clean(elem[0])
|
||||
} else {
|
||||
elems[0] = filepath.Clean(strings.ReplaceAll(elem[0], "\\", pathSeparator))
|
||||
}
|
||||
if !filepath.IsAbs(elems[0]) {
|
||||
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
|
||||
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
|
||||
}
|
||||
|
||||
for i := 1; i < len(elem); i++ {
|
||||
if elem[i] == "" {
|
||||
continue
|
||||
}
|
||||
if isOSWindows() {
|
||||
elems[i] = filepath.Clean(pathSeparator + elem[i])
|
||||
} else {
|
||||
elems[i] = filepath.Clean(pathSeparator + strings.ReplaceAll(elem[i], "\\", pathSeparator))
|
||||
}
|
||||
}
|
||||
// the elems[0] must be an absolute path, just join them together
|
||||
return filepath.Join(elems...)
|
||||
}
|
||||
|
||||
// IsDir returns true if given path is a directory,
|
||||
|
||||
@@ -138,13 +138,75 @@ func TestMisc_IsReadmeFileName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCleanPath(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"../../test": "test",
|
||||
"/test": "/test",
|
||||
"/../test": "/test",
|
||||
cases := []struct {
|
||||
elems []string
|
||||
expected string
|
||||
}{
|
||||
{[]string{}, ``},
|
||||
{[]string{``}, ``},
|
||||
{[]string{`..`}, `.`},
|
||||
{[]string{`a`}, `a`},
|
||||
{[]string{`/a/`}, `a`},
|
||||
{[]string{`../a/`, `../b`, `c/..`, `d`}, `a/b/d`},
|
||||
{[]string{`a\..\b`}, `a\..\b`},
|
||||
{[]string{`a`, ``, `b`}, `a/b`},
|
||||
{[]string{`a`, `..`, `b`}, `a/b`},
|
||||
{[]string{`lfs`, `repo/..`, `user/../path`}, `lfs/path`},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, PathJoinRel(c.elems...), "case: %v", c.elems)
|
||||
}
|
||||
|
||||
for k, v := range cases {
|
||||
assert.Equal(t, v, CleanPath(k))
|
||||
cases = []struct {
|
||||
elems []string
|
||||
expected string
|
||||
}{
|
||||
{[]string{}, ``},
|
||||
{[]string{``}, ``},
|
||||
{[]string{`..`}, `.`},
|
||||
{[]string{`a`}, `a`},
|
||||
{[]string{`/a/`}, `a`},
|
||||
{[]string{`../a/`, `../b`, `c/..`, `d`}, `a/b/d`},
|
||||
{[]string{`a\..\b`}, `b`},
|
||||
{[]string{`a`, ``, `b`}, `a/b`},
|
||||
{[]string{`a`, `..`, `b`}, `a/b`},
|
||||
{[]string{`lfs`, `repo/..`, `user/../path`}, `lfs/path`},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, PathJoinRelX(c.elems...), "case: %v", c.elems)
|
||||
}
|
||||
|
||||
// for POSIX only, but the result is similar on Windows, because the first element must be an absolute path
|
||||
if isOSWindows() {
|
||||
cases = []struct {
|
||||
elems []string
|
||||
expected string
|
||||
}{
|
||||
{[]string{`C:\..`}, `C:\`},
|
||||
{[]string{`C:\a`}, `C:\a`},
|
||||
{[]string{`C:\a/`}, `C:\a`},
|
||||
{[]string{`C:\..\a\`, `../b`, `c\..`, `d`}, `C:\a\b\d`},
|
||||
{[]string{`C:\a/..\b`}, `C:\b`},
|
||||
{[]string{`C:\a`, ``, `b`}, `C:\a\b`},
|
||||
{[]string{`C:\a`, `..`, `b`}, `C:\a\b`},
|
||||
{[]string{`C:\lfs`, `repo/..`, `user/../path`}, `C:\lfs\path`},
|
||||
}
|
||||
} else {
|
||||
cases = []struct {
|
||||
elems []string
|
||||
expected string
|
||||
}{
|
||||
{[]string{`/..`}, `/`},
|
||||
{[]string{`/a`}, `/a`},
|
||||
{[]string{`/a/`}, `/a`},
|
||||
{[]string{`/../a/`, `../b`, `c/..`, `d`}, `/a/b/d`},
|
||||
{[]string{`/a\..\b`}, `/b`},
|
||||
{[]string{`/a`, ``, `b`}, `/a/b`},
|
||||
{[]string{`/a`, `..`, `b`}, `/a/b`},
|
||||
{[]string{`/lfs`, `repo/..`, `user/../path`}, `/lfs/path`},
|
||||
}
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems...), "case: %v", c.elems)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user