Merge branch 'main' into diffenh1

This commit is contained in:
Giteabot 2024-04-27 03:10:53 +08:00 committed by GitHub
commit 056ed23f67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 913 additions and 611 deletions

View File

@ -1558,8 +1558,8 @@ LEVEL = Info
;; email = use the username part of the email attribute ;; email = use the username part of the email attribute
;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: ;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
;; - diacritics are removed ;; - diacritics are removed
;; - the characters in the set `['´\x60]` are removed ;; - the characters in the set ['´`] are removed
;; - the characters in the set `[\s~+]` are replaced with `-` ;; - the characters in the set [\s~+] are replaced with "-"
;USERNAME = nickname ;USERNAME = nickname
;; ;;
;; Update avatar if available from oauth2 provider. ;; Update avatar if available from oauth2 provider.

View File

@ -612,7 +612,7 @@ And the following unique queues:
- `email` - use the username part of the email attribute - `email` - use the username part of the email attribute
- Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: - Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
- diacritics are removed - diacritics are removed
- the characters in the set `['´\x60]` are removed - the characters in the set ```['´`]``` are removed
- the characters in the set `[\s~+]` are replaced with `-` - the characters in the set `[\s~+]` are replaced with `-`
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login. - `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
- `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists: - `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists:

View File

@ -74,6 +74,13 @@ func (run *ActionRun) Link() string {
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index)
} }
func (run *ActionRun) WorkflowLink() string {
if run.Repo == nil {
return ""
}
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
}
// RefLink return the url of run's ref // RefLink return the url of run's ref
func (run *ActionRun) RefLink() string { func (run *ActionRun) RefLink() string {
refName := git.RefName(run.Ref) refName := git.RefName(run.Ref)
@ -156,6 +163,10 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err
return nil, fmt.Errorf("event %s is not a pull request event", run.Event) return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
} }
func (run *ActionRun) IsSchedule() bool {
return run.ScheduleID > 0
}
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID). _, err := db.GetEngine(ctx).ID(repo.ID).
SetExpr("num_action_runs", SetExpr("num_action_runs",

View File

@ -43,11 +43,6 @@ func RemigrateU2FCredentials(x *xorm.Engine) error {
if err != nil { if err != nil {
return err return err
} }
case schemas.ORACLE:
_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY credential_id VARCHAR(410)")
if err != nil {
return err
}
case schemas.MSSQL: case schemas.MSSQL:
// This column has an index on it. I could write all of the code to attempt to change the index OR // This column has an index on it. I could write all of the code to attempt to change the index OR
// I could just use recreate table. // I could just use recreate table.

View File

@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
if setting.Database.Type.IsMySQL() { if setting.Database.Type.IsMySQL() {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
} else if setting.Database.Type.IsMSSQL() { } else if setting.Database.Type.IsMSSQL() {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1])) _, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] NVARCHAR(64)", alts[0], alts[1]))
} else { } else {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1]))
} }

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/setting/config"
@ -106,6 +107,7 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
fatalTestError("Error creating test engine: %v\n", err) fatalTestError("Error creating test engine: %v\n", err)
} }
setting.IsInTesting = true
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
setting.RunUser = "runuser" setting.RunUser = "runuser"
setting.SSH.User = "sshuser" setting.SSH.User = "sshuser"
@ -148,6 +150,9 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
config.SetDynGetter(system.NewDatabaseDynKeyGetter()) config.SetDynGetter(system.NewDatabaseDynKeyGetter())
if err = cache.Init(); err != nil {
fatalTestError("cache.Init: %v\n", err)
}
if err = storage.Init(); err != nil { if err = storage.Init(); err != nil {
fatalTestError("storage.Init: %v\n", err) fatalTestError("storage.Init: %v\n", err)
} }

View File

@ -501,19 +501,19 @@ func GetUserSalt() (string, error) {
// Note: The set of characters here can safely expand without a breaking change, // Note: The set of characters here can safely expand without a breaking change,
// but characters removed from this set can cause user account linking to break // but characters removed from this set can cause user account linking to break
var ( var (
customCharsReplacement = strings.NewReplacer("Æ", "AE") customCharsReplacement = strings.NewReplacer("Æ", "AE")
removeCharsRE = regexp.MustCompile(`['´\x60]`) removeCharsRE = regexp.MustCompile("['`´]")
removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`) replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
) )
// normalizeUserName returns a string with single-quotes and diacritics // NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
// removed, and any other non-supported username characters replaced with // It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
// a `-` character
func NormalizeUserName(s string) (string, error) { func NormalizeUserName(s string) (string, error) {
strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s)) s, _, _ = strings.Cut(s, "@")
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s) return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
} }
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
} }

View File

@ -506,15 +506,16 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
Expected string Expected string
IsNormalizedValid bool IsNormalizedValid bool
}{ }{
{"test", "test", true}, {"name@example.com", "name", true},
{"test'`´name", "testname", true},
{"Sinéad.O'Connor", "Sinead.OConnor", true}, {"Sinéad.O'Connor", "Sinead.OConnor", true},
{"Æsir", "AEsir", true}, {"Æsir", "AEsir", true},
// \u00e9\u0065\u0301 {"éé", "ee", true}, // \u00e9\u0065\u0301
{"éé", "ee", true},
{"Awareness Hub", "Awareness-Hub", true}, {"Awareness Hub", "Awareness-Hub", true},
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters {"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
{".bad.", ".bad.", false}, {".bad.", ".bad.", false},
{"new😀user", "new😀user", false}, // No plans to support {"new😀user", "new😀user", false}, // No plans to support
{`"quoted"`, `"quoted"`, false}, // No plans to support
} }
for _, testCase := range testCases { for _, testCase := range testCases {
normalizedName, err := user_model.NormalizeUserName(testCase.Input) normalizedName, err := user_model.NormalizeUserName(testCase.Input)

View File

@ -33,7 +33,6 @@ type ObjectFormat interface {
ComputeHash(t ObjectType, content []byte) ObjectID ComputeHash(t ObjectType, content []byte) ObjectID
} }
/* SHA1 Type */
type Sha1ObjectFormatImpl struct{} type Sha1ObjectFormatImpl struct{}
var ( var (
@ -70,14 +69,10 @@ func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID
_, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(" "))
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) _, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
_, _ = hasher.Write([]byte{0}) _, _ = hasher.Write([]byte{0})
_, _ = hasher.Write(content)
// HashSum generates a SHA1 for the provided hash return h.MustID(hasher.Sum(nil))
var sha1 Sha1Hash
copy(sha1[:], hasher.Sum(nil))
return &sha1
} }
/* SHA256 Type */
type Sha256ObjectFormatImpl struct{} type Sha256ObjectFormatImpl struct{}
var ( var (
@ -116,11 +111,8 @@ func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) Object
_, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(" "))
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) _, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
_, _ = hasher.Write([]byte{0}) _, _ = hasher.Write([]byte{0})
_, _ = hasher.Write(content)
// HashSum generates a SHA256 for the provided hash return h.MustID(hasher.Sum(nil))
var sha256 Sha1Hash
copy(sha256[:], hasher.Sum(nil))
return &sha256
} }
var ( var (

View File

@ -16,7 +16,6 @@ type ObjectID interface {
Type() ObjectFormat Type() ObjectFormat
} }
/* SHA1 */
type Sha1Hash [20]byte type Sha1Hash [20]byte
func (h *Sha1Hash) String() string { func (h *Sha1Hash) String() string {
@ -40,7 +39,6 @@ func MustIDFromString(hexHash string) ObjectID {
return id return id
} }
/* SHA256 */
type Sha256Hash [32]byte type Sha256Hash [32]byte
func (h *Sha256Hash) String() string { func (h *Sha256Hash) String() string {
@ -54,7 +52,6 @@ func (h *Sha256Hash) IsZero() bool {
func (h *Sha256Hash) RawValue() []byte { return h[:] } func (h *Sha256Hash) RawValue() []byte { return h[:] }
func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat } func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat }
/* utility */
func NewIDFromString(hexHash string) (ObjectID, error) { func NewIDFromString(hexHash string) (ObjectID, error) {
var theObjectFormat ObjectFormat var theObjectFormat ObjectFormat
for _, objectFormat := range SupportedObjectFormats { for _, objectFormat := range SupportedObjectFormats {

View File

@ -18,4 +18,8 @@ func TestIsValidSHAPattern(t *testing.T) {
assert.False(t, h.IsValid("abc")) assert.False(t, h.IsValid("abc"))
assert.False(t, h.IsValid("123g")) assert.False(t, h.IsValid("123g"))
assert.False(t, h.IsValid("some random text")) assert.False(t, h.IsValid("some random text"))
assert.Equal(t, "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", ComputeBlobHash(Sha1ObjectFormat, nil).String())
assert.Equal(t, "2e65efe2a145dda7ee51d1741299f848e5bf752e", ComputeBlobHash(Sha1ObjectFormat, []byte("a")).String())
assert.Equal(t, "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813", ComputeBlobHash(Sha256ObjectFormat, nil).String())
assert.Equal(t, "eb337bcee2061c5313c9a1392116b6c76039e9e30d71467ae359b36277e17dc7", ComputeBlobHash(Sha256ObjectFormat, []byte("a")).String())
} }

View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pipeline
import (
"fmt"
"time"
"code.gitea.io/gitea/modules/git"
)
// LFSResult represents commits found using a provided pointer file hash
type LFSResult struct {
Name string
SHA string
Summary string
When time.Time
ParentHashes []git.ObjectID
BranchName string
FullCommitName string
}
type lfsResultSlice []*LFSResult
func (a lfsResultSlice) Len() int { return len(a) }
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
func lfsError(msg string, err error) error {
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
}

View File

@ -7,12 +7,10 @@ package pipeline
import ( import (
"bufio" "bufio"
"fmt"
"io" "io"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -21,23 +19,6 @@ import (
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
// LFSResult represents commits found using a provided pointer file hash
type LFSResult struct {
Name string
SHA string
Summary string
When time.Time
ParentHashes []git.ObjectID
BranchName string
FullCommitName string
}
type lfsResultSlice []*LFSResult
func (a lfsResultSlice) Len() int { return len(a) }
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
// FindLFSFile finds commits that contain a provided pointer file hash // FindLFSFile finds commits that contain a provided pointer file hash
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
resultsMap := map[string]*LFSResult{} resultsMap := map[string]*LFSResult{}
@ -51,7 +32,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
All: true, All: true,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err) return nil, lfsError("failed to get GoGit CommitsIter", err)
} }
err = commitsIter.ForEach(func(gitCommit *object.Commit) error { err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
@ -85,7 +66,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
return nil return nil
}) })
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err) return nil, lfsError("failure in CommitIter.ForEach", err)
} }
for _, result := range resultsMap { for _, result := range resultsMap {
@ -156,7 +137,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
select { select {
case err, has := <-errChan: case err, has := <-errChan:
if has { if has {
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err) return nil, lfsError("unable to obtain name for LFS files", err)
} }
default: default:
} }

View File

@ -8,33 +8,14 @@ package pipeline
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"io" "io"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
) )
// LFSResult represents commits found using a provided pointer file hash
type LFSResult struct {
Name string
SHA string
Summary string
When time.Time
ParentIDs []git.ObjectID
BranchName string
FullCommitName string
}
type lfsResultSlice []*LFSResult
func (a lfsResultSlice) Len() int { return len(a) }
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
// FindLFSFile finds commits that contain a provided pointer file hash // FindLFSFile finds commits that contain a provided pointer file hash
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
resultsMap := map[string]*LFSResult{} resultsMap := map[string]*LFSResult{}
@ -137,11 +118,11 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
n += int64(count) n += int64(count)
if bytes.Equal(binObjectID, objectID.RawValue()) { if bytes.Equal(binObjectID, objectID.RawValue()) {
result := LFSResult{ result := LFSResult{
Name: curPath + string(fname), Name: curPath + string(fname),
SHA: curCommit.ID.String(), SHA: curCommit.ID.String(),
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0], Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
When: curCommit.Author.When, When: curCommit.Author.When,
ParentIDs: curCommit.Parents, ParentHashes: curCommit.Parents,
} }
resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
} else if string(mode) == git.EntryModeTree.String() { } else if string(mode) == git.EntryModeTree.String() {
@ -183,7 +164,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
for _, result := range resultsMap { for _, result := range resultsMap {
hasParent := false hasParent := false
for _, parentID := range result.ParentIDs { for _, parentID := range result.ParentHashes {
if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent { if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent {
break break
} }
@ -240,7 +221,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
select { select {
case err, has := <-errChan: case err, has := <-errChan:
if has { if has {
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err) return nil, lfsError("unable to obtain name for LFS files", err)
} }
default: default:
} }

26
modules/session/mock.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package session
import (
"net/http"
"gitea.com/go-chi/session"
)
type MockStore struct {
*session.MemStore
}
func (m *MockStore) Destroy(writer http.ResponseWriter, request *http.Request) error {
return nil
}
type mockStoreContextKeyStruct struct{}
var MockStoreContextKey = mockStoreContextKeyStruct{}
func NewMockStore(sid string) *MockStore {
return &MockStore{session.NewMemStore(sid)}
}

View File

@ -6,6 +6,8 @@ package session
import ( import (
"net/http" "net/http"
"code.gitea.io/gitea/modules/setting"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
) )
@ -14,6 +16,10 @@ type Store interface {
Get(any) any Get(any) any
Set(any, any) error Set(any, any) error
Delete(any) error Delete(any) error
ID() string
Release() error
Flush() error
Destroy(http.ResponseWriter, *http.Request) error
} }
// RegenerateSession regenerates the underlying session and returns the new store // RegenerateSession regenerates the underlying session and returns the new store
@ -21,8 +27,21 @@ func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, erro
for _, f := range BeforeRegenerateSession { for _, f := range BeforeRegenerateSession {
f(resp, req) f(resp, req)
} }
s, err := session.RegenerateSession(resp, req) if setting.IsInTesting {
return s, err if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
return store, nil
}
}
return session.RegenerateSession(resp, req)
}
func GetContextSession(req *http.Request) Store {
if setting.IsInTesting {
if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
return store
}
}
return session.GetSession(req)
} }
// BeforeRegenerateSession is a list of functions that are called before a session is regenerated. // BeforeRegenerateSession is a list of functions that are called before a session is regenerated.

View File

@ -16,14 +16,10 @@ import (
type OAuth2UsernameType string type OAuth2UsernameType string
const ( const (
// OAuth2UsernameUserid oauth2 userid field will be used as gitea name OAuth2UsernameUserid OAuth2UsernameType = "userid" // use user id (sub) field as gitea's username
OAuth2UsernameUserid OAuth2UsernameType = "userid" OAuth2UsernameNickname OAuth2UsernameType = "nickname" // use nickname field
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name OAuth2UsernameEmail OAuth2UsernameType = "email" // use email field
OAuth2UsernameNickname OAuth2UsernameType = "nickname" OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" // use preferred_username field
// OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
OAuth2UsernameEmail OAuth2UsernameType = "email"
// OAuth2UsernameEmail username of oauth2 preferred_username field will be used as gitea name
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username"
) )
func (username OAuth2UsernameType) isValid() bool { func (username OAuth2UsernameType) isValid() bool {
@ -71,8 +67,8 @@ func loadOAuth2ClientFrom(rootCfg ConfigProvider) {
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool() OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname))) OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
if !OAuth2Client.Username.isValid() { if !OAuth2Client.Username.isValid() {
log.Warn("Username setting is not valid: '%s', will fallback to '%s'", OAuth2Client.Username, OAuth2UsernameNickname)
OAuth2Client.Username = OAuth2UsernameNickname OAuth2Client.Username = OAuth2UsernameNickname
log.Warn("[oauth2_client].USERNAME setting is invalid, falls back to %q", OAuth2Client.Username)
} }
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool() OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin))) OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin)))

View File

@ -436,6 +436,7 @@ oauth_signin_submit = Link Account
oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator. oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator.
oauth.signin.error.access_denied = The authorization request was denied. oauth.signin.error.access_denied = The authorization request was denied.
oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later. oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later.
oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically, please create or link to an account, or contact the site administrator.
openid_connect_submit = Connect openid_connect_submit = Connect
openid_connect_title = Connect to an existing account openid_connect_title = Connect to an existing account
openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here. openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here.

View File

@ -763,6 +763,8 @@ manage_themes=Escolher o tema padrão
manage_openid=Gerir endereços OpenID manage_openid=Gerir endereços OpenID
email_desc=O seu endereço de email principal irá ser usado para notificações, recuperação de senha e, desde que não esteja oculto, operações Git baseados na web. email_desc=O seu endereço de email principal irá ser usado para notificações, recuperação de senha e, desde que não esteja oculto, operações Git baseados na web.
theme_desc=Este será o seu tema padrão em todo o sítio. theme_desc=Este será o seu tema padrão em todo o sítio.
theme_colorblindness_help=Suporte a temas para daltónicos
theme_colorblindness_prompt=O Gitea acabou de obter alguns temas com suporte básico para daltónicos que têm apenas algumas cores definidas. O trabalho ainda está em andamento. Poderiam ser feitos mais melhoramentos se fossem definidas mais cores nos ficheiros CSS do tema.
primary=Principal primary=Principal
activated=Em uso activated=Em uso
requires_activation=Tem que ser habilitado requires_activation=Tem que ser habilitado
@ -2356,7 +2358,7 @@ settings.protected_branch.delete_rule=Eliminar regra
settings.protected_branch_can_push=Permitir envios? settings.protected_branch_can_push=Permitir envios?
settings.protected_branch_can_push_yes=Pode enviar settings.protected_branch_can_push_yes=Pode enviar
settings.protected_branch_can_push_no=Não pode enviar settings.protected_branch_can_push_no=Não pode enviar
settings.branch_protection=Salvaguarda do ramo '<b>%s</b>' settings.branch_protection=Regras de salvaguarda do ramo '<b>%s</b>'
settings.protect_this_branch=Habilitar salvaguarda do ramo settings.protect_this_branch=Habilitar salvaguarda do ramo
settings.protect_this_branch_desc=Impede a eliminação e restringe envios e integrações do Git no ramo. settings.protect_this_branch_desc=Impede a eliminação e restringe envios e integrações do Git no ramo.
settings.protect_disable_push=Desabilitar envios settings.protect_disable_push=Desabilitar envios
@ -2400,7 +2402,7 @@ settings.protect_patterns=Padrões
settings.protect_protected_file_patterns=Padrões de ficheiros protegidos (separados com ponto e vírgula ';'): settings.protect_protected_file_patterns=Padrões de ficheiros protegidos (separados com ponto e vírgula ';'):
settings.protect_protected_file_patterns_desc=Ficheiros protegidos não podem ser modificados imediatamente, mesmo que o utilizador tenha direitos para adicionar, editar ou eliminar ficheiros neste ramo. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.protect_protected_file_patterns_desc=Ficheiros protegidos não podem ser modificados imediatamente, mesmo que o utilizador tenha direitos para adicionar, editar ou eliminar ficheiros neste ramo. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
settings.protect_unprotected_file_patterns=Padrões de ficheiros desprotegidos (separados com ponto e vírgula ';'): settings.protect_unprotected_file_patterns=Padrões de ficheiros desprotegidos (separados com ponto e vírgula ';'):
settings.protect_unprotected_file_patterns_desc=Ficheiros desprotegidos que podem ser modificados imediatamente se o utilizador tiver direitos de escrita, contornando a restrição no envio. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.protect_unprotected_file_patterns_desc=Ficheiros desprotegidos que podem ser modificados imediatamente se o utilizador tiver direitos de escrita, contornando a restrição no envio. Padrões múltiplos podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
settings.add_protected_branch=Habilitar salvaguarda settings.add_protected_branch=Habilitar salvaguarda
settings.delete_protected_branch=Desabilitar salvaguarda settings.delete_protected_branch=Desabilitar salvaguarda
settings.update_protect_branch_success=A salvaguarda do ramo "%s" foi modificada. settings.update_protect_branch_success=A salvaguarda do ramo "%s" foi modificada.
@ -2416,7 +2418,7 @@ settings.block_outdated_branch=Bloquear integração se o pedido de integração
settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base. settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base.
settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos: settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos:
settings.merge_style_desc=Estilos de integração settings.merge_style_desc=Estilos de integração
settings.default_merge_style_desc=Tipo de integração predefinido para pedidos de integração: settings.default_merge_style_desc=Tipo de integração predefinido
settings.choose_branch=Escolha um ramo… settings.choose_branch=Escolha um ramo…
settings.no_protected_branch=Não existem ramos protegidos. settings.no_protected_branch=Não existem ramos protegidos.
settings.edit_protected_branch=Editar settings.edit_protected_branch=Editar
@ -2786,7 +2788,7 @@ self_check=Auto-verificação
identity_access=Identidade e acesso identity_access=Identidade e acesso
users=Contas de utilizador users=Contas de utilizador
organizations=Organizações organizations=Organizações
assets=Recursos de código assets=Recursos do código-fonte
repositories=Repositórios repositories=Repositórios
hooks=Automatismos web hooks=Automatismos web
integrations=Integrações integrations=Integrações
@ -2867,14 +2869,14 @@ dashboard.mspan_structures_obtained=Estruturas MSpan obtidas
dashboard.mcache_structures_usage=Uso das estruturas MCache dashboard.mcache_structures_usage=Uso das estruturas MCache
dashboard.mcache_structures_obtained=Estruturas MCache obtidas dashboard.mcache_structures_obtained=Estruturas MCache obtidas
dashboard.profiling_bucket_hash_table_obtained=Perfil obtido da tabela de hash do balde dashboard.profiling_bucket_hash_table_obtained=Perfil obtido da tabela de hash do balde
dashboard.gc_metadata_obtained=Metadados da recolha de lixo obtidos dashboard.gc_metadata_obtained=Metadados obtidos da recolha de lixo
dashboard.other_system_allocation_obtained=Outras alocações de sistema obtidas dashboard.other_system_allocation_obtained=Outras alocações de sistema obtidas
dashboard.next_gc_recycle=Próxima reciclagem da recolha de lixo dashboard.next_gc_recycle=Próxima reciclagem da recolha de lixo
dashboard.last_gc_time=Tempo decorrido desde a última recolha de lixo dashboard.last_gc_time=Tempo decorrido desde a última recolha de lixo
dashboard.total_gc_time=Pausa total da recolha de lixo dashboard.total_gc_time=Pausa total da recolha de lixo
dashboard.total_gc_pause=Pausa total da recolha de lixo dashboard.total_gc_pause=Pausa total da recolha de lixo
dashboard.last_gc_pause=Última pausa da recolha de lixo dashboard.last_gc_pause=Última pausa da recolha de lixo
dashboard.gc_times=Tempos da recolha de lixo dashboard.gc_times=N.º de recolhas de lixo
dashboard.delete_old_actions=Eliminar todas as operações antigas da base de dados dashboard.delete_old_actions=Eliminar todas as operações antigas da base de dados
dashboard.delete_old_actions.started=Foi iniciado o processo de eliminação de todas as operações antigas da base de dados. dashboard.delete_old_actions.started=Foi iniciado o processo de eliminação de todas as operações antigas da base de dados.
dashboard.update_checker=Verificador de novas versões dashboard.update_checker=Verificador de novas versões
@ -3023,7 +3025,7 @@ auths.attribute_surname=Atributo do Sobrenome
auths.attribute_mail=Atributo do email auths.attribute_mail=Atributo do email
auths.attribute_ssh_public_key=Atributo da chave pública SSH auths.attribute_ssh_public_key=Atributo da chave pública SSH
auths.attribute_avatar=Atributo do avatar auths.attribute_avatar=Atributo do avatar
auths.attributes_in_bind=Buscar os atributos no contexto de Bind DN auths.attributes_in_bind=Buscar atributos no contexto do Bind DN
auths.allow_deactivate_all=Permitir que um resultado de pesquisa vazio desabilite todos os utilizadores auths.allow_deactivate_all=Permitir que um resultado de pesquisa vazio desabilite todos os utilizadores
auths.use_paged_search=Usar pesquisa paginada auths.use_paged_search=Usar pesquisa paginada
auths.search_page_size=Tamanho da página auths.search_page_size=Tamanho da página
@ -3222,7 +3224,7 @@ config.session_config=Configuração de sessão
config.session_provider=Fornecedor da sessão config.session_provider=Fornecedor da sessão
config.provider_config=Configuração do fornecedor config.provider_config=Configuração do fornecedor
config.cookie_name=Nome do cookie config.cookie_name=Nome do cookie
config.gc_interval_time=Intervalo da recolha do lixo config.gc_interval_time=Intervalo de tempo entre recolhas do lixo
config.session_life_time=Tempo de vida da sessão config.session_life_time=Tempo de vida da sessão
config.https_only=Apenas HTTPS config.https_only=Apenas HTTPS
config.cookie_life_time=Tempo de vida do cookie config.cookie_life_time=Tempo de vida do cookie

8
package-lock.json generated
View File

@ -28,7 +28,7 @@
"esbuild-loader": "4.1.0", "esbuild-loader": "4.1.0",
"escape-goat": "4.0.0", "escape-goat": "4.0.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"htmx.org": "1.9.11", "htmx.org": "1.9.12",
"idiomorph": "0.3.0", "idiomorph": "0.3.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.10", "katex": "0.16.10",
@ -6728,9 +6728,9 @@
} }
}, },
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "1.9.11", "version": "1.9.12",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz",
"integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw==" "integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw=="
}, },
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "5.0.0", "version": "5.0.0",

View File

@ -27,7 +27,7 @@
"esbuild-loader": "4.1.0", "esbuild-loader": "4.1.0",
"escape-goat": "4.0.0", "escape-goat": "4.0.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"htmx.org": "1.9.11", "htmx.org": "1.9.12",
"idiomorph": "0.3.0", "idiomorph": "0.3.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.10", "katex": "0.16.10",

View File

@ -93,6 +93,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -835,6 +836,34 @@ func Routes() *web.Route {
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInView,
})) }))
addActionsRoutes := func(
m *web.Route,
reqChecker func(ctx *context.APIContext),
act actions.API,
) {
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Get("", reqToken(), reqChecker, act.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqChecker, act.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqChecker, act.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqChecker, act.GetVariable).
Delete(reqToken(), reqChecker, act.DeleteVariable).
Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
})
})
}
m.Group("", func() { m.Group("", func() {
// Miscellaneous (no scope required) // Miscellaneous (no scope required)
if setting.API.EnableSwagger { if setting.API.EnableSwagger {
@ -1073,26 +1102,11 @@ func Routes() *web.Route {
m.Post("/accept", repo.AcceptTransfer) m.Post("/accept", repo.AcceptTransfer)
m.Post("/reject", repo.RejectTransfer) m.Post("/reject", repo.RejectTransfer)
}, reqToken()) }, reqToken())
m.Group("/actions", func() { addActionsRoutes(
m.Group("/secrets", func() { m,
m.Combo("/{secretname}"). reqOwner(),
Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret). repo.NewAction(),
Delete(reqToken(), reqOwner(), repo.DeleteSecret) )
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOwner(), repo.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOwner(), repo.GetVariable).
Delete(reqToken(), reqOwner(), repo.DeleteVariable).
Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
})
})
m.Group("/hooks/git", func() { m.Group("/hooks/git", func() {
m.Combo("").Get(repo.ListGitHooks) m.Combo("").Get(repo.ListGitHooks)
m.Group("/{id}", func() { m.Group("/{id}", func() {
@ -1460,27 +1474,11 @@ func Routes() *web.Route {
m.Combo("/{username}").Get(reqToken(), org.IsMember). m.Combo("/{username}").Get(reqToken(), org.IsMember).
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember) Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
}) })
m.Group("/actions", func() { addActionsRoutes(
m.Group("/secrets", func() { m,
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets) reqOrgOwnership(),
m.Combo("/{secretname}"). org.NewAction(),
Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret). )
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOrgOwnership(), org.GetVariable).
Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
})
})
m.Group("/public_members", func() { m.Group("/public_members", func() {
m.Get("", org.ListPublicMembers) m.Get("", org.ListPublicMembers)
m.Combo("/{username}").Get(org.IsPublicMember). m.Combo("/{username}").Get(org.IsPublicMember).

View File

@ -9,16 +9,188 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
) )
// ListActionsSecrets list an organization's actions secrets
func (Action) ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets
// ---
// summary: List an organization's actions secrets
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
opts := &secret_model.FindSecretsOptions{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the organization
func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
// ---
// summary: Create or Update a secret value in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// "204":
// description: response when updating a secret
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
}
return
}
if created {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}
// DeleteSecret delete one secret of the organization
func (Action) DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
// ---
// summary: Delete a secret in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register org runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
// ListVariables list org-level variables // ListVariables list org-level variables
func ListVariables(ctx *context.APIContext) { func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
// --- // ---
// summary: Get an org-level variables list // summary: Get an org-level variables list
@ -70,7 +242,7 @@ func ListVariables(ctx *context.APIContext) {
} }
// GetVariable get an org-level variable // GetVariable get an org-level variable
func GetVariable(ctx *context.APIContext) { func (Action) GetVariable(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
// --- // ---
// summary: Get an org-level variable // summary: Get an org-level variable
@ -119,7 +291,7 @@ func GetVariable(ctx *context.APIContext) {
} }
// DeleteVariable delete an org-level variable // DeleteVariable delete an org-level variable
func DeleteVariable(ctx *context.APIContext) { func (Action) DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
// --- // ---
// summary: Delete an org-level variable // summary: Delete an org-level variable
@ -163,7 +335,7 @@ func DeleteVariable(ctx *context.APIContext) {
} }
// CreateVariable create an org-level variable // CreateVariable create an org-level variable
func CreateVariable(ctx *context.APIContext) { func (Action) CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
// --- // ---
// summary: Create an org-level variable // summary: Create an org-level variable
@ -227,7 +399,7 @@ func CreateVariable(ctx *context.APIContext) {
} }
// UpdateVariable update an org-level variable // UpdateVariable update an org-level variable
func UpdateVariable(ctx *context.APIContext) { func (Action) UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
// --- // ---
// summary: Update an org-level variable // summary: Update an org-level variable
@ -289,3 +461,13 @@ func UpdateVariable(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
var _ actions_service.API = new(Action)
// Action implements actions_service.API
type Action struct{}
// NewAction creates a new Action service
func NewAction() actions_service.API {
return Action{}
}

View File

@ -1,31 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register org runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}

View File

@ -1,166 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
// ListActionsSecrets list an organization's actions secrets
func ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets
// ---
// summary: List an organization's actions secrets
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
opts := &secret_model.FindSecretsOptions{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the organization
func CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
// ---
// summary: Create or Update a secret value in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// "204":
// description: response when updating a secret
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
}
return
}
if created {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}
// DeleteSecret delete one secret of the organization
func DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
// ---
// summary: Delete a secret in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
}
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -9,17 +9,76 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets" secret_service "code.gitea.io/gitea/services/secrets"
) )
// ListActionsSecrets list an repo's actions secrets
func (Action) ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets
// ---
// summary: List an repo's actions secrets
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repository
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
opts := &secret_model.FindSecretsOptions{
RepoID: repo.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the repository // create or update one secret of the repository
func CreateOrUpdateSecret(ctx *context.APIContext) { func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret // swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
// --- // ---
// summary: Create or Update a secret value in a repository // summary: Create or Update a secret value in a repository
@ -82,7 +141,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
} }
// DeleteSecret delete one secret of the repository // DeleteSecret delete one secret of the repository
func DeleteSecret(ctx *context.APIContext) { func (Action) DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret // swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret
// --- // ---
// summary: Delete a secret in a repository // summary: Delete a secret in a repository
@ -133,7 +192,7 @@ func DeleteSecret(ctx *context.APIContext) {
} }
// GetVariable get a repo-level variable // GetVariable get a repo-level variable
func GetVariable(ctx *context.APIContext) { func (Action) GetVariable(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
// --- // ---
// summary: Get a repo-level variable // summary: Get a repo-level variable
@ -186,7 +245,7 @@ func GetVariable(ctx *context.APIContext) {
} }
// DeleteVariable delete a repo-level variable // DeleteVariable delete a repo-level variable
func DeleteVariable(ctx *context.APIContext) { func (Action) DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
// --- // ---
// summary: Delete a repo-level variable // summary: Delete a repo-level variable
@ -235,7 +294,7 @@ func DeleteVariable(ctx *context.APIContext) {
} }
// CreateVariable create a repo-level variable // CreateVariable create a repo-level variable
func CreateVariable(ctx *context.APIContext) { func (Action) CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
// --- // ---
// summary: Create a repo-level variable // summary: Create a repo-level variable
@ -302,7 +361,7 @@ func CreateVariable(ctx *context.APIContext) {
} }
// UpdateVariable update a repo-level variable // UpdateVariable update a repo-level variable
func UpdateVariable(ctx *context.APIContext) { func (Action) UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
// --- // ---
// summary: Update a repo-level variable // summary: Update a repo-level variable
@ -369,7 +428,7 @@ func UpdateVariable(ctx *context.APIContext) {
} }
// ListVariables list repo-level variables // ListVariables list repo-level variables
func ListVariables(ctx *context.APIContext) { func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
// --- // ---
// summary: Get repo-level variables list // summary: Get repo-level variables list
@ -423,3 +482,38 @@ func ListVariables(ctx *context.APIContext) {
ctx.SetTotalCountHeader(count) ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables) ctx.JSON(http.StatusOK, variables)
} }
// GetRegistrationToken returns the token to register repo runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
}
var _ actions_service.API = new(Action)
// Action implements actions_service.API
type Action struct{}
// NewAction creates a new Action service
func NewAction() actions_service.API {
return Action{}
}

View File

@ -1,34 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
// GetRegistrationToken returns the token to register repo runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
}

View File

@ -5,6 +5,7 @@ package routers
import ( import (
"context" "context"
"net/http"
"reflect" "reflect"
"runtime" "runtime"
@ -25,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/routing"
actions_router "code.gitea.io/gitea/routers/api/actions" actions_router "code.gitea.io/gitea/routers/api/actions"
packages_router "code.gitea.io/gitea/routers/api/packages" packages_router "code.gitea.io/gitea/routers/api/packages"
apiv1 "code.gitea.io/gitea/routers/api/v1" apiv1 "code.gitea.io/gitea/routers/api/v1"
@ -202,5 +204,9 @@ func NormalRoutes() *web.Route {
r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix)) r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix))
} }
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))
http.NotFound(w, req)
})
return r return r
} }

View File

@ -382,17 +382,17 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/" return setting.AppSubURL + "/"
} }
func getUserName(gothUser *goth.User) (string, error) { // extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
// It returns ("", nil) if the required field doesn't exist.
func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) {
switch setting.OAuth2Client.Username { switch setting.OAuth2Client.Username {
case setting.OAuth2UsernameEmail: case setting.OAuth2UsernameEmail:
return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0]) return user_model.NormalizeUserName(gothUser.Email)
case setting.OAuth2UsernamePreferredUsername: case setting.OAuth2UsernamePreferredUsername:
preferredUsername, exists := gothUser.RawData["preferred_username"] if preferredUsername, ok := gothUser.RawData["preferred_username"].(string); ok {
if exists { return user_model.NormalizeUserName(preferredUsername)
return user_model.NormalizeUserName(preferredUsername.(string))
} else {
return "", fmt.Errorf("preferred_username is missing in received user data but configured as username source for user_id %q. Check if OPENID_CONNECT_SCOPES contains profile", gothUser.UserID)
} }
return "", nil
case setting.OAuth2UsernameNickname: case setting.OAuth2UsernameNickname:
return user_model.NormalizeUserName(gothUser.NickName) return user_model.NormalizeUserName(gothUser.NickName)
default: // OAuth2UsernameUserid default: // OAuth2UsernameUserid

View File

@ -8,12 +8,31 @@ import (
"net/url" "net/url"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
cfg.Provider = util.IfZero(cfg.Provider, "gitea")
err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{
Type: auth_model.OAuth2,
Name: authName,
IsActive: true,
Cfg: &cfg,
})
assert.NoError(t, err)
}
func TestUserLogin(t *testing.T) { func TestUserLogin(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/user/login") ctx, resp := contexttest.MockContext(t, "/user/login")
SignIn(ctx) SignIn(ctx)
@ -41,3 +60,24 @@ func TestUserLogin(t *testing.T) {
SignIn(ctx) SignIn(ctx)
assert.Equal(t, "/", test.RedirectURL(resp)) assert.Equal(t, "/", test.RedirectURL(resp))
} }
func TestSignUpOAuth2ButMissingFields(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
})()
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
ctx.SetParams("provider", "dummy-auth-source")
SignInOAuthCallback(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
// then the user will be redirected to the link account page, and see a message about the missing fields
ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
LinkAccount(ctx)
assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
}

View File

@ -48,23 +48,27 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser") gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
if gothUser == nil { if !ok {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) // no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login")
return return
} }
gu, _ := gothUser.(goth.User) if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
uname, err := getUserName(&gu) ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ","))
}
uname, err := extractUserNameFromOAuth2(&gothUser)
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
} }
email := gu.Email email := gothUser.Email
ctx.Data["user_name"] = uname ctx.Data["user_name"] = uname
ctx.Data["email"] = email ctx.Data["email"] = email
if len(email) != 0 { if email != "" {
u, err := user_model.GetUserByEmail(ctx, email) u, err := user_model.GetUserByEmail(ctx, email)
if err != nil && !user_model.IsErrUserNotExist(err) { if err != nil && !user_model.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
@ -73,7 +77,7 @@ func LinkAccount(ctx *context.Context) {
if u != nil { if u != nil {
ctx.Data["user_exists"] = true ctx.Data["user_exists"] = true
} }
} else if len(uname) != 0 { } else if uname != "" {
u, err := user_model.GetUserByName(ctx, uname) u, err := user_model.GetUserByName(ctx, uname)
if err != nil && !user_model.IsErrUserNotExist(err) { if err != nil && !user_model.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)

View File

@ -934,7 +934,7 @@ func SignInOAuthCallback(ctx *context.Context) {
if u == nil { if u == nil {
if ctx.Doer != nil { if ctx.Doer != nil {
// attach user to already logged in user // attach user to the current signed-in user
err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser) err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
if err != nil { if err != nil {
ctx.ServerError("UserLinkAccount", err) ctx.ServerError("UserLinkAccount", err)
@ -952,23 +952,32 @@ func SignInOAuthCallback(ctx *context.Context) {
if gothUser.Email == "" { if gothUser.Email == "" {
missingFields = append(missingFields, "email") missingFields = append(missingFields, "email")
} }
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" { uname, err := extractUserNameFromOAuth2(&gothUser)
missingFields = append(missingFields, "nickname")
}
if len(missingFields) > 0 {
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
}
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
ctx.ServerError("CreateUser", err)
return
}
uname, err := getUserName(&gothUser)
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
} }
if uname == "" {
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname {
missingFields = append(missingFields, "nickname")
} else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername {
missingFields = append(missingFields, "preferred_username")
} // else: "UserID" and "Email" have been handled above separately
}
if len(missingFields) > 0 {
log.Error(`OAuth2 auto registration (ENABLE_AUTO_REGISTRATION) is enabled but OAuth2 provider %q doesn't return required fields: %s. `+
`Suggest to: disable auto registration, or make OPENID_CONNECT_SCOPES (for OpenIDConnect) / Authentication Source Scopes (for Admin panel) to request all required fields, and the fields shouldn't be empty.`,
authSource.Name, strings.Join(missingFields, ","))
// The RawData is the only way to pass the missing fields to the another page at the moment, other ways all have various problems:
// by session or cookie: difficult to clean or reset; by URL: could be injected with uncontrollable content; by ctx.Flash: the link_account page is a mess ...
// Since the RawData is for the provider's data, so we need to use our own prefix here to avoid conflict.
if gothUser.RawData == nil {
gothUser.RawData = make(map[string]any)
}
gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
showLinkingLogin(ctx, gothUser)
return
}
u = &user_model.User{ u = &user_model.User{
Name: uname, Name: uname,
FullName: gothUser.Name, FullName: gothUser.Name,

View File

@ -67,6 +67,9 @@ type ViewResponse struct {
CanRerun bool `json:"canRerun"` CanRerun bool `json:"canRerun"`
CanDeleteArtifact bool `json:"canDeleteArtifact"` CanDeleteArtifact bool `json:"canDeleteArtifact"`
Done bool `json:"done"` Done bool `json:"done"`
WorkflowID string `json:"workflowID"`
WorkflowLink string `json:"workflowLink"`
IsSchedule bool `json:"isSchedule"`
Jobs []*ViewJob `json:"jobs"` Jobs []*ViewJob `json:"jobs"`
Commit ViewCommit `json:"commit"` Commit ViewCommit `json:"commit"`
} `json:"run"` } `json:"run"`
@ -90,12 +93,10 @@ type ViewJob struct {
} }
type ViewCommit struct { type ViewCommit struct {
LocaleCommit string `json:"localeCommit"` ShortSha string `json:"shortSHA"`
LocalePushedBy string `json:"localePushedBy"` Link string `json:"link"`
ShortSha string `json:"shortSHA"` Pusher ViewUser `json:"pusher"`
Link string `json:"link"` Branch ViewBranch `json:"branch"`
Pusher ViewUser `json:"pusher"`
Branch ViewBranch `json:"branch"`
} }
type ViewUser struct { type ViewUser struct {
@ -151,6 +152,9 @@ func ViewPost(ctx *context_module.Context) {
resp.State.Run.CanRerun = run.Status.IsDone() && 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.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.Done = run.Status.IsDone() resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.WorkflowID = run.WorkflowID
resp.State.Run.WorkflowLink = run.WorkflowLink()
resp.State.Run.IsSchedule = run.IsSchedule()
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
resp.State.Run.Status = run.Status.String() resp.State.Run.Status = run.Status.String()
for _, v := range jobs { for _, v := range jobs {
@ -172,12 +176,10 @@ func ViewPost(ctx *context_module.Context) {
Link: run.RefLink(), Link: run.RefLink(),
} }
resp.State.Run.Commit = ViewCommit{ resp.State.Run.Commit = ViewCommit{
LocaleCommit: ctx.Locale.TrString("actions.runs.commit"), ShortSha: base.ShortSha(run.CommitSHA),
LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
ShortSha: base.ShortSha(run.CommitSHA), Pusher: pusher,
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Branch: branch,
Pusher: pusher,
Branch: branch,
} }
var task *actions_model.ActionTask var task *actions_model.ActionTask

View File

@ -212,8 +212,6 @@ func SearchCommits(ctx *context.Context) {
// FileHistory show a file's reversions // FileHistory show a file's reversions
func FileHistory(ctx *context.Context) { func FileHistory(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true
fileName := ctx.Repo.TreePath fileName := ctx.Repo.TreePath
if len(fileName) == 0 { if len(fileName) == 0 {
Commits(ctx) Commits(ctx)

View File

@ -800,7 +800,6 @@ func CompareDiff(ctx *context.Context) {
} }
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID) ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true ctx.Data["IsDiffCompare"] = true
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates) _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)

View File

@ -3149,13 +3149,10 @@ func UpdateCommentContent(ctx *context.Context) {
} }
oldContent := comment.Content oldContent := comment.Content
comment.Content = ctx.FormString("content") newContent := ctx.FormString("content")
if len(comment.Content) == 0 {
ctx.JSON(http.StatusOK, map[string]any{ // allow to save empty content
"content": "", comment.Content = newContent
})
return
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) { if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
@ -3178,21 +3175,27 @@ func UpdateCommentContent(ctx *context.Context) {
} }
} }
content, err := markdown.RenderString(&markup.RenderContext{ var renderedContent template.HTML
Links: markup.Links{ if comment.Content != "" {
Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? renderedContent, err = markdown.RenderString(&markup.RenderContext{
}, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
}, comment.Content) GitRepo: ctx.Repo.GitRepo,
if err != nil { Ctx: ctx,
ctx.ServerError("RenderString", err) }, comment.Content)
return if err != nil {
ctx.ServerError("RenderString", err)
return
}
} else {
contentEmpty := fmt.Sprintf(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
renderedContent = template.HTML(contentEmpty)
} }
ctx.JSON(http.StatusOK, map[string]any{ ctx.JSON(http.StatusOK, map[string]any{
"content": content, "content": renderedContent,
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
}) })
} }

View File

@ -863,21 +863,21 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if pull.HeadRepo != nil { if pull.HeadRepo != nil {
ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch) ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch)
}
if !pull.HasMerged && ctx.Doer != nil { if !pull.HasMerged && ctx.Doer != nil {
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
if err != nil { if err != nil {
ctx.ServerError("GetUserRepoPermission", err) ctx.ServerError("GetUserRepoPermission", err)
return return
} }
if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) { if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) {
ctx.Data["CanEditFile"] = true ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link() ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link()
ctx.Data["HeadBranchName"] = pull.HeadBranch ctx.Data["HeadBranchName"] = pull.HeadBranch
ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI() ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI()
}
} }
} }
} }
@ -1225,7 +1225,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
ctx.Data["PageIsComparePull"] = true ctx.Data["PageIsComparePull"] = true
ctx.Data["IsDiffCompare"] = true ctx.Data["IsDiffCompare"] = true
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment") upload.AddUploadContext(ctx, "comment")

View File

@ -1612,7 +1612,7 @@ func registerRoutes(m *web.Route) {
m.NotFound(func(w http.ResponseWriter, req *http.Request) { m.NotFound(func(w http.ResponseWriter, req *http.Request) {
ctx := context.GetWebContext(req) ctx := context.GetWebContext(req)
routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "GlobalNotFound")) routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))
ctx.NotFound("", nil) ctx.NotFound("", nil)
}) })
} }

View File

@ -0,0 +1,28 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import "code.gitea.io/gitea/services/context"
// API for actions of a repository or organization
type API interface {
// ListActionsSecrets list secrets
ListActionsSecrets(*context.APIContext)
// CreateOrUpdateSecret create or update a secret
CreateOrUpdateSecret(*context.APIContext)
// DeleteSecret delete a secret
DeleteSecret(*context.APIContext)
// ListVariables list variables
ListVariables(*context.APIContext)
// GetVariable get a variable
GetVariable(*context.APIContext)
// DeleteVariable delete a variable
DeleteVariable(*context.APIContext)
// CreateVariable create a variable
CreateVariable(*context.APIContext)
// UpdateVariable update a variable
UpdateVariable(*context.APIContext)
// GetRegistrationToken get registration token
GetRegistrationToken(*context.APIContext)
}

View File

@ -20,14 +20,13 @@ import (
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
web_types "code.gitea.io/gitea/modules/web/types" web_types "code.gitea.io/gitea/modules/web/types"
"gitea.com/go-chi/session"
) )
// Render represents a template render // Render represents a template render
@ -154,7 +153,7 @@ func Contexter() func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := NewBaseContext(resp, req) base, baseCleanUp := NewBaseContext(resp, req)
defer baseCleanUp() defer baseCleanUp()
ctx := NewWebContext(base, rnd, session.GetSession(req)) ctx := NewWebContext(base, rnd, session.GetContextSession(req))
ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this

View File

@ -19,7 +19,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -43,7 +45,8 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
} }
type MockContextOption struct { type MockContextOption struct {
Render context.Render Render context.Render
SessionStore *session.MockStore
} }
// MockContext mock context for unit tests // MockContext mock context for unit tests
@ -62,12 +65,17 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
chiCtx := chi.NewRouteContext()
ctx := context.NewWebContext(base, opt.Render, nil) ctx := context.NewWebContext(base, opt.Render, nil)
ctx.AppendContextValue(context.WebContextKey, ctx) ctx.AppendContextValue(context.WebContextKey, ctx)
ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
if opt.SessionStore != nil {
ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore)
ctx.Session = opt.SessionStore
}
ctx.Cache = cache.GetCache()
ctx.PageData = map[string]any{} ctx.PageData = map[string]any{}
ctx.Data["PageStartTime"] = time.Now() ctx.Data["PageStartTime"] = time.Now()
chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp return ctx, resp
} }

View File

@ -38,12 +38,10 @@ func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheVal
if ok && statusStr != "" { if ok && statusStr != "" {
var cv commitStatusCacheValue var cv commitStatusCacheValue
err := json.Unmarshal([]byte(statusStr), &cv) err := json.Unmarshal([]byte(statusStr), &cv)
if err == nil && cv.State != "" { if err == nil {
return &cv return &cv
} }
if err != nil { log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
}
} }
return nil return nil
} }
@ -128,15 +126,22 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache // FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) { func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
results := make([]*git_model.CommitStatus, len(repos)) results := make([]*git_model.CommitStatus, len(repos))
allCached := true
for i, repo := range repos { for i, repo := range repos {
if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil { if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
results[i] = &git_model.CommitStatus{ results[i] = &git_model.CommitStatus{
State: api.CommitStatusState(cv.State), State: api.CommitStatusState(cv.State),
TargetURL: cv.TargetURL, TargetURL: cv.TargetURL,
} }
} else {
allCached = false
} }
} }
if allCached {
return results, nil
}
// collect the latest commit of each repo // collect the latest commit of each repo
// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
repoBranchNames := make(map[int64]string, len(repos)) repoBranchNames := make(map[int64]string, len(repos))
@ -165,10 +170,10 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
for i, repo := range repos { for i, repo := range repos {
if repo.ID == summary.RepoID { if repo.ID == summary.RepoID {
results[i] = summary results[i] = summary
_ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool { repoSHAs = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
return repoSHA.RepoID == repo.ID return repoSHA.RepoID == repo.ID
}) })
if results[i].State != "" { if results[i] != nil {
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
} }
@ -177,6 +182,9 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
} }
} }
} }
if len(repoSHAs) == 0 {
return results, nil
}
// call the database O(1) times to get the commit statuses for all repos // call the database O(1) times to get the commit statuses for all repos
repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs) repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
@ -187,7 +195,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
for i, repo := range repos { for i, repo := range repos {
if results[i] == nil { if results[i] == nil {
results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
if results[i].State != "" { if results[i] != nil {
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
} }

View File

@ -15,7 +15,7 @@
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}} {{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
</a> </a>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>: <span><b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>:</span>
{{- if .ScheduleID -}} {{- if .ScheduleID -}}
{{ctx.Locale.Tr "actions.runs.scheduled"}} {{ctx.Locale.Tr "actions.runs.scheduled"}}
{{- else -}} {{- else -}}

View File

@ -10,6 +10,9 @@
data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" data-locale-cancel="{{ctx.Locale.Tr "cancel"}}"
data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}"
data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}"
data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}"
data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}"
data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}"
data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}"
data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}"
data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}"

View File

@ -90,7 +90,16 @@
{{ctx.Locale.Tr "repo.use_template"}} {{ctx.Locale.Tr "repo.use_template"}}
</a> </a>
{{end}} {{end}}
{{if (not $isHomepage)}} {{if $isHomepage}}
{{/* only show the "code search" on the repo home page, it only does global search,
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input">
<input name="q" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}}
</div>
</form>
{{else}}
<span class="breadcrumb repo-path tw-ml-1"> <span class="breadcrumb repo-path tw-ml-1">
<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> <a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
{{- range $i, $v := .TreeNames -}} {{- range $i, $v := .TreeNames -}}
@ -103,13 +112,6 @@
{{- end -}} {{- end -}}
</span> </span>
{{end}} {{end}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input">
<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}}
</div>
</form>
</div> </div>
<div class="tw-flex tw-items-center"> <div class="tw-flex tw-items-center">
<!-- Only show clone panel in repository home page --> <!-- Only show clone panel in repository home page -->
@ -136,7 +138,7 @@
</div> </div>
{{template "repo/cite/cite_modal" .}} {{template "repo/cite/cite_modal" .}}
{{end}} {{end}}
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}} {{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}"> <a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
</a> </a>
@ -147,7 +149,7 @@
{{template "repo/view_file" .}} {{template "repo/view_file" .}}
{{else if .IsBlame}} {{else if .IsBlame}}
{{template "repo/blame" .}} {{template "repo/blame" .}}
{{else}} {{else}}{{/* IsViewDirectory */}}
{{template "repo/view_list" .}} {{template "repo/view_list" .}}
{{end}} {{end}}
</div> </div>

View File

@ -68,6 +68,6 @@
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
{{if .ReadmeExist}} {{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
{{template "repo/view_file" .}} {{template "repo/view_file" .}}
{{end}} {{end}}

View File

@ -3843,6 +3843,54 @@
} }
} }
}, },
"/repos/{owner}/{repo}/actions/secrets": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List an repo's actions secrets",
"operationId": "repoListActionsSecrets",
"parameters": [
{
"type": "string",
"description": "owner of the repository",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/SecretList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/actions/secrets/{secretname}": { "/repos/{owner}/{repo}/actions/secrets/{secretname}": {
"put": { "put": {
"consumes": [ "consumes": [

View File

@ -17,15 +17,12 @@
</overflow-menu> </overflow-menu>
<div class="ui middle very relaxed page grid"> <div class="ui middle very relaxed page grid">
<div class="column"> <div class="column">
<div class="ui tab {{if not .user_exists}}active{{end}}" <div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab">
data-tab="auth-link-signup-tab"> {{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}}
{{template "user/auth/signup_inner" .}} {{template "user/auth/signup_inner" .}}
</div> </div>
<div class="ui tab {{if .user_exists}}active{{end}}" <div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab">
data-tab="auth-link-signin-tab"> {{template "user/auth/signin_inner" .}}
<div class="ui user signin container icon">
{{template "user/auth/signin_inner" .}}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -24,6 +24,12 @@ func TestAPIRepoSecrets(t *testing.T) {
session := loginUser(t, user.Name) session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
t.Run("List", func(t *testing.T) {
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/secrets", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
})
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
cases := []struct { cases := []struct {
Name string Name string
@ -31,7 +37,7 @@ func TestAPIRepoSecrets(t *testing.T) {
}{ }{
{ {
Name: "", Name: "",
ExpectedStatus: http.StatusNotFound, ExpectedStatus: http.StatusMethodNotAllowed,
}, },
{ {
Name: "-", Name: "-",

View File

@ -4,9 +4,17 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"net/url"
"testing" "testing"
"code.gitea.io/gitea/models/db"
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"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -32,4 +40,36 @@ func TestPullCompare(t *testing.T) {
doc := NewHTMLParser(t, resp.Body) doc := NewHTMLParser(t, resp.Body)
editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
issueIndex := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueIndex{GroupID: repo1.ID}, unittest.OrderBy("group_id ASC"))
prFilesURL := fmt.Sprintf("/user2/repo1/pulls/%d/files", issueIndex.MaxIndex)
req = NewRequest(t, "GET", prFilesURL)
resp = session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// delete the head repository and revisit the PR diff view
err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, repoForked.ID)
assert.NoError(t, err)
req = NewRequest(t, "GET", prFilesURL)
resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body)
editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
assert.EqualValues(t, editButtonCount, 0, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted")
})
} }

View File

@ -403,7 +403,7 @@
background: var(--color-body); background: var(--color-body);
border-top-width: 1px; border-top-width: 1px;
border-color: var(--color-secondary); border-color: var(--color-secondary);
font-weight: var(--font-weight-medium); color: var(--color-text-dark);
margin-bottom: -1px; margin-bottom: -1px;
border-radius: 0.28571429rem 0.28571429rem 0 0 !important; border-radius: 0.28571429rem 0.28571429rem 0 0 !important;
} }

View File

@ -151,6 +151,11 @@
border-top: none; border-top: none;
} }
.ui.attached.segment:has(+ .ui[class*="top attached"].header),
.ui.attached.segment:last-child {
border-radius: 0 0 0.28571429rem 0.28571429rem;
}
.ui[class*="top attached"].segment { .ui[class*="top attached"].segment {
bottom: 0; bottom: 0;
margin-bottom: 0; margin-bottom: 0;

View File

@ -44,6 +44,9 @@ const sfc = {
canApprove: false, canApprove: false,
canRerun: false, canRerun: false,
done: false, done: false,
workflowID: '',
workflowLink: '',
isSchedule: false,
jobs: [ jobs: [
// { // {
// id: 0, // id: 0,
@ -338,10 +341,13 @@ export function initRepositoryActionView() {
approve: el.getAttribute('data-locale-approve'), approve: el.getAttribute('data-locale-approve'),
cancel: el.getAttribute('data-locale-cancel'), cancel: el.getAttribute('data-locale-cancel'),
rerun: el.getAttribute('data-locale-rerun'), rerun: el.getAttribute('data-locale-rerun'),
rerun_all: el.getAttribute('data-locale-rerun-all'),
scheduled: el.getAttribute('data-locale-runs-scheduled'),
commit: el.getAttribute('data-locale-runs-commit'),
pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
artifactsTitle: el.getAttribute('data-locale-artifacts-title'), artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
areYouSure: el.getAttribute('data-locale-are-you-sure'), areYouSure: el.getAttribute('data-locale-are-you-sure'),
confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
rerun_all: el.getAttribute('data-locale-rerun-all'),
showTimeStamps: el.getAttribute('data-locale-show-timestamps'), showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
showFullScreen: el.getAttribute('data-locale-show-full-screen'), showFullScreen: el.getAttribute('data-locale-show-full-screen'),
@ -382,10 +388,16 @@ export function initRepositoryActionView() {
</button> </button>
</div> </div>
<div class="action-commit-summary"> <div class="action-commit-summary">
{{ run.commit.localeCommit }} <span><a class="muted" :href="run.workflowLink"><b>{{ run.workflowID }}</b></a>:</span>
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a> <template v-if="run.isSchedule">
{{ run.commit.localePushedBy }} {{ locale.scheduled }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a> </template>
<template v-else>
{{ locale.commit }}
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
{{ locale.pushedBy }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
</template>
<span class="ui label tw-max-w-full" v-if="run.commit.shortSHA"> <span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
<a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a> <a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a>
</span> </span>

View File

@ -1,6 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
import {GET} from '../modules/fetch.js'; import {GET} from '../modules/fetch.js';
import {hideElem, loadElem} from '../utils/dom.js'; import {hideElem, loadElem, queryElemChildren} from '../utils/dom.js';
import {parseDom} from '../utils.js'; import {parseDom} from '../utils.js';
function getDefaultSvgBoundsIfUndefined(text, src) { function getDefaultSvgBoundsIfUndefined(text, src) {
@ -38,36 +38,36 @@ function getDefaultSvgBoundsIfUndefined(text, src) {
return null; return null;
} }
function createContext(imageAfter, imageBefore) {
const sizeAfter = {
width: imageAfter?.width || 0,
height: imageAfter?.height || 0,
};
const sizeBefore = {
width: imageBefore?.width || 0,
height: imageBefore?.height || 0,
};
const maxSize = {
width: Math.max(sizeBefore.width, sizeAfter.width),
height: Math.max(sizeBefore.height, sizeAfter.height),
};
return {
imageAfter,
imageBefore,
sizeAfter,
sizeBefore,
maxSize,
ratio: [
Math.floor(maxSize.width - sizeAfter.width) / 2,
Math.floor(maxSize.height - sizeAfter.height) / 2,
Math.floor(maxSize.width - sizeBefore.width) / 2,
Math.floor(maxSize.height - sizeBefore.height) / 2,
],
};
}
export function initImageDiff() { export function initImageDiff() {
function createContext(image1, image2) {
const size1 = {
width: image1 && image1.width || 0,
height: image1 && image1.height || 0,
};
const size2 = {
width: image2 && image2.width || 0,
height: image2 && image2.height || 0,
};
const max = {
width: Math.max(size2.width, size1.width),
height: Math.max(size2.height, size1.height),
};
return {
$image1: $(image1),
$image2: $(image2),
size1,
size2,
max,
ratio: [
Math.floor(max.width - size1.width) / 2,
Math.floor(max.height - size1.height) / 2,
Math.floor(max.width - size2.width) / 2,
Math.floor(max.height - size2.height) / 2,
],
};
}
$('.image-diff:not([data-image-diff-loaded])').each(async function() { $('.image-diff:not([data-image-diff-loaded])').each(async function() {
const $container = $(this); const $container = $(this);
this.setAttribute('data-image-diff-loaded', 'true'); this.setAttribute('data-image-diff-loaded', 'true');
@ -116,94 +116,96 @@ export function initImageDiff() {
initOverlay(createContext($imagesAfter[2], $imagesBefore[2])); initOverlay(createContext($imagesAfter[2], $imagesBefore[2]));
} }
this.querySelector(':scope > .image-diff-tabs')?.classList.remove('is-loading'); queryElemChildren(this, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
function initSideBySide(container, sizes) { function initSideBySide(container, sizes) {
let factor = 1; let factor = 1;
if (sizes.max.width > (diffContainerWidth - 24) / 2) { if (sizes.maxSize.width > (diffContainerWidth - 24) / 2) {
factor = (diffContainerWidth - 24) / 2 / sizes.max.width; factor = (diffContainerWidth - 24) / 2 / sizes.maxSize.width;
} }
const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth; const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth;
const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight; const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight;
if (sizes.$image1?.length) { if (sizes.imageAfter) {
const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width'); const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width');
boundsInfoAfterWidth.textContent = `${sizes.$image1[0].naturalWidth}px`; if (boundsInfoAfterWidth) {
if (widthChanged) boundsInfoAfterWidth.classList.add('green'); boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`;
boundsInfoAfterWidth.classList.toggle('green', widthChanged);
}
const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height'); const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height');
boundsInfoAfterHeight.textContent = `${sizes.$image1[0].naturalHeight}px`; if (boundsInfoAfterHeight) {
if (heightChanged) boundsInfoAfterHeight.classList.add('green'); boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`;
boundsInfoAfterHeight.classList.toggle('green', heightChanged);
}
} }
if (sizes.$image2?.length) { if (sizes.imageBefore) {
const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width'); const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width');
boundsInfoBeforeWidth.textContent = `${sizes.$image2[0].naturalWidth}px`; if (boundsInfoBeforeWidth) {
if (widthChanged) boundsInfoBeforeWidth.classList.add('red'); boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`;
boundsInfoBeforeWidth.classList.toggle('red', widthChanged);
}
const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height'); const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height');
boundsInfoBeforeHeight.textContent = `${sizes.$image2[0].naturalHeight}px`; if (boundsInfoBeforeHeight) {
if (heightChanged) boundsInfoBeforeHeight.classList.add('red'); boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`;
boundsInfoBeforeHeight.classList.add('red', heightChanged);
}
} }
const image1 = sizes.$image1[0]; if (sizes.imageAfter) {
if (image1) { const container = sizes.imageAfter.parentNode;
const container = image1.parentNode; sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
image1.style.width = `${sizes.size1.width * factor}px`; sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
image1.style.height = `${sizes.size1.height * factor}px`;
container.style.margin = '10px auto'; container.style.margin = '10px auto';
container.style.width = `${sizes.size1.width * factor + 2}px`; container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
container.style.height = `${sizes.size1.height * factor + 2}px`; container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
} }
const image2 = sizes.$image2[0]; if (sizes.imageBefore) {
if (image2) { const container = sizes.imageBefore.parentNode;
const container = image2.parentNode; sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
image2.style.width = `${sizes.size2.width * factor}px`; sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
image2.style.height = `${sizes.size2.height * factor}px`;
container.style.margin = '10px auto'; container.style.margin = '10px auto';
container.style.width = `${sizes.size2.width * factor + 2}px`; container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
container.style.height = `${sizes.size2.height * factor + 2}px`; container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
} }
} }
function initSwipe(sizes) { function initSwipe(sizes) {
let factor = 1; let factor = 1;
if (sizes.max.width > diffContainerWidth - 12) { if (sizes.maxSize.width > diffContainerWidth - 12) {
factor = (diffContainerWidth - 12) / sizes.max.width; factor = (diffContainerWidth - 12) / sizes.maxSize.width;
} }
const image1 = sizes.$image1[0]; if (sizes.imageAfter) {
if (image1) { const container = sizes.imageAfter.parentNode;
const container = image1.parentNode;
const swipeFrame = container.parentNode; const swipeFrame = container.parentNode;
image1.style.width = `${sizes.size1.width * factor}px`; sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
image1.style.height = `${sizes.size1.height * factor}px`; sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
container.style.margin = `0px ${sizes.ratio[0] * factor}px`; container.style.margin = `0px ${sizes.ratio[0] * factor}px`;
container.style.width = `${sizes.size1.width * factor + 2}px`; container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
container.style.height = `${sizes.size1.height * factor + 2}px`; container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`; swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`;
swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
} }
const image2 = sizes.$image2[0]; if (sizes.imageBefore) {
if (image2) { const container = sizes.imageBefore.parentNode;
const container = image2.parentNode;
const swipeFrame = container.parentNode; const swipeFrame = container.parentNode;
image2.style.width = `${sizes.size2.width * factor}px`; sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
image2.style.height = `${sizes.size2.height * factor}px`; sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
container.style.width = `${sizes.size2.width * factor + 2}px`; container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
container.style.height = `${sizes.size2.height * factor + 2}px`; container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
swipeFrame.style.height = `${sizes.max.height * factor + 2}px`; swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
} }
// extra height for inner "position: absolute" elements // extra height for inner "position: absolute" elements
const swipe = $container.find('.diff-swipe')[0]; const swipe = $container.find('.diff-swipe')[0];
if (swipe) { if (swipe) {
swipe.style.width = `${sizes.max.width * factor + 2}px`; swipe.style.width = `${sizes.maxSize.width * factor + 2}px`;
swipe.style.height = `${sizes.max.height * factor + 30}px`; swipe.style.height = `${sizes.maxSize.height * factor + 30}px`;
} }
$container.find('.swipe-bar').on('mousedown', function(e) { $container.find('.swipe-bar').on('mousedown', function(e) {
@ -229,39 +231,37 @@ export function initImageDiff() {
function initOverlay(sizes) { function initOverlay(sizes) {
let factor = 1; let factor = 1;
if (sizes.max.width > diffContainerWidth - 12) { if (sizes.maxSize.width > diffContainerWidth - 12) {
factor = (diffContainerWidth - 12) / sizes.max.width; factor = (diffContainerWidth - 12) / sizes.maxSize.width;
} }
const image1 = sizes.$image1[0]; if (sizes.imageAfter) {
if (image1) { const container = sizes.imageAfter.parentNode;
const container = image1.parentNode; sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
image1.style.width = `${sizes.size1.width * factor}px`; sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
image1.style.height = `${sizes.size1.height * factor}px`;
container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`; container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`;
container.style.width = `${sizes.size1.width * factor + 2}px`; container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
container.style.height = `${sizes.size1.height * factor + 2}px`; container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
} }
const image2 = sizes.$image2[0]; if (sizes.imageBefore) {
if (image2) { const container = sizes.imageBefore.parentNode;
const container = image2.parentNode;
const overlayFrame = container.parentNode; const overlayFrame = container.parentNode;
image2.style.width = `${sizes.size2.width * factor}px`; sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
image2.style.height = `${sizes.size2.height * factor}px`; sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
container.style.width = `${sizes.size2.width * factor + 2}px`; container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
container.style.height = `${sizes.size2.height * factor + 2}px`; container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
// some inner elements are `position: absolute`, so the container's height must be large enough // some inner elements are `position: absolute`, so the container's height must be large enough
overlayFrame.style.width = `${sizes.max.width * factor + 2}px`; overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
overlayFrame.style.height = `${sizes.max.height * factor + 2}px`; overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
} }
const rangeInput = $container[0].querySelector('input[type="range"]'); const rangeInput = $container[0].querySelector('input[type="range"]');
function updateOpacity() { function updateOpacity() {
if (sizes?.$image1?.[0]) { if (sizes.imageAfter) {
sizes.$image1[0].parentNode.style.opacity = `${rangeInput.value / 100}`; sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`;
} }
} }
rangeInput?.addEventListener('input', updateOpacity); rangeInput?.addEventListener('input', updateOpacity);