mirror of
https://github.com/go-gitea/gitea
synced 2025-02-06 06:07:01 +01:00
Merge branch 'main' into issue-updates
This commit is contained in:
commit
b5f61df0a9
4
Makefile
4
Makefile
@ -164,8 +164,8 @@ ifdef DEPS_PLAYWRIGHT
|
||||
endif
|
||||
|
||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g
|
||||
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g
|
||||
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g
|
||||
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
|
||||
SWAGGER_EXCLUDE := code.gitea.io/sdk
|
||||
SWAGGER_NEWLINE_COMMAND := -e '$$a\'
|
||||
|
||||
|
@ -95,12 +95,6 @@ Gitea Actions目前不支持此功能,如果使用它,结果将始终为空
|
||||
|
||||
## 缺失的功能
|
||||
|
||||
### 变量
|
||||
|
||||
请参阅[变量](https://docs.github.com/zh/actions/learn-github-actions/variables)。
|
||||
|
||||
目前变量功能正在开发中。
|
||||
|
||||
### 问题匹配器
|
||||
|
||||
问题匹配器是一种扫描Actions输出以查找指定正则表达式模式并在用户界面中突出显示该信息的方法。
|
||||
|
@ -26,6 +26,8 @@ const (
|
||||
ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
|
||||
ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored
|
||||
ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired
|
||||
ArtifactStatusPendingDeletion // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion
|
||||
ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -147,8 +149,28 @@ func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
|
||||
Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
|
||||
}
|
||||
|
||||
// ListPendingDeleteArtifacts returns all artifacts in pending-delete status.
|
||||
// limit is the max number of artifacts to return.
|
||||
func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) {
|
||||
arts := make([]*ActionArtifact, 0, limit)
|
||||
return arts, db.GetEngine(ctx).
|
||||
Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts)
|
||||
}
|
||||
|
||||
// SetArtifactExpired sets an artifact to expired
|
||||
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
|
||||
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
|
||||
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetArtifactDeleted sets an artifact to deleted
|
||||
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)})
|
||||
return err
|
||||
}
|
||||
|
@ -292,8 +292,14 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
|
||||
|
||||
// CreateReview creates a new review based on opts
|
||||
func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
review := &Review{
|
||||
Type: opts.Type,
|
||||
Issue: opts.Issue,
|
||||
IssueID: opts.Issue.ID,
|
||||
Reviewer: opts.Reviewer,
|
||||
@ -303,15 +309,39 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
|
||||
CommitID: opts.CommitID,
|
||||
Stale: opts.Stale,
|
||||
}
|
||||
|
||||
if opts.Reviewer != nil {
|
||||
review.Type = opts.Type
|
||||
review.ReviewerID = opts.Reviewer.ID
|
||||
} else {
|
||||
if review.Type != ReviewTypeRequest {
|
||||
review.Type = ReviewTypeRequest
|
||||
|
||||
reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID}
|
||||
// make sure user review requests are cleared
|
||||
if opts.Type != ReviewTypePending {
|
||||
if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// make sure if the created review gets dismissed no old review surface
|
||||
// other types can be ignored, as they don't affect branch protection
|
||||
if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject {
|
||||
if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))).
|
||||
Cols("dismissed").Update(&Review{Dismissed: true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
} else if opts.ReviewerTeam != nil {
|
||||
review.Type = ReviewTypeRequest
|
||||
review.ReviewerTeamID = opts.ReviewerTeam.ID
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("provide either reviewer or reviewer team")
|
||||
}
|
||||
return review, db.Insert(ctx, review)
|
||||
|
||||
if _, err := sess.Insert(review); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return review, committer.Commit()
|
||||
}
|
||||
|
||||
// GetCurrentReview returns the current pending review of reviewer for given issue
|
||||
|
@ -131,8 +131,8 @@ func AssertSuccessfulInsert(t assert.TestingT, beans ...any) {
|
||||
}
|
||||
|
||||
// AssertCount assert the count of a bean
|
||||
func AssertCount(t assert.TestingT, bean, expected any) {
|
||||
assert.EqualValues(t, expected, GetCount(t, bean))
|
||||
func AssertCount(t assert.TestingT, bean, expected any) bool {
|
||||
return assert.EqualValues(t, expected, GetCount(t, bean))
|
||||
}
|
||||
|
||||
// AssertInt64InRange assert value is in range [low, high]
|
||||
@ -150,7 +150,7 @@ func GetCountByCond(t assert.TestingT, tableName string, cond builder.Cond) int6
|
||||
}
|
||||
|
||||
// AssertCountByCond test the count of database entries matching bean
|
||||
func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) {
|
||||
assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
|
||||
func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) bool {
|
||||
return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
|
||||
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
|
||||
}
|
||||
|
@ -52,7 +52,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
||||
case webhook_module.HookEventPullRequest,
|
||||
webhook_module.HookEventPullRequestSync,
|
||||
webhook_module.HookEventPullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel:
|
||||
webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestReviewRequest,
|
||||
webhook_module.HookEventPullRequestMilestone:
|
||||
return true
|
||||
|
||||
default:
|
||||
|
@ -221,7 +221,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
||||
webhook_module.HookEventPullRequest,
|
||||
webhook_module.HookEventPullRequestSync,
|
||||
webhook_module.HookEventPullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel:
|
||||
webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestReviewRequest,
|
||||
webhook_module.HookEventPullRequestMilestone:
|
||||
return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt)
|
||||
|
||||
case // pull_request_review
|
||||
@ -397,13 +399,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
} else {
|
||||
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
// Actions with the same name:
|
||||
// opened, edited, closed, reopened, assigned, unassigned
|
||||
// opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned
|
||||
// Actions need to be converted:
|
||||
// synchronized -> synchronize
|
||||
// label_updated -> labeled
|
||||
// label_cleared -> unlabeled
|
||||
// Unsupported activity types:
|
||||
// converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled
|
||||
// converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued
|
||||
|
||||
action := prPayload.Action
|
||||
switch action {
|
||||
|
@ -115,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string {
|
||||
|
||||
// create sha1 encode string
|
||||
sh := sha1.New()
|
||||
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes)))
|
||||
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
|
||||
encoded := hex.EncodeToString(sh.Sum(nil))
|
||||
|
||||
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
|
||||
|
@ -6,6 +6,7 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
@ -124,7 +125,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
||||
func Contexter() func(next http.Handler) http.Handler {
|
||||
rnd := templates.HTMLRenderer()
|
||||
csrfOpts := CsrfOptions{
|
||||
Secret: setting.SecretKey,
|
||||
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
|
||||
Cookie: setting.CSRFCookieName,
|
||||
SetCookie: true,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
|
@ -90,6 +90,20 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||
}
|
||||
}
|
||||
|
||||
// JSONTemplate renders the template as JSON response
|
||||
// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
|
||||
func (ctx *Context) JSONTemplate(tmpl base.TplName) {
|
||||
t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
|
||||
if err != nil {
|
||||
ctx.ServerError("unable to find template", err)
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
|
||||
ctx.ServerError("unable to execute template", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderToString renders the template content to a string
|
||||
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
|
||||
var buf strings.Builder
|
||||
|
@ -5,10 +5,7 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
var _ context.Context = TemplateContext(nil)
|
||||
@ -36,14 +33,3 @@ func (c TemplateContext) Err() error {
|
||||
func (c TemplateContext) Value(key any) any {
|
||||
return c.parentContext().Value(key)
|
||||
}
|
||||
|
||||
// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context
|
||||
// as the current template's rendering context (request context), to help to find data race issues as early as possible.
|
||||
// When the code is proven to be correct and stable, this function should be removed.
|
||||
func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) {
|
||||
if c.parentContext() != dataCtx {
|
||||
log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2))
|
||||
return "", errors.New("parent context mismatch")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
@ -12,12 +12,11 @@ import (
|
||||
|
||||
// LFS represents the configuration for Git LFS
|
||||
var LFS = struct {
|
||||
StartServer bool `ini:"LFS_START_SERVER"`
|
||||
JWTSecretBase64 string `ini:"LFS_JWT_SECRET"`
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
|
||||
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
|
||||
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
|
||||
StartServer bool `ini:"LFS_START_SERVER"`
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
|
||||
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
|
||||
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
|
||||
|
||||
Storage *Storage
|
||||
}{}
|
||||
@ -59,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(LFS.JWTSecretBase64)
|
||||
jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
|
||||
}
|
||||
@ -72,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
|
||||
}
|
||||
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
|
||||
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
|
||||
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err := saveCfg.Save(); err != nil {
|
||||
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package setting
|
||||
import (
|
||||
"math"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -96,7 +97,6 @@ var OAuth2 = struct {
|
||||
RefreshTokenExpirationTime int64
|
||||
InvalidateRefreshTokens bool
|
||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||
JWTSecretBase64 string `ini:"JWT_SECRET"`
|
||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||
MaxTokenLength int
|
||||
DefaultApplications []string
|
||||
@ -128,28 +128,50 @@ func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
return
|
||||
}
|
||||
|
||||
OAuth2.JWTSecretBase64 = loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
|
||||
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
|
||||
|
||||
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
|
||||
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
|
||||
}
|
||||
|
||||
if InstallLock {
|
||||
if _, err := generate.DecodeJwtSecretBase64(OAuth2.JWTSecretBase64); err != nil {
|
||||
_, OAuth2.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("error generating JWT secret: %v", err)
|
||||
}
|
||||
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
}
|
||||
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
|
||||
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
|
||||
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err := saveCfg.Save(); err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
}
|
||||
}
|
||||
generalSigningSecret.Store(&jwtSecretBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// generalSigningSecret is used as container for a []byte value
|
||||
// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
|
||||
var generalSigningSecret atomic.Pointer[[]byte]
|
||||
|
||||
func GetGeneralTokenSigningSecret() []byte {
|
||||
old := generalSigningSecret.Load()
|
||||
if old == nil || len(*old) == 0 {
|
||||
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("Unable to generate general JWT secret: %s", err.Error())
|
||||
}
|
||||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
|
||||
// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
|
||||
log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
|
||||
return jwtSecret
|
||||
}
|
||||
return *generalSigningSecret.Load()
|
||||
}
|
||||
return *old
|
||||
}
|
||||
|
34
modules/setting/oauth2_test.go
Normal file
34
modules/setting/oauth2_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetGeneralSigningSecret(t *testing.T) {
|
||||
// when there is no general signing secret, it should be generated, and keep the same value
|
||||
assert.Nil(t, generalSigningSecret.Load())
|
||||
s1 := GetGeneralTokenSigningSecret()
|
||||
assert.NotNil(t, s1)
|
||||
s2 := GetGeneralTokenSigningSecret()
|
||||
assert.Equal(t, s1, s2)
|
||||
|
||||
// the config value should always override any pre-generated value
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[oauth2]
|
||||
JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
`)
|
||||
defer test.MockVariableValue(&InstallLock, true)()
|
||||
loadOAuth2From(cfg)
|
||||
actual := GetGeneralTokenSigningSecret()
|
||||
expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
|
||||
assert.Len(t, actual, 32)
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap {
|
||||
"Safe": Safe,
|
||||
"Escape": Escape,
|
||||
"QueryEscape": url.QueryEscape,
|
||||
"JSEscape": template.JSEscapeString,
|
||||
"JSEscape": JSEscapeSafe,
|
||||
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
|
||||
"URLJoin": util.URLJoin,
|
||||
"DotEscape": DotEscape,
|
||||
@ -211,6 +211,10 @@ func Escape(s any) template.HTML {
|
||||
panic(fmt.Sprintf("unexpected type %T", s))
|
||||
}
|
||||
|
||||
func JSEscapeSafe(s string) template.HTML {
|
||||
return template.HTML(template.JSEscapeString(s))
|
||||
}
|
||||
|
||||
func RenderEmojiPlain(s any) any {
|
||||
switch v := s.(type) {
|
||||
case string:
|
||||
|
@ -52,3 +52,7 @@ func TestSubjectBodySeparator(t *testing.T) {
|
||||
"",
|
||||
"Insuficient\n--\nSeparators")
|
||||
}
|
||||
|
||||
func TestJSEscapeSafe(t *testing.T) {
|
||||
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -82,6 +83,71 @@ c=22
|
||||
assert.Equal(t, "22", lang1.TrString("c"))
|
||||
}
|
||||
|
||||
type stringerPointerReceiver struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (s *stringerPointerReceiver) String() string {
|
||||
return s.s
|
||||
}
|
||||
|
||||
type stringerStructReceiver struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (s stringerStructReceiver) String() string {
|
||||
return s.s
|
||||
}
|
||||
|
||||
type errorStructReceiver struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (e errorStructReceiver) Error() string {
|
||||
return e.s
|
||||
}
|
||||
|
||||
type errorPointerReceiver struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (e *errorPointerReceiver) Error() string {
|
||||
return e.s
|
||||
}
|
||||
|
||||
func TestLocaleWithTemplate(t *testing.T) {
|
||||
ls := NewLocaleStore()
|
||||
assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
|
||||
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
|
||||
tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
|
||||
|
||||
cases := []struct {
|
||||
in any
|
||||
want string
|
||||
}{
|
||||
{"<str>", "<a><str></a>"},
|
||||
{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
|
||||
{template.HTML("<html>"), "<a><html></a>"},
|
||||
{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{<stringerPointerReceiver>}</a>"},
|
||||
{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a><stringerPointerReceiver ptr></a>"},
|
||||
{stringerStructReceiver{"<stringerStructReceiver>"}, "<a><stringerStructReceiver></a>"},
|
||||
{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a><stringerStructReceiver ptr></a>"},
|
||||
{errorStructReceiver{"<errorStructReceiver>"}, "<a><errorStructReceiver></a>"},
|
||||
{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a><errorStructReceiver ptr></a>"},
|
||||
{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{<errorPointerReceiver>}</a>"},
|
||||
{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a><errorPointerReceiver ptr></a>"},
|
||||
}
|
||||
|
||||
buf := &strings.Builder{}
|
||||
for _, c := range cases {
|
||||
buf.Reset()
|
||||
assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
|
||||
assert.Equal(t, c.want, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocaleStoreQuirks(t *testing.T) {
|
||||
const nl = "\n"
|
||||
q := func(q1, s string, q2 ...string) string {
|
||||
|
@ -133,12 +133,14 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
||||
args := slices.Clone(trArgs)
|
||||
for i, v := range args {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
|
||||
// for most basic types (including template.HTML which is safe), just do nothing and use it
|
||||
case string:
|
||||
args[i] = template.HTML(template.HTMLEscapeString(v))
|
||||
args[i] = template.HTMLEscapeString(v)
|
||||
case fmt.Stringer:
|
||||
args[i] = template.HTMLEscapeString(v.String())
|
||||
default: // int, float, include template.HTML
|
||||
// do nothing, just use it
|
||||
default:
|
||||
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
return template.HTML(l.TrString(trKey, args...))
|
||||
|
17
options/license/Brian-Gladman-2-Clause
Normal file
17
options/license/Brian-Gladman-2-Clause
Normal file
@ -0,0 +1,17 @@
|
||||
Copyright (C) 1998-2013, Brian Gladman, Worcester, UK. All
|
||||
rights reserved.
|
||||
|
||||
The redistribution and use of this software (with or without
|
||||
changes) is allowed without the payment of fees or royalties
|
||||
provided that:
|
||||
|
||||
source code distributions include the above copyright notice,
|
||||
this list of conditions and the following disclaimer;
|
||||
|
||||
binary distributions include the above copyright notice, this
|
||||
list of conditions and the following disclaimer in their
|
||||
documentation.
|
||||
|
||||
This software is provided 'as is' with no explicit or implied
|
||||
warranties in respect of its operation, including, but not limited
|
||||
to, correctness and fitness for purpose.
|
11
options/license/CMU-Mach-nodoc
Normal file
11
options/license/CMU-Mach-nodoc
Normal file
@ -0,0 +1,11 @@
|
||||
Copyright (C) 2002 Naval Research Laboratory (NRL/CCS)
|
||||
|
||||
Permission to use, copy, modify and distribute this software and
|
||||
its documentation is hereby granted, provided that both the
|
||||
copyright notice and this permission notice appear in all copies of
|
||||
the software, derivative works or modified versions, and any
|
||||
portions thereof.
|
||||
|
||||
NRL ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION AND
|
||||
DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER
|
||||
RESULTING FROM THE USE OF THIS SOFTWARE.
|
1
options/license/GNOME-examples-exception
Normal file
1
options/license/GNOME-examples-exception
Normal file
@ -0,0 +1 @@
|
||||
As a special exception, the copyright holders give you permission to copy, modify, and distribute the example code contained in this document under the terms of your choosing, without restriction.
|
16
options/license/Gmsh-exception
Normal file
16
options/license/Gmsh-exception
Normal file
@ -0,0 +1,16 @@
|
||||
The copyright holders of Gmsh give you permission to combine Gmsh
|
||||
with code included in the standard release of Netgen (from Joachim
|
||||
Sch"oberl), METIS (from George Karypis at the University of
|
||||
Minnesota), OpenCASCADE (from Open CASCADE S.A.S) and ParaView
|
||||
(from Kitware, Inc.) under their respective licenses. You may copy
|
||||
and distribute such a system following the terms of the GNU GPL for
|
||||
Gmsh and the licenses of the other code concerned, provided that
|
||||
you include the source code of that other code when and as the GNU
|
||||
GPL requires distribution of source code.
|
||||
|
||||
Note that people who make modified versions of Gmsh are not
|
||||
obligated to grant this special exception for their modified
|
||||
versions; it is their choice whether to do so. The GNU General
|
||||
Public License gives permission to release a modified version
|
||||
without this exception; this exception also makes it possible to
|
||||
release a modified version which carries forward this exception.
|
13
options/license/HPND-Fenneberg-Livingston
Normal file
13
options/license/HPND-Fenneberg-Livingston
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright (C) 1995,1996,1997,1998 Lars Fenneberg <lf@elemental.net>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose and without fee is hereby granted, provided that this copyright and
|
||||
permission notice appear on all copies and supporting documentation, the
|
||||
name of Lars Fenneberg not be used in advertising or publicity pertaining to
|
||||
distribution of the program without specific prior permission, and notice be
|
||||
given in supporting documentation that copying and distribution is by
|
||||
permission of Lars Fenneberg.
|
||||
|
||||
Lars Fenneberg makes no representations about the suitability of this
|
||||
software for any purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
9
options/license/HPND-INRIA-IMAG
Normal file
9
options/license/HPND-INRIA-IMAG
Normal file
@ -0,0 +1,9 @@
|
||||
This software is available with usual "research" terms with
|
||||
the aim of retain credits of the software. Permission to use,
|
||||
copy, modify and distribute this software for any purpose and
|
||||
without fee is hereby granted, provided that the above copyright
|
||||
notice and this permission notice appear in all copies, and
|
||||
the name of INRIA, IMAG, or any contributor not be used in
|
||||
advertising or publicity pertaining to this material without
|
||||
the prior explicit permission. The software is provided "as
|
||||
is" without any warranties, support or liabilities of any kind.
|
25
options/license/Mackerras-3-Clause
Normal file
25
options/license/Mackerras-3-Clause
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 1995 Eric Rosenquist. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
3. The name(s) of the authors of this software must not be used to
|
||||
endorse or promote products derived from this software without
|
||||
prior written permission.
|
||||
|
||||
THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
25
options/license/Mackerras-3-Clause-acknowledgment
Normal file
25
options/license/Mackerras-3-Clause-acknowledgment
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 1993-2002 Paul Mackerras. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. The name(s) of the authors of this software must not be used to
|
||||
endorse or promote products derived from this software without
|
||||
prior written permission.
|
||||
|
||||
3. Redistributions of any form whatsoever must retain the following
|
||||
acknowledgment:
|
||||
"This product includes software developed by Paul Mackerras
|
||||
<paulus@ozlabs.org>".
|
||||
|
||||
THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
33
options/license/OpenVision
Normal file
33
options/license/OpenVision
Normal file
@ -0,0 +1,33 @@
|
||||
Copyright, OpenVision Technologies, Inc., 1993-1996, All Rights
|
||||
Reserved
|
||||
|
||||
WARNING: Retrieving the OpenVision Kerberos Administration system
|
||||
source code, as described below, indicates your acceptance of the
|
||||
following terms. If you do not agree to the following terms, do
|
||||
not retrieve the OpenVision Kerberos administration system.
|
||||
|
||||
You may freely use and distribute the Source Code and Object Code
|
||||
compiled from it, with or without modification, but this Source
|
||||
Code is provided to you "AS IS" EXCLUSIVE OF ANY WARRANTY,
|
||||
INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY OR
|
||||
FITNESS FOR A PARTICULAR PURPOSE, OR ANY OTHER WARRANTY, WHETHER
|
||||
EXPRESS OR IMPLIED. IN NO EVENT WILL OPENVISION HAVE ANY LIABILITY
|
||||
FOR ANY LOST PROFITS, LOSS OF DATA OR COSTS OF PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, INDIRECT, OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, INCLUDING,
|
||||
WITHOUT LIMITATION, THOSE RESULTING FROM THE USE OF THE SOURCE
|
||||
CODE, OR THE FAILURE OF THE SOURCE CODE TO PERFORM, OR FOR ANY
|
||||
OTHER REASON.
|
||||
|
||||
OpenVision retains all copyrights in the donated Source Code.
|
||||
OpenVision also retains copyright to derivative works of the Source
|
||||
Code, whether created by OpenVision or by a third party. The
|
||||
OpenVision copyright notice must be preserved if derivative works
|
||||
are made based on the donated Source Code.
|
||||
|
||||
OpenVision Technologies, Inc. has donated this Kerberos
|
||||
Administration system to MIT for inclusion in the standard Kerberos
|
||||
5 distribution. This donation underscores our commitment to
|
||||
continuing Kerberos technology development and our gratitude for
|
||||
the valuable work which has been performed by MIT and the Kerberos
|
||||
community.
|
13
options/license/Sun-PPP
Normal file
13
options/license/Sun-PPP
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright (c) 2001 by Sun Microsystems, Inc.
|
||||
All rights reserved.
|
||||
|
||||
Non-exclusive rights to redistribute, modify, translate, and use
|
||||
this software in source and binary forms, in whole or in part, is
|
||||
hereby granted, provided that the above copyright notice is
|
||||
duplicated in any source form, and that neither the name of the
|
||||
copyright holder nor the author is used to endorse or promote
|
||||
products derived from this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
19
options/license/UMich-Merit
Normal file
19
options/license/UMich-Merit
Normal file
@ -0,0 +1,19 @@
|
||||
[C] The Regents of the University of Michigan and Merit Network, Inc. 1992,
|
||||
1993, 1994, 1995 All Rights Reserved
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted, provided
|
||||
that the above copyright notice and this permission notice appear in all
|
||||
copies of the software and derivative works or modified versions thereof,
|
||||
and that both the copyright notice and this permission and disclaimer
|
||||
notice appear in supporting documentation.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
|
||||
EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE REGENTS OF THE
|
||||
UNIVERSITY OF MICHIGAN AND MERIT NETWORK, INC. DO NOT WARRANT THAT THE
|
||||
FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET LICENSEE'S REQUIREMENTS OR
|
||||
THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. The Regents of the
|
||||
University of Michigan and Merit Network, Inc. shall not be liable for any
|
||||
special, indirect, incidental or consequential damages with respect to any
|
||||
claim by Licensee or any third party arising from use of the software.
|
11
options/license/bcrypt-Solar-Designer
Normal file
11
options/license/bcrypt-Solar-Designer
Normal file
@ -0,0 +1,11 @@
|
||||
Written by Solar Designer <solar at openwall.com> in 1998-2014.
|
||||
No copyright is claimed, and the software is hereby placed in the public
|
||||
domain. In case this attempt to disclaim copyright and place the software
|
||||
in the public domain is deemed null and void, then the software is
|
||||
Copyright (c) 1998-2014 Solar Designer and it is hereby released to the
|
||||
general public under the following terms:
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted.
|
||||
|
||||
There's ABSOLUTELY NO WARRANTY, express or implied.
|
6
options/license/gtkbook
Normal file
6
options/license/gtkbook
Normal file
@ -0,0 +1,6 @@
|
||||
Copyright 2005 Syd Logan, All Rights Reserved
|
||||
|
||||
This code is distributed without warranty. You are free to use
|
||||
this code for any purpose, however, if this code is republished or
|
||||
redistributed in its original form, as hardcopy or electronically,
|
||||
then you must include this copyright notice along with the code.
|
6
options/license/softSurfer
Normal file
6
options/license/softSurfer
Normal file
@ -0,0 +1,6 @@
|
||||
Copyright 2001, softSurfer (www.softsurfer.com)
|
||||
This code may be freely used and modified for any purpose
|
||||
providing that this copyright notice is included with it.
|
||||
SoftSurfer makes no warranty for this code, and cannot be held
|
||||
liable for any real or imagined damage resulting from its use.
|
||||
Users of this code must verify correctness for their application.
|
@ -123,6 +123,7 @@ pin = Pin
|
||||
unpin = Unpin
|
||||
|
||||
artifacts = Artifacts
|
||||
confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ?
|
||||
|
||||
archived = Archived
|
||||
|
||||
@ -1979,15 +1980,10 @@ activity.git_stats_and_deletions = and
|
||||
activity.git_stats_deletion_1 = %d deletion
|
||||
activity.git_stats_deletion_n = %d deletions
|
||||
|
||||
contributors = Contributors
|
||||
contributors.contribution_type.filter_label = Contribution type:
|
||||
contributors.contribution_type.commits = Commits
|
||||
contributors.contribution_type.additions = Additions
|
||||
contributors.contribution_type.deletions = Deletions
|
||||
contributors.loading_title = Loading contributions...
|
||||
contributors.loading_title_failed = Could not load contributions
|
||||
contributors.loading_info = This might take a bit…
|
||||
contributors.component_failed_to_load = An unexpected error happened.
|
||||
|
||||
search = Search
|
||||
search.search_repo = Search repository
|
||||
@ -2592,6 +2588,13 @@ error.csv.too_large = Can't render this file because it is too large.
|
||||
error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
|
||||
error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
|
||||
|
||||
[graphs]
|
||||
component_loading = Loading %s...
|
||||
component_loading_failed = Could not load %s
|
||||
component_loading_info = This might take a bit…
|
||||
component_failed_to_load = An unexpected error happened.
|
||||
contributors.what = contributions
|
||||
|
||||
[org]
|
||||
org_name_holder = Organization Name
|
||||
org_full_name_holder = Organization Full Name
|
||||
|
@ -6,9 +6,9 @@
|
||||
//
|
||||
// This documentation describes the Gitea API.
|
||||
//
|
||||
// Schemes: http, https
|
||||
// Schemes: https, http
|
||||
// BasePath: /api/v1
|
||||
// Version: {{AppVer | JSEscape | Safe}}
|
||||
// Version: {{AppVer | JSEscape}}
|
||||
// License: MIT http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Consumes:
|
||||
|
@ -408,7 +408,7 @@ func canReadFiles(r *context.Repository) bool {
|
||||
return r.Permission.CanRead(unit.TypeCode)
|
||||
}
|
||||
|
||||
func base64Reader(s string) (io.Reader, error) {
|
||||
func base64Reader(s string) (io.ReadSeeker, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -579,16 +579,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
|
||||
|
||||
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
|
||||
func OIDCWellKnown(ctx *context.Context) {
|
||||
t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil)
|
||||
if err != nil {
|
||||
ctx.ServerError("unable to find template", err)
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
|
||||
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
|
||||
ctx.ServerError("unable to execute template", err)
|
||||
}
|
||||
ctx.JSONTemplate("user/auth/oidc_wellknown")
|
||||
}
|
||||
|
||||
// OIDCKeys generates the JSON Web Key Set
|
||||
|
@ -57,15 +57,16 @@ type ViewRequest struct {
|
||||
type ViewResponse struct {
|
||||
State struct {
|
||||
Run struct {
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
CanCancel bool `json:"canCancel"`
|
||||
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
||||
CanRerun bool `json:"canRerun"`
|
||||
Done bool `json:"done"`
|
||||
Jobs []*ViewJob `json:"jobs"`
|
||||
Commit ViewCommit `json:"commit"`
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
CanCancel bool `json:"canCancel"`
|
||||
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
||||
CanRerun bool `json:"canRerun"`
|
||||
CanDeleteArtifact bool `json:"canDeleteArtifact"`
|
||||
Done bool `json:"done"`
|
||||
Jobs []*ViewJob `json:"jobs"`
|
||||
Commit ViewCommit `json:"commit"`
|
||||
} `json:"run"`
|
||||
CurrentJob struct {
|
||||
Title string `json:"title"`
|
||||
@ -146,6 +147,7 @@ func ViewPost(ctx *context_module.Context) {
|
||||
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.Done = run.Status.IsDone()
|
||||
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
|
||||
resp.State.Run.Status = run.Status.String()
|
||||
@ -535,6 +537,29 @@ func ArtifactsView(ctx *context_module.Context) {
|
||||
ctx.JSON(http.StatusOK, artifactsResponse)
|
||||
}
|
||||
|
||||
func ArtifactsDeleteView(ctx *context_module.Context) {
|
||||
if !ctx.Repo.CanWrite(unit.TypeActions) {
|
||||
ctx.Error(http.StatusForbidden, "no permission")
|
||||
return
|
||||
}
|
||||
|
||||
runIndex := ctx.ParamsInt64("run")
|
||||
artifactName := ctx.Params("artifact_name")
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
}
|
||||
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, struct{}{})
|
||||
}
|
||||
|
||||
func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
runIndex := ctx.ParamsInt64("run")
|
||||
artifactName := ctx.Params("artifact_name")
|
||||
@ -562,6 +587,14 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||
for _, art := range artifacts {
|
||||
if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
|
||||
ctx.Error(http.StatusNotFound, "artifact not found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||
|
||||
writer := zip.NewWriter(ctx.Resp)
|
||||
|
@ -18,7 +18,7 @@ const (
|
||||
|
||||
// Contributors render the page to show repository contributors graph
|
||||
func Contributors(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.contributors")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
|
||||
|
||||
ctx.Data["PageIsActivity"] = true
|
||||
ctx.Data["PageIsContributors"] = true
|
||||
|
@ -652,6 +652,24 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
|
||||
}
|
||||
|
||||
if pb != nil && pb.EnableStatusCheck {
|
||||
|
||||
var missingRequiredChecks []string
|
||||
for _, requiredContext := range pb.StatusCheckContexts {
|
||||
contextFound := false
|
||||
matchesRequiredContext := createRequiredContextMatcher(requiredContext)
|
||||
for _, presentStatus := range commitStatuses {
|
||||
if matchesRequiredContext(presentStatus.Context) {
|
||||
contextFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !contextFound {
|
||||
missingRequiredChecks = append(missingRequiredChecks, requiredContext)
|
||||
}
|
||||
}
|
||||
ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
|
||||
|
||||
ctx.Data["is_context_required"] = func(context string) bool {
|
||||
for _, c := range pb.StatusCheckContexts {
|
||||
if c == context {
|
||||
@ -720,6 +738,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
|
||||
return compareInfo
|
||||
}
|
||||
|
||||
func createRequiredContextMatcher(requiredContext string) func(string) bool {
|
||||
if gp, err := glob.Compile(requiredContext); err == nil {
|
||||
return func(contextToCheck string) bool {
|
||||
return gp.Match(contextToCheck)
|
||||
}
|
||||
}
|
||||
|
||||
return func(contextToCheck string) bool {
|
||||
return requiredContext == contextToCheck
|
||||
}
|
||||
}
|
||||
|
||||
type pullCommitList struct {
|
||||
Commits []pull_service.CommitInfo `json:"commits"`
|
||||
LastReviewCommitSha string `json:"last_review_commit_sha"`
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"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/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -67,6 +68,88 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReleaseInfo struct {
|
||||
Release *repo_model.Release
|
||||
CommitStatus *git_model.CommitStatus
|
||||
CommitStatuses []*git_model.CommitStatus
|
||||
}
|
||||
|
||||
func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) {
|
||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, release := range releases {
|
||||
release.Repo = ctx.Repo.Repository
|
||||
}
|
||||
|
||||
if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Temporary cache commits count of used branches to speed up.
|
||||
countCache := make(map[string]int64)
|
||||
cacheUsers := make(map[int64]*user_model.User)
|
||||
if ctx.Doer != nil {
|
||||
cacheUsers[ctx.Doer.ID] = ctx.Doer
|
||||
}
|
||||
var ok bool
|
||||
|
||||
canReadActions := ctx.Repo.CanRead(unit.TypeActions)
|
||||
|
||||
releaseInfos := make([]*ReleaseInfo, 0, len(releases))
|
||||
for _, r := range releases {
|
||||
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
|
||||
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
r.Publisher = user_model.NewGhostUser()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cacheUsers[r.PublisherID] = r.Publisher
|
||||
}
|
||||
|
||||
r.Note, err = markdown.RenderString(&markup.RenderContext{
|
||||
Links: markup.Links{
|
||||
Base: ctx.Repo.RepoLink,
|
||||
},
|
||||
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
|
||||
GitRepo: ctx.Repo.GitRepo,
|
||||
Ctx: ctx,
|
||||
}, r.Note)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !r.IsDraft {
|
||||
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
info := &ReleaseInfo{
|
||||
Release: r,
|
||||
}
|
||||
|
||||
if canReadActions {
|
||||
statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptions{ListAll: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.CommitStatus = git_model.CalcCommitStatus(statuses)
|
||||
info.CommitStatuses = statuses
|
||||
}
|
||||
|
||||
releaseInfos = append(releaseInfos, info)
|
||||
}
|
||||
|
||||
return releaseInfos, nil
|
||||
}
|
||||
|
||||
// Releases render releases list page
|
||||
func Releases(ctx *context.Context) {
|
||||
ctx.Data["PageIsReleaseList"] = true
|
||||
@ -91,77 +174,21 @@ func Releases(ctx *context.Context) {
|
||||
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
|
||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
||||
|
||||
opts := repo_model.FindReleasesOptions{
|
||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
||||
ListOptions: listOptions,
|
||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||
IncludeDrafts: writeAccess,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReleasesByRepoID", err)
|
||||
ctx.ServerError("getReleaseInfos", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, release := range releases {
|
||||
release.Repo = ctx.Repo.Repository
|
||||
}
|
||||
|
||||
if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
|
||||
ctx.ServerError("GetReleaseAttachments", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Temporary cache commits count of used branches to speed up.
|
||||
countCache := make(map[string]int64)
|
||||
cacheUsers := make(map[int64]*user_model.User)
|
||||
if ctx.Doer != nil {
|
||||
cacheUsers[ctx.Doer.ID] = ctx.Doer
|
||||
}
|
||||
var ok bool
|
||||
|
||||
for _, r := range releases {
|
||||
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
|
||||
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
r.Publisher = user_model.NewGhostUser()
|
||||
} else {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
cacheUsers[r.PublisherID] = r.Publisher
|
||||
}
|
||||
|
||||
r.Note, err = markdown.RenderString(&markup.RenderContext{
|
||||
Links: markup.Links{
|
||||
Base: ctx.Repo.RepoLink,
|
||||
},
|
||||
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
|
||||
GitRepo: ctx.Repo.GitRepo,
|
||||
Ctx: ctx,
|
||||
}, r.Note)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.IsDraft {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
|
||||
ctx.ServerError("calReleaseNumCommitsBehind", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Releases"] = releases
|
||||
|
||||
numReleases := ctx.Data["NumReleases"].(int64)
|
||||
pager := context.NewPagination(int(numReleases), opts.PageSize, opts.Page, 5)
|
||||
pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@ -249,15 +276,24 @@ func SingleRelease(ctx *context.Context) {
|
||||
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
|
||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
||||
|
||||
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*"))
|
||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 1},
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
TagNames: []string{ctx.Params("*")},
|
||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||
IncludeDrafts: writeAccess,
|
||||
})
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.NotFound("GetRelease", err)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetReleasesByRepoID", err)
|
||||
ctx.ServerError("getReleaseInfos", err)
|
||||
return
|
||||
}
|
||||
if len(releases) != 1 {
|
||||
ctx.NotFound("SingleRelease", err)
|
||||
return
|
||||
}
|
||||
|
||||
release := releases[0].Release
|
||||
|
||||
ctx.Data["PageIsSingleTag"] = release.IsTag
|
||||
if release.IsTag {
|
||||
ctx.Data["Title"] = release.TagName
|
||||
@ -265,43 +301,7 @@ func SingleRelease(ctx *context.Context) {
|
||||
ctx.Data["Title"] = release.Title
|
||||
}
|
||||
|
||||
release.Repo = ctx.Repo.Repository
|
||||
|
||||
err = repo_model.GetReleaseAttachments(ctx, release)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReleaseAttachments", err)
|
||||
return
|
||||
}
|
||||
|
||||
release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
release.Publisher = user_model.NewGhostUser()
|
||||
} else {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !release.IsDraft {
|
||||
if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
|
||||
ctx.ServerError("calReleaseNumCommitsBehind", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
release.Note, err = markdown.RenderString(&markup.RenderContext{
|
||||
Links: markup.Links{
|
||||
Base: ctx.Repo.RepoLink,
|
||||
},
|
||||
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
|
||||
GitRepo: ctx.Repo.GitRepo,
|
||||
Ctx: ctx,
|
||||
}, release.Note)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Releases"] = []*repo_model.Release{release}
|
||||
ctx.Data["Releases"] = releases
|
||||
ctx.HTML(http.StatusOK, tplReleasesList)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
@ -36,7 +38,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
|
||||
|
||||
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
|
||||
ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL
|
||||
ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
|
||||
|
||||
// Show OpenID URIs
|
||||
openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
|
||||
|
@ -4,22 +4,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
)
|
||||
|
||||
// tplSwaggerV1Json swagger v1 json template
|
||||
const tplSwaggerV1Json base.TplName = "swagger/v1_json"
|
||||
|
||||
// SwaggerV1Json render swagger v1 json
|
||||
func SwaggerV1Json(ctx *context.Context) {
|
||||
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil)
|
||||
if err != nil {
|
||||
ctx.ServerError("unable to find template", err)
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
|
||||
ctx.ServerError("unable to execute template", err)
|
||||
}
|
||||
ctx.JSONTemplate("swagger/v1_json")
|
||||
}
|
||||
|
@ -1371,6 +1371,7 @@ func registerRoutes(m *web.Route) {
|
||||
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
||||
m.Post("/artifacts", actions.ArtifactsView)
|
||||
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
|
||||
m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
|
||||
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||
})
|
||||
}, reqRepoActionsReader, actions.MustEnableActions)
|
||||
|
@ -38,7 +38,7 @@ func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString([]byte(setting.SecretKey))
|
||||
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -62,7 +62,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return []byte(setting.SecretKey), nil
|
||||
return setting.GetGeneralTokenSigningSecret(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -20,7 +20,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
|
||||
assert.NotEqual(t, "", token)
|
||||
claims := jwt.MapClaims{}
|
||||
_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(setting.SecretKey), nil
|
||||
return setting.GetGeneralTokenSigningSecret(), nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
scp, ok := claims["scp"]
|
||||
|
@ -20,23 +20,59 @@ func Cleanup(taskCtx context.Context, olderThan time.Duration) error {
|
||||
return CleanupArtifacts(taskCtx)
|
||||
}
|
||||
|
||||
// CleanupArtifacts removes expired artifacts and set records expired status
|
||||
// CleanupArtifacts removes expired add need-deleted artifacts and set records expired status
|
||||
func CleanupArtifacts(taskCtx context.Context) error {
|
||||
if err := cleanExpiredArtifacts(taskCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
return cleanNeedDeleteArtifacts(taskCtx)
|
||||
}
|
||||
|
||||
func cleanExpiredArtifacts(taskCtx context.Context) error {
|
||||
artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Found %d expired artifacts", len(artifacts))
|
||||
for _, artifact := range artifacts {
|
||||
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
|
||||
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
|
||||
continue
|
||||
}
|
||||
if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil {
|
||||
log.Error("Cannot set artifact %d expired: %v", artifact.ID, err)
|
||||
continue
|
||||
}
|
||||
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
|
||||
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
|
||||
continue
|
||||
}
|
||||
log.Info("Artifact %d set expired", artifact.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteArtifactBatchSize is the batch size of deleting artifacts
|
||||
const deleteArtifactBatchSize = 100
|
||||
|
||||
func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
|
||||
for {
|
||||
artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Found %d artifacts pending deletion", len(artifacts))
|
||||
for _, artifact := range artifacts {
|
||||
if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
|
||||
log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
|
||||
continue
|
||||
}
|
||||
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
|
||||
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
|
||||
continue
|
||||
}
|
||||
log.Info("Artifact %d set deleted", artifact.ID)
|
||||
}
|
||||
if len(artifacts) < deleteArtifactBatchSize {
|
||||
log.Debug("No more artifacts pending deletion")
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -64,6 +64,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
|
||||
return fmt.Errorf("head of pull request is missing in event payload")
|
||||
}
|
||||
sha = payload.PullRequest.Head.Sha
|
||||
case webhook_module.HookEventRelease:
|
||||
event = string(run.Event)
|
||||
sha = run.CommitSHA
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -55,6 +55,47 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
|
||||
}).Notify(withMethod(ctx, "NewIssue"))
|
||||
}
|
||||
|
||||
// IssueChangeContent notifies change content of issue
|
||||
func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
|
||||
ctx = withMethod(ctx, "IssueChangeContent")
|
||||
|
||||
var err error
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
|
||||
if issue.IsPull {
|
||||
if err = issue.LoadPullRequest(ctx); err != nil {
|
||||
log.Error("loadPullRequest: %v", err)
|
||||
return
|
||||
}
|
||||
newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest).
|
||||
WithDoer(doer).
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: api.HookIssueEdited,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).
|
||||
WithPullRequest(issue.PullRequest).
|
||||
Notify(ctx)
|
||||
return
|
||||
}
|
||||
newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).
|
||||
WithDoer(doer).
|
||||
WithPayload(&api.IssuePayload{
|
||||
Action: api.HookIssueEdited,
|
||||
Index: issue.Index,
|
||||
Issue: convert.ToAPIIssue(ctx, issue),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).
|
||||
Notify(ctx)
|
||||
}
|
||||
|
||||
// IssueChangeStatus notifies close or reopen issue to notifiers
|
||||
func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) {
|
||||
ctx = withMethod(ctx, "IssueChangeStatus")
|
||||
@ -101,11 +142,40 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
|
||||
Notify(ctx)
|
||||
}
|
||||
|
||||
// IssueChangeAssignee notifies assigned or unassigned to notifiers
|
||||
func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
|
||||
ctx = withMethod(ctx, "IssueChangeAssignee")
|
||||
|
||||
var action api.HookIssueAction
|
||||
if removed {
|
||||
action = api.HookIssueUnassigned
|
||||
} else {
|
||||
action = api.HookIssueAssigned
|
||||
}
|
||||
notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestAssign, action)
|
||||
}
|
||||
|
||||
// IssueChangeMilestone notifies assignee to notifiers
|
||||
func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
|
||||
ctx = withMethod(ctx, "IssueChangeMilestone")
|
||||
|
||||
var action api.HookIssueAction
|
||||
if issue.MilestoneID > 0 {
|
||||
action = api.HookIssueMilestoned
|
||||
} else {
|
||||
action = api.HookIssueDemilestoned
|
||||
}
|
||||
notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestMilestone, action)
|
||||
}
|
||||
|
||||
func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
|
||||
_, _ []*issues_model.Label,
|
||||
) {
|
||||
ctx = withMethod(ctx, "IssueChangeLabels")
|
||||
notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestLabel, api.HookIssueLabelUpdated)
|
||||
}
|
||||
|
||||
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
|
||||
var err error
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("LoadRepo: %v", err)
|
||||
@ -117,20 +187,15 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
|
||||
return
|
||||
}
|
||||
|
||||
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
|
||||
if issue.IsPull {
|
||||
if err = issue.LoadPullRequest(ctx); err != nil {
|
||||
log.Error("loadPullRequest: %v", err)
|
||||
return
|
||||
}
|
||||
if err = issue.PullRequest.LoadIssue(ctx); err != nil {
|
||||
log.Error("LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel).
|
||||
newNotifyInputFromIssue(issue, event).
|
||||
WithDoer(doer).
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: api.HookIssueLabelUpdated,
|
||||
Action: action,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
|
||||
@ -140,10 +205,11 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
|
||||
Notify(ctx)
|
||||
return
|
||||
}
|
||||
newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel).
|
||||
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
|
||||
newNotifyInputFromIssue(issue, event).
|
||||
WithDoer(doer).
|
||||
WithPayload(&api.IssuePayload{
|
||||
Action: api.HookIssueLabelUpdated,
|
||||
Action: action,
|
||||
Index: issue.Index,
|
||||
Issue: convert.ToAPIIssue(ctx, issue),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
@ -305,6 +371,39 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
|
||||
}).Notify(ctx)
|
||||
}
|
||||
|
||||
func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
if !issue.IsPull {
|
||||
log.Warn("PullRequestReviewRequest: issue is not a pull request: %v", issue.ID)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = withMethod(ctx, "PullRequestReviewRequest")
|
||||
|
||||
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
log.Error("LoadPullRequest failed: %v", err)
|
||||
return
|
||||
}
|
||||
var action api.HookIssueAction
|
||||
if isRequest {
|
||||
action = api.HookIssueReviewRequested
|
||||
} else {
|
||||
action = api.HookIssueReviewRequestRemoved
|
||||
}
|
||||
newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestReviewRequest).
|
||||
WithDoer(doer).
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: action,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).
|
||||
WithPullRequest(issue.PullRequest).
|
||||
Notify(ctx)
|
||||
}
|
||||
|
||||
func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
ctx = withMethod(ctx, "MergePullRequest")
|
||||
|
||||
|
@ -18,7 +18,7 @@ func (source *Source) FromDB(bs []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
// ToDB exports the config to a byte slice to be saved into database (this method is just dummy and does nothing for DB source)
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -301,7 +300,7 @@ func InitSigningKey() error {
|
||||
case "HS384":
|
||||
fallthrough
|
||||
case "HS512":
|
||||
key, err = loadSymmetricKey()
|
||||
key = setting.GetGeneralTokenSigningSecret()
|
||||
case "RS256":
|
||||
fallthrough
|
||||
case "RS384":
|
||||
@ -334,12 +333,6 @@ func InitSigningKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSymmetricKey checks if the configured secret is valid.
|
||||
// If it is not valid, it will return an error.
|
||||
func loadSymmetricKey() (any, error) {
|
||||
return generate.DecodeJwtSecretBase64(setting.OAuth2.JWTSecretBase64)
|
||||
}
|
||||
|
||||
// loadOrCreateAsymmetricKey checks if the configured private key exists.
|
||||
// If it does not exist a new random key gets generated and saved on the configured path.
|
||||
func loadOrCreateAsymmetricKey() (any, error) {
|
||||
|
@ -33,7 +33,7 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString([]byte(setting.SecretKey))
|
||||
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -57,7 +57,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return []byte(setting.SecretKey), nil
|
||||
return setting.GetGeneralTokenSigningSecret(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -51,6 +51,10 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if matchedCount != len(requiredContexts) {
|
||||
return structs.CommitStatusPending
|
||||
}
|
||||
|
||||
if matchedCount == 0 {
|
||||
status := git_model.CalcCommitStatus(commitStatuses)
|
||||
if status != nil {
|
||||
|
@ -40,7 +40,7 @@ type ChangeRepoFile struct {
|
||||
Operation string
|
||||
TreePath string
|
||||
FromTreePath string
|
||||
ContentReader io.Reader
|
||||
ContentReader io.ReadSeeker
|
||||
SHA string
|
||||
Options *RepoFileOptions
|
||||
}
|
||||
@ -448,6 +448,10 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
_, err := file.ContentReader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil {
|
||||
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
|
||||
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
|
||||
|
@ -321,14 +321,9 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
|
||||
return nil
|
||||
}
|
||||
|
||||
lowerTags := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
lowerTags = append(lowerTags, strings.ToLower(tag))
|
||||
}
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
TagNames: lowerTags,
|
||||
TagNames: tags,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
|
||||
@ -338,6 +333,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
|
||||
relMap[rel.LowerTagName] = rel
|
||||
}
|
||||
|
||||
lowerTags := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
lowerTags = append(lowerTags, strings.ToLower(tag))
|
||||
}
|
||||
|
||||
newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap))
|
||||
|
||||
emailToUser := make(map[string]*user_model.User)
|
||||
|
@ -88,7 +88,7 @@
|
||||
{{ctx.Locale.Tr "packages.settings.delete"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ctx.Locale.Tr "packages.settings.delete.notice" `<span class="name"></span>` `<span class="dataVersion"></span>` | Safe}}
|
||||
{{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|Safe) (`<span class="dataVersion"></span>`|Safe)}}
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
|
||||
{{ctx.Locale.Tr "repo.settings.delete_notices_2" `<span class="name"></span>` | Safe}}<br>
|
||||
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|Safe)}}<br>
|
||||
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
|
@ -39,7 +39,7 @@
|
||||
{{ctx.Locale.Tr "admin.monitor.process.cancel"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" `<span class="name"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|Safe)}}</p>
|
||||
<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<div class="inline field {{if .Err_Visibility}}error{{end}}">
|
||||
<span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span>
|
||||
<div class="ui selection type dropdown">
|
||||
<input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{.visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}">
|
||||
<input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{printf "%d" .visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}">
|
||||
<div class="text">
|
||||
{{if .DefaultUserVisibilityMode.IsPublic}}{{ctx.Locale.Tr "settings.visibility.public"}}{{end}}
|
||||
{{if .DefaultUserVisibilityMode.IsLimited}}{{ctx.Locale.Tr "settings.visibility.limited"}}{{end}}
|
||||
|
@ -16,6 +16,5 @@
|
||||
<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + '. Please make sure the asset files can be accessed.')"></script>
|
||||
|
||||
{{template "custom/footer" .}}
|
||||
{{ctx.DataRaceCheck $.Context}}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -30,7 +30,6 @@
|
||||
{{template "custom/header" .}}
|
||||
</head>
|
||||
<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph,ws" hx-push-url="false" ws-connect="/-/ws">
|
||||
{{ctx.DataRaceCheck $.Context}}
|
||||
{{template "custom/body_outer_pre" .}}
|
||||
|
||||
<div class="full height">
|
||||
|
@ -73,7 +73,7 @@
|
||||
{{ctx.Locale.Tr "org.members.leave"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
@ -82,7 +82,7 @@
|
||||
{{ctx.Locale.Tr "org.members.remove"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataOrganizationName"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|Safe) (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
@ -81,7 +81,7 @@
|
||||
{{ctx.Locale.Tr "org.members.remove"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataTeamName"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|Safe) (`<span class="dataTeamName"></span>`|Safe)}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
@ -88,7 +88,7 @@
|
||||
{{ctx.Locale.Tr "org.teams.leave"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|Safe)}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@
|
||||
{{ctx.Locale.Tr "org.teams.leave"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|Safe)}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@
|
||||
data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}"
|
||||
data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}"
|
||||
data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}"
|
||||
data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}"
|
||||
data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}"
|
||||
data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}"
|
||||
data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}"
|
||||
|
@ -88,7 +88,7 @@
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field">
|
||||
<label>
|
||||
{{ctx.Locale.Tr "repo.branch.new_branch_from" `<span class="text" id="modal-create-branch-from-span"></span>` | Safe}}
|
||||
{{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|Safe)}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="required field">
|
||||
@ -113,7 +113,7 @@
|
||||
<input type="hidden" name="create_tag" value="true">
|
||||
<div class="field">
|
||||
<label>
|
||||
{{ctx.Locale.Tr "repo.tag.create_tag_from" `<span class="text" id="modal-create-tag-from-span"></span>` | Safe}}
|
||||
{{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|Safe)}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="required field">
|
||||
@ -184,7 +184,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{{if .Commit.Signature}}
|
||||
<div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw {{$class}}">
|
||||
<div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw gt-mb-0 {{$class}}">
|
||||
<div class="gt-df gt-ac">
|
||||
{{if .Verification.Verified}}
|
||||
{{if ne .Verification.SigningUser.ID 0}}
|
||||
|
@ -1,10 +1,10 @@
|
||||
{{if .Statuses}}
|
||||
{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
|
||||
<a class="gt-vm gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
|
||||
<a class="gt-vm {{.AdditionalClasses}} gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
|
||||
{{template "repo/commit_status" .Status}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="gt-vm" data-tippy="commit-statuses" tabindex="0">
|
||||
<span class="gt-vm {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
|
||||
{{template "repo/commit_status" .Status}}
|
||||
</span>
|
||||
{{end}}
|
||||
|
@ -4,10 +4,10 @@
|
||||
data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
|
||||
data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"
|
||||
data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}"
|
||||
data-locale-loading-title="{{ctx.Locale.Tr "repo.contributors.loading_title"}}"
|
||||
data-locale-loading-title-failed="{{ctx.Locale.Tr "repo.contributors.loading_title_failed"}}"
|
||||
data-locale-loading-info="{{ctx.Locale.Tr "repo.contributors.loading_info"}}"
|
||||
data-locale-component-failed-to-load="{{ctx.Locale.Tr "repo.contributors.component_failed_to_load"}}"
|
||||
data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.contributors.what")}}"
|
||||
data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.contributors.what")}}"
|
||||
data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
|
||||
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
|
||||
>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
|
||||
<div>
|
||||
<div class="diff-detail-box diff-box">
|
||||
<div class="gt-df gt-ac gt-fw">
|
||||
<div class="gt-df gt-ac gt-fw gt-gap-3 gt-ml-1">
|
||||
{{if $showFileTree}}
|
||||
<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
|
||||
{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
|
||||
|
@ -112,9 +112,9 @@
|
||||
{{template "shared/user/authorlink" .Poster}}
|
||||
{{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}}
|
||||
{{if eq $.Issue.PullRequest.Status 3}}
|
||||
{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}}
|
||||
{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID) | Safe) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape) | Safe) $createdStr}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}}
|
||||
{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID) | Safe) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape) | Safe) $createdStr}}
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -24,6 +24,7 @@
|
||||
{{template "repo/pulls/status" (dict
|
||||
"CommitStatus" .LatestCommitStatus
|
||||
"CommitStatuses" .LatestCommitStatuses
|
||||
"MissingRequiredChecks" .MissingRequiredChecks
|
||||
"ShowHideChecks" true
|
||||
"is_context_required" .is_context_required
|
||||
)}}
|
||||
@ -38,7 +39,7 @@
|
||||
{{ctx.Locale.Tr "repo.pulls.merged_success"}}
|
||||
</h3>
|
||||
<div class="merge-section-info">
|
||||
{{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape)) | Str2html}}
|
||||
{{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape) | Safe)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-section-right">
|
||||
|
@ -2,6 +2,7 @@
|
||||
Template Attributes:
|
||||
* CommitStatus: summary of all commit status state
|
||||
* CommitStatuses: all commit status elements
|
||||
* MissingRequiredChecks: commit check contexts that are required by branch protection but not present
|
||||
* ShowHideChecks: whether use a button to show/hide the checks
|
||||
* is_context_required: Used in pull request commit status check table
|
||||
*/}}
|
||||
@ -9,7 +10,7 @@ Template Attributes:
|
||||
{{if .CommitStatus}}
|
||||
<div class="commit-status-panel">
|
||||
<div class="ui top attached header commit-status-header">
|
||||
{{if eq .CommitStatus.State "pending"}}
|
||||
{{if or (eq .CommitStatus.State "pending") (.MissingRequiredChecks)}}
|
||||
{{ctx.Locale.Tr "repo.pulls.status_checking"}}
|
||||
{{else if eq .CommitStatus.State "success"}}
|
||||
{{ctx.Locale.Tr "repo.pulls.status_checks_success"}}
|
||||
@ -46,6 +47,13 @@ Template Attributes:
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range .MissingRequiredChecks}}
|
||||
<div class="commit-status-item">
|
||||
{{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}}
|
||||
<div class="status-context gt-ellipsis">{{.}}</div>
|
||||
<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -5,90 +5,90 @@
|
||||
{{template "base/alert" .}}
|
||||
{{template "repo/release_tag_header" .}}
|
||||
<ul id="release-list">
|
||||
{{range $idx, $release := .Releases}}
|
||||
{{range $idx, $info := .Releases}}
|
||||
{{$release := $info.Release}}
|
||||
<li class="ui grid">
|
||||
<div class="ui four wide column meta">
|
||||
<a class="muted" href="{{if not (and .Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "gt-mr-2"}}{{.TagName}}</a>
|
||||
{{if and .Sha1 ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a>
|
||||
{{template "repo/branch_dropdown" dict "root" $ "release" .}}
|
||||
{{end}}
|
||||
<a class="muted" href="{{if not (and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{$release.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "gt-mr-2"}}{{$release.TagName}}</a>
|
||||
{{if and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{$release.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha $release.Sha1}}</a>
|
||||
{{template "repo/branch_dropdown" dict "root" $ "release" $release}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ui twelve wide column detail">
|
||||
<div class="gt-df gt-ac gt-sb gt-fw gt-mb-3">
|
||||
<h4 class="release-list-title gt-word-break">
|
||||
<a href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{.Title}}</a>
|
||||
{{if .IsDraft}}
|
||||
<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
|
||||
{{else if .IsPrerelease}}
|
||||
<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
|
||||
{{else}}
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
|
||||
{{end}}
|
||||
</h4>
|
||||
<div>
|
||||
{{if $.CanCreateRelease}}
|
||||
<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{.TagName | PathEscapeSegments}}" rel="nofollow">
|
||||
{{svg "octicon-pencil"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text grey">
|
||||
<span class="author">
|
||||
{{if .OriginalAuthor}}
|
||||
{{svg (MigrationIcon .Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{.OriginalAuthor}}
|
||||
{{else if .Publisher}}
|
||||
{{ctx.AvatarUtils.Avatar .Publisher 20 "gt-mr-2"}}
|
||||
<a href="{{.Publisher.HomeLink}}">{{.Publisher.GetDisplayName}}</a>
|
||||
<div class="gt-df gt-ac gt-sb gt-fw gt-mb-3">
|
||||
<h4 class="release-list-title gt-word-break">
|
||||
<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>
|
||||
{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "gt-df"}}
|
||||
{{if $release.IsDraft}}
|
||||
<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
|
||||
{{else if $release.IsPrerelease}}
|
||||
<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
|
||||
{{else}}
|
||||
Ghost
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
|
||||
{{end}}
|
||||
</span>
|
||||
<span class="released">
|
||||
{{ctx.Locale.Tr "repo.released_this"}}
|
||||
</span>
|
||||
{{if .CreatedUnix}}
|
||||
<span class="time">{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
|
||||
</h4>
|
||||
<div>
|
||||
{{if $.CanCreateRelease}}
|
||||
<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{$release.TagName | PathEscapeSegments}}" rel="nofollow">
|
||||
{{svg "octicon-pencil"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" .TargetBehind}}</span>
|
||||
{{end}}
|
||||
</p>
|
||||
<div class="markup desc">
|
||||
{{Str2html .Note}}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<details class="download" {{if eq $idx 0}}open{{end}}>
|
||||
<summary class="gt-my-4">
|
||||
{{ctx.Locale.Tr "repo.release.downloads"}}
|
||||
</summary>
|
||||
<ul class="list">
|
||||
{{if and (not $.DisableDownloadSourceArchives) (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<li>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if .Attachments}}
|
||||
{{range .Attachments}}
|
||||
<li>
|
||||
<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
||||
<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
|
||||
</a>
|
||||
<div>
|
||||
<span class="text grey">{{.Size | FileSize}}</span>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
|
||||
{{svg "octicon-info"}}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
<p class="text grey">
|
||||
<span class="author">
|
||||
{{if $release.OriginalAuthor}}
|
||||
{{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{$release.OriginalAuthor}}
|
||||
{{else if $release.Publisher}}
|
||||
{{ctx.AvatarUtils.Avatar $release.Publisher 20 "gt-mr-2"}}
|
||||
<a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a>
|
||||
{{else}}
|
||||
Ghost
|
||||
{{end}}
|
||||
</span>
|
||||
<span class="released">
|
||||
{{ctx.Locale.Tr "repo.released_this"}}
|
||||
</span>
|
||||
{{if $release.CreatedUnix}}
|
||||
<span class="time">{{TimeSinceUnix $release.CreatedUnix ctx.Locale}}</span>
|
||||
{{end}}
|
||||
{{if and (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind | Str2html}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span>
|
||||
{{end}}
|
||||
</p>
|
||||
<div class="markup desc">
|
||||
{{Str2html $release.Note}}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<details class="download" {{if eq $idx 0}}open{{end}}>
|
||||
<summary class="gt-my-4">
|
||||
{{ctx.Locale.Tr "repo.release.downloads"}}
|
||||
</summary>
|
||||
<ul class="list">
|
||||
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<li>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{range $release.Attachments}}
|
||||
<li>
|
||||
<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
||||
<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
|
||||
</a>
|
||||
<div>
|
||||
<span class="text grey">{{.Size | FileSize}}</span>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
|
||||
{{svg "octicon-info"}}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</details>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -263,7 +263,7 @@
|
||||
<label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label>
|
||||
<input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}>
|
||||
{{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}}
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" "<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Str2html}}</span>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Safe)}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
|
@ -31,9 +31,8 @@
|
||||
<li>
|
||||
{{svg "octicon-location"}}
|
||||
<span class="gt-f1">{{.ContextUser.Location}}</span>
|
||||
{{if .UserLocationMapURL}}
|
||||
{{/* We presume that the UserLocationMapURL is safe, as it is provided by the site administrator. */}}
|
||||
<a href="{{.UserLocationMapURL | Safe}}{{.ContextUser.Location | QueryEscape}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
|
||||
{{if .ContextUserLocationMapURL}}
|
||||
<a href="{{.ContextUserLocationMapURL}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
|
||||
{{svg "octicon-link-external"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
8
templates/swagger/v1_json.tmpl
generated
8
templates/swagger/v1_json.tmpl
generated
@ -8,8 +8,8 @@
|
||||
"text/html"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
"https",
|
||||
"http"
|
||||
],
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
@ -19,9 +19,9 @@
|
||||
"name": "MIT",
|
||||
"url": "http://opensource.org/licenses/MIT"
|
||||
},
|
||||
"version": "{{AppVer | JSEscape | Safe}}"
|
||||
"version": "{{AppVer | JSEscape}}"
|
||||
},
|
||||
"basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1",
|
||||
"basePath": "{{AppSubUrl | JSEscape}}/api/v1",
|
||||
"paths": {
|
||||
"/activitypub/user-id/{user-id}": {
|
||||
"get": {
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"issuer": "{{AppUrl | JSEscape | Safe}}",
|
||||
"authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
|
||||
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
|
||||
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
|
||||
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
|
||||
"introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
|
||||
"issuer": "{{AppUrl | JSEscape}}",
|
||||
"authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize",
|
||||
"token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token",
|
||||
"jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys",
|
||||
"userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo",
|
||||
"introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect",
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"id_token"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}"
|
||||
"{{.SigningKey.SigningMethod.Alg | JSEscape}}"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public"
|
||||
|
@ -47,7 +47,7 @@
|
||||
{{ctx.Locale.Tr "org.members.leave"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p>
|
||||
<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|Safe)}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
@ -13,11 +13,14 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestAPIPullReview(t *testing.T) {
|
||||
@ -314,3 +317,126 @@ func TestAPIPullReviewRequest(t *testing.T) {
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIPullReviewStayDismissed(t *testing.T) {
|
||||
// This test against issue https://github.com/go-gitea/gitea/issues/28542
|
||||
// where old reviews surface after a review request got dismissed.
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session2 := loginUser(t, user2.LoginName)
|
||||
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
|
||||
user8 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
|
||||
session8 := loginUser(t, user8.LoginName)
|
||||
token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// user2 request user8
|
||||
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
|
||||
Reviewers: []string{user8.LoginName},
|
||||
}).AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check we have only one review request",
|
||||
pullIssue.ID, user8.ID, 0, 1, 1, false)
|
||||
|
||||
// user2 request user8 again, it is expected to be ignored
|
||||
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
|
||||
Reviewers: []string{user8.LoginName},
|
||||
}).AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check we have only one review request, even after re-request it again",
|
||||
pullIssue.ID, user8.ID, 0, 1, 1, false)
|
||||
|
||||
// user8 reviews it as accept
|
||||
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
|
||||
Event: "APPROVED",
|
||||
Body: "lgtm",
|
||||
}).AddTokenAuth(token8)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check we have one valid approval",
|
||||
pullIssue.ID, user8.ID, 0, 0, 1, true)
|
||||
|
||||
// emulate of auto-dismiss lgtm on a protected branch that where a pull just got an update
|
||||
_, err := db.GetEngine(db.DefaultContext).Where("issue_id = ? AND reviewer_id = ?", pullIssue.ID, user8.ID).
|
||||
Cols("dismissed").Update(&issues_model.Review{Dismissed: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// user2 request user8 again
|
||||
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
|
||||
Reviewers: []string{user8.LoginName},
|
||||
}).AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check we have no valid approval and one review request",
|
||||
pullIssue.ID, user8.ID, 1, 1, 2, false)
|
||||
|
||||
// user8 dismiss review
|
||||
_, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, user8, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check new review request is now dismissed",
|
||||
pullIssue.ID, user8.ID, 1, 0, 1, false)
|
||||
|
||||
// add a new valid approval
|
||||
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
|
||||
Event: "APPROVED",
|
||||
Body: "lgtm",
|
||||
}).AddTokenAuth(token8)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check that old reviews requests are deleted",
|
||||
pullIssue.ID, user8.ID, 1, 0, 2, true)
|
||||
|
||||
// now add a change request witch should dismiss the approval
|
||||
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
|
||||
Event: "REQUEST_CHANGES",
|
||||
Body: "please change XYZ",
|
||||
}).AddTokenAuth(token8)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
reviewsCountCheck(t,
|
||||
"check that old reviews are dismissed",
|
||||
pullIssue.ID, user8.ID, 2, 0, 3, false)
|
||||
}
|
||||
|
||||
func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
unittest.AssertCountByCond(t, "review", builder.Eq{
|
||||
"issue_id": issueID,
|
||||
"reviewer_id": reviewerID,
|
||||
"dismissed": true,
|
||||
}, expectedDismissed)
|
||||
|
||||
unittest.AssertCountByCond(t, "review", builder.Eq{
|
||||
"issue_id": issueID,
|
||||
"reviewer_id": reviewerID,
|
||||
}, expectedTotal)
|
||||
|
||||
unittest.AssertCountByCond(t, "review", builder.Eq{
|
||||
"issue_id": issueID,
|
||||
"reviewer_id": reviewerID,
|
||||
"type": issues_model.ReviewTypeRequest,
|
||||
}, expectedRequested)
|
||||
|
||||
approvalCount := 0
|
||||
if expectApproval {
|
||||
approvalCount = 1
|
||||
}
|
||||
unittest.AssertCountByCond(t, "review", builder.Eq{
|
||||
"issue_id": issueID,
|
||||
"reviewer_id": reviewerID,
|
||||
"type": issues_model.ReviewTypeApprove,
|
||||
"dismissed": false,
|
||||
}, approvalCount)
|
||||
})
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
--border-radius-circle: 50%;
|
||||
--opacity-disabled: 0.55;
|
||||
--height-loading: 16rem;
|
||||
--min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
|
||||
--tab-size: 4;
|
||||
}
|
||||
|
||||
@ -492,6 +493,11 @@ ol.ui.list li,
|
||||
background: var(--color-active) !important;
|
||||
}
|
||||
|
||||
.ui.form textarea:not([rows]) {
|
||||
height: var(--min-height-textarea); /* override fomantic default 12em */
|
||||
min-height: var(--min-height-textarea); /* override fomantic default 8em */
|
||||
}
|
||||
|
||||
/* styles from removed fomantic transition module */
|
||||
.hidden.transition {
|
||||
visibility: hidden;
|
||||
|
@ -37,13 +37,12 @@
|
||||
.combo-markdown-editor textarea.markdown-text-editor {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: calc(100vh - 200px);
|
||||
max-height: calc(100vh - var(--min-height-textarea));
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.combo-markdown-editor .CodeMirror-scroll {
|
||||
max-height: calc(100vh - 200px);
|
||||
max-height: calc(100vh - var(--min-height-textarea));
|
||||
}
|
||||
|
||||
/* use the same styles as markup/content.css */
|
||||
|
@ -1498,12 +1498,6 @@
|
||||
background: var(--color-body);
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.repository .diff-detail-box {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.repository .diff-detail-box {
|
||||
flex-wrap: wrap;
|
||||
@ -1528,7 +1522,7 @@
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
@media (max-width: 800px) {
|
||||
.repository .diff-detail-box .diff-detail-stats {
|
||||
display: none !important;
|
||||
}
|
||||
@ -1538,7 +1532,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
@ -1548,15 +1541,6 @@
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.repository .diff-detail-box .diff-detail-actions {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
.repository .diff-detail-box .diff-detail-actions .ui.button:not(.btn-submit) {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.repository .diff-detail-box span.status {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
|
@ -5,7 +5,7 @@ import {createApp} from 'vue';
|
||||
import {toggleElem} from '../utils/dom.js';
|
||||
import {getCurrentLocale} from '../utils.js';
|
||||
import {renderAnsi} from '../render/ansi.js';
|
||||
import {POST} from '../modules/fetch.js';
|
||||
import {POST, DELETE} from '../modules/fetch.js';
|
||||
|
||||
const sfc = {
|
||||
name: 'RepoActionView',
|
||||
@ -200,6 +200,12 @@ const sfc = {
|
||||
return await resp.json();
|
||||
},
|
||||
|
||||
async deleteArtifact(name) {
|
||||
if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return;
|
||||
await DELETE(`${this.run.link}/artifacts/${name}`);
|
||||
await this.loadJob();
|
||||
},
|
||||
|
||||
async fetchJob() {
|
||||
const logCursors = this.currentJobStepsStates.map((it, idx) => {
|
||||
// cursor is used to indicate the last position of the logs
|
||||
@ -329,6 +335,8 @@ export function initRepositoryActionView() {
|
||||
cancel: el.getAttribute('data-locale-cancel'),
|
||||
rerun: el.getAttribute('data-locale-rerun'),
|
||||
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
|
||||
areYouSure: el.getAttribute('data-locale-are-you-sure'),
|
||||
confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
|
||||
rerun_all: el.getAttribute('data-locale-rerun-all'),
|
||||
showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
|
||||
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
|
||||
@ -404,6 +412,9 @@ export function initRepositoryActionView() {
|
||||
<a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
|
||||
<SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }}
|
||||
</a>
|
||||
<a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete">
|
||||
<SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -528,6 +539,8 @@ export function initRepositoryActionView() {
|
||||
.job-artifacts-item {
|
||||
margin: 5px 0;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.job-artifacts-list {
|
||||
|
@ -40,7 +40,7 @@ export function initCommonIssueListQuickGoto() {
|
||||
$form.on('submit', (e) => {
|
||||
// if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
|
||||
let doQuickGoto = !isElemHidden($goto);
|
||||
const submitter = submitEventSubmitter(e.originalEvent);
|
||||
const submitter = submitEventSubmitter(e);
|
||||
if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
|
||||
if (!doQuickGoto) return;
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {POST} from '../../modules/fetch.js';
|
||||
import {imageInfo} from '../../utils/image.js';
|
||||
|
||||
async function uploadFile(file, uploadUrl) {
|
||||
const formData = new FormData();
|
||||
@ -109,10 +111,22 @@ const uploadClipboardImage = async (editor, dropzone, e) => {
|
||||
|
||||
const placeholder = `![${name}](uploading ...)`;
|
||||
editor.insertPlaceholder(placeholder);
|
||||
const data = await uploadFile(img, uploadUrl);
|
||||
editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);
|
||||
|
||||
const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
|
||||
const {uuid} = await uploadFile(img, uploadUrl);
|
||||
const {width, dppx} = await imageInfo(img);
|
||||
|
||||
const url = `/attachments/${uuid}`;
|
||||
let text;
|
||||
if (width > 0 && dppx > 1) {
|
||||
// Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
|
||||
// method to change image size in Markdown that is supported by all implementations.
|
||||
text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
|
||||
} else {
|
||||
text = `![${name}](${url})`;
|
||||
}
|
||||
editor.replacePlaceholder(placeholder, text);
|
||||
|
||||
const $input = $(`<input name="files" type="hidden">`).attr('id', uuid).val(uuid);
|
||||
$files.append($input);
|
||||
}
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ function initRepoDiffConversationForm() {
|
||||
const formData = new FormData($form[0]);
|
||||
|
||||
// if the form is submitted by a button, append the button's name and value to the form data
|
||||
const submitter = submitEventSubmitter(e.originalEvent);
|
||||
const submitter = submitEventSubmitter(e);
|
||||
const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
|
||||
if (isSubmittedByButton && submitter.name) {
|
||||
formData.append(submitter.name, submitter.value);
|
||||
|
@ -1,18 +1,17 @@
|
||||
import $ from 'jquery';
|
||||
import {hideElem, showElem} from '../utils/dom.js';
|
||||
import {GET, POST} from '../modules/fetch.js';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export function initRepoMigrationStatusChecker() {
|
||||
const $repoMigrating = $('#repo_migrating');
|
||||
if (!$repoMigrating.length) return;
|
||||
const repoMigrating = document.getElementById('repo_migrating');
|
||||
if (!repoMigrating) return;
|
||||
|
||||
$('#repo_migrating_retry').on('click', doMigrationRetry);
|
||||
document.getElementById('repo_migrating_retry').addEventListener('click', doMigrationRetry);
|
||||
|
||||
const task = $repoMigrating.attr('data-migrating-task-id');
|
||||
const task = repoMigrating.getAttribute('data-migrating-task-id');
|
||||
|
||||
// returns true if the refresh still need to be called after a while
|
||||
// returns true if the refresh still needs to be called after a while
|
||||
const refresh = async () => {
|
||||
const res = await GET(`${appSubUrl}/user/task/${task}`);
|
||||
if (res.status !== 200) return true; // continue to refresh if network error occurs
|
||||
@ -21,7 +20,7 @@ export function initRepoMigrationStatusChecker() {
|
||||
|
||||
// for all status
|
||||
if (data.message) {
|
||||
$('#repo_migrating_progress_message').text(data.message);
|
||||
document.getElementById('repo_migrating_progress_message').textContent = data.message;
|
||||
}
|
||||
|
||||
// TaskStatusFinished
|
||||
@ -37,7 +36,7 @@ export function initRepoMigrationStatusChecker() {
|
||||
showElem('#repo_migrating_retry');
|
||||
showElem('#repo_migrating_failed');
|
||||
showElem('#repo_migrating_failed_image');
|
||||
$('#repo_migrating_failed_error').text(data.message);
|
||||
document.getElementById('repo_migrating_failed_error').textContent = data.message;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -59,6 +58,6 @@ export function initRepoMigrationStatusChecker() {
|
||||
}
|
||||
|
||||
async function doMigrationRetry(e) {
|
||||
await POST($(e.target).attr('data-migrating-task-retry-url'));
|
||||
await POST(e.target.getAttribute('data-migrating-task-retry-url'));
|
||||
window.location.reload();
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
import $ from 'jquery';
|
||||
import {hideElem, showElem} from '../utils/dom.js';
|
||||
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
|
||||
|
||||
export function initRepoRelease() {
|
||||
$(document).on('click', '.remove-rel-attach', function() {
|
||||
const uuid = $(this).data('uuid');
|
||||
const id = $(this).data('id');
|
||||
$(`input[name='attachment-del-${uuid}']`).attr('value', true);
|
||||
hideElem($(`#attachment-${id}`));
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.remove-rel-attach')) {
|
||||
const uuid = e.target.getAttribute('data-uuid');
|
||||
const id = e.target.getAttribute('data-id');
|
||||
document.querySelector(`input[name='attachment-del-${uuid}']`).value = 'true';
|
||||
hideElem(`#attachment-${id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initRepoReleaseNew() {
|
||||
const $repoReleaseNew = $('.repository.new.release');
|
||||
if (!$repoReleaseNew.length) return;
|
||||
if (!document.querySelector('.repository.new.release')) return;
|
||||
|
||||
initTagNameEditor();
|
||||
initRepoReleaseEditor();
|
||||
@ -45,9 +45,9 @@ function initTagNameEditor() {
|
||||
}
|
||||
|
||||
function initRepoReleaseEditor() {
|
||||
const $editor = $('.repository.new.release .combo-markdown-editor');
|
||||
if ($editor.length === 0) {
|
||||
const editor = document.querySelector('.repository.new.release .combo-markdown-editor');
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const _promise = initComboMarkdownEditor($editor);
|
||||
initComboMarkdownEditor(editor);
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {checkAppUrl} from './common-global.js';
|
||||
|
||||
export function initUserAuthOauth2() {
|
||||
@ -21,30 +20,3 @@ export function initUserAuthOauth2() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initUserAuthLinkAccountView() {
|
||||
const $lnkUserPage = $('.page-content.user.link-account');
|
||||
if ($lnkUserPage.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const $signinTab = $lnkUserPage.find('.item[data-tab="auth-link-signin-tab"]');
|
||||
const $signUpTab = $lnkUserPage.find('.item[data-tab="auth-link-signup-tab"]');
|
||||
const $signInView = $lnkUserPage.find('.tab[data-tab="auth-link-signin-tab"]');
|
||||
const $signUpView = $lnkUserPage.find('.tab[data-tab="auth-link-signup-tab"]');
|
||||
|
||||
$signUpTab.on('click', () => {
|
||||
$signinTab.removeClass('active');
|
||||
$signInView.removeClass('active');
|
||||
$signUpTab.addClass('active');
|
||||
$signUpView.addClass('active');
|
||||
return false;
|
||||
});
|
||||
|
||||
$signinTab.on('click', () => {
|
||||
$signUpTab.removeClass('active');
|
||||
$signUpView.removeClass('active');
|
||||
$signinTab.addClass('active');
|
||||
$signInView.addClass('active');
|
||||
});
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js';
|
||||
import {initCommentContent, initMarkupContent} from './markup/content.js';
|
||||
import {initPdfViewer} from './render/pdf.js';
|
||||
|
||||
import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js';
|
||||
import {initUserAuthOauth2} from './features/user-auth.js';
|
||||
import {
|
||||
initRepoIssueDue,
|
||||
initRepoIssueReferenceRepositorySearch,
|
||||
@ -178,7 +178,6 @@ onDomReady(() => {
|
||||
initCommitStatuses();
|
||||
initCaptcha();
|
||||
|
||||
initUserAuthLinkAccountView();
|
||||
initUserAuthOauth2();
|
||||
initUserAuthWebAuthn();
|
||||
initUserAuthWebAuthnRegister();
|
||||
|
@ -67,6 +67,7 @@ import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethro
|
||||
import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
|
||||
import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
|
||||
import octiconTag from '../../public/assets/img/svg/octicon-tag.svg';
|
||||
import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
|
||||
import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
|
||||
import octiconX from '../../public/assets/img/svg/octicon-x.svg';
|
||||
import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
|
||||
@ -139,6 +140,7 @@ const svgs = {
|
||||
'octicon-sync': octiconSync,
|
||||
'octicon-table': octiconTable,
|
||||
'octicon-tag': octiconTag,
|
||||
'octicon-trash': octiconTrash,
|
||||
'octicon-triangle-down': octiconTriangleDown,
|
||||
'octicon-x': octiconX,
|
||||
'octicon-x-circle-fill': octiconXCircleFill,
|
||||
|
@ -211,6 +211,7 @@ export function loadElem(el, src) {
|
||||
const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
|
||||
|
||||
export function submitEventSubmitter(e) {
|
||||
e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself
|
||||
return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
|
||||
}
|
||||
|
||||
|
47
web_src/js/utils/image.js
Normal file
47
web_src/js/utils/image.js
Normal file
@ -0,0 +1,47 @@
|
||||
export async function pngChunks(blob) {
|
||||
const uint8arr = new Uint8Array(await blob.arrayBuffer());
|
||||
const chunks = [];
|
||||
if (uint8arr.length < 12) return chunks;
|
||||
const view = new DataView(uint8arr.buffer);
|
||||
if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let index = 8;
|
||||
while (index < uint8arr.length) {
|
||||
const len = view.getUint32(index);
|
||||
chunks.push({
|
||||
name: decoder.decode(uint8arr.slice(index + 4, index + 8)),
|
||||
data: uint8arr.slice(index + 8, index + 8 + len),
|
||||
});
|
||||
index += len + 12;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// decode a image and try to obtain width and dppx. If will never throw but instead
|
||||
// return default values.
|
||||
export async function imageInfo(blob) {
|
||||
let width = 0; // 0 means no width could be determined
|
||||
let dppx = 1; // 1 dot per pixel for non-HiDPI screens
|
||||
|
||||
if (blob.type === 'image/png') { // only png is supported currently
|
||||
try {
|
||||
for (const {name, data} of await pngChunks(blob)) {
|
||||
const view = new DataView(data.buffer);
|
||||
if (name === 'IHDR' && data?.length) {
|
||||
// extract width from mandatory IHDR chunk
|
||||
width = view.getUint32(0);
|
||||
} else if (name === 'pHYs' && data?.length) {
|
||||
// extract dppx from optional pHYs chunk, assuming pixels are square
|
||||
const unit = view.getUint8(8);
|
||||
if (unit === 1) {
|
||||
dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {width, dppx};
|
||||
}
|
29
web_src/js/utils/image.test.js
Normal file
29
web_src/js/utils/image.test.js
Normal file
@ -0,0 +1,29 @@
|
||||
import {pngChunks, imageInfo} from './image.js';
|
||||
|
||||
const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==';
|
||||
const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==';
|
||||
const pngEmpty = 'data:image/png;base64,';
|
||||
|
||||
async function dataUriToBlob(datauri) {
|
||||
return await (await globalThis.fetch(datauri)).blob();
|
||||
}
|
||||
|
||||
test('pngChunks', async () => {
|
||||
expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([
|
||||
{name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])},
|
||||
{name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])},
|
||||
{name: 'IEND', data: new Uint8Array([])},
|
||||
]);
|
||||
expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([
|
||||
{name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])},
|
||||
{name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])},
|
||||
{name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])},
|
||||
]);
|
||||
expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]);
|
||||
});
|
||||
|
||||
test('imageInfo', async () => {
|
||||
expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1});
|
||||
expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2});
|
||||
expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user