1
1
Fork 1
mirror of https://github.com/go-gitea/gitea.git synced 2024-05-30 04:06:10 +02:00

Merge branch 'main' into issue-updates

This commit is contained in:
Anbraten 2024-02-19 23:22:31 +01:00 committed by GitHub
commit b5f61df0a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
94 changed files with 1204 additions and 431 deletions

View File

@ -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\'

View File

@ -95,12 +95,6 @@ Gitea Actions目前不支持此功能如果使用它结果将始终为空
## 缺失的功能
### 变量
请参阅[变量](https://docs.github.com/zh/actions/learn-github-actions/variables)。
目前变量功能正在开发中。
### 问题匹配器
问题匹配器是一种扫描Actions输出以查找指定正则表达式模式并在用户界面中突出显示该信息的方法。

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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:

View File

@ -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 {

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View 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)
}

View File

@ -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:

View File

@ -52,3 +52,7 @@ func TestSubjectBodySeparator(t *testing.T) {
"",
"Insuficient\n--\nSeparators")
}
func TestJSEscapeSafe(t *testing.T) {
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
}

View File

@ -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>&lt;str&gt;</a>"},
{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
{template.HTML("<html>"), "<a><html></a>"},
{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{&lt;stringerPointerReceiver&gt;}</a>"},
{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a>&lt;stringerPointerReceiver ptr&gt;</a>"},
{stringerStructReceiver{"<stringerStructReceiver>"}, "<a>&lt;stringerStructReceiver&gt;</a>"},
{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a>&lt;stringerStructReceiver ptr&gt;</a>"},
{errorStructReceiver{"<errorStructReceiver>"}, "<a>&lt;errorStructReceiver&gt;</a>"},
{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a>&lt;errorStructReceiver ptr&gt;</a>"},
{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{&lt;errorPointerReceiver&gt;}</a>"},
{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a>&lt;errorPointerReceiver ptr&gt;</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 {

View File

@ -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...))

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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
View 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.

View 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.

View 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
View 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.

View 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.

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"`

View File

@ -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)
}

View File

@ -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)

View File

@ -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")
}

View File

@ -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)

View File

@ -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

View File

@ -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"]

View File

@ -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
}

View File

@ -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
}

View File

@ -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")

View File

@ -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
}

View File

@ -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) {

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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}}

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"}}"

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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 */}}

View File

@ -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>

View File

@ -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">

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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": {

View File

@ -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"

View File

@ -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>

View File

@ -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)
})
}

View File

@ -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;

View File

@ -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 */

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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);

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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');
});
}

View File

@ -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();

View File

@ -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,

View File

@ -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
View 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};
}

View 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});
});