diff --git a/models/issues/reaction.go b/models/issues/reaction.go
index 11b3c6be203..f24001fd231 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -7,6 +7,7 @@ import (
"bytes"
"context"
"fmt"
+ "strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -321,6 +322,11 @@ func valuesUser(m map[int64]*user_model.User) []*user_model.User {
return values
}
+// newMigrationOriginalUser creates and returns a fake user for external user
+func newMigrationOriginalUser(name string) *user_model.User {
+ return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
+}
+
// LoadUsers loads reactions' all users
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
if len(list) == 0 {
@@ -338,7 +344,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit
for _, reaction := range list {
if reaction.OriginalAuthor != "" {
- reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
+ reaction.User = newMigrationOriginalUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
} else if user, ok := userMaps[reaction.UserID]; ok {
reaction.User = user
} else {
diff --git a/models/user/avatar.go b/models/user/avatar.go
index 5453c78fc61..2a41b991292 100644
--- a/models/user/avatar.go
+++ b/models/user/avatar.go
@@ -38,27 +38,30 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
u.Avatar = avatars.HashEmail(seed)
- // Don't share the images so that we can delete them easily
- if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
- if err := png.Encode(w, img); err != nil {
- log.Error("Encode: %v", err)
+ _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+ if err != nil {
+ // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
+ // Don't share the images so that we can delete them easily
+ if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
+ if err := png.Encode(w, img); err != nil {
+ log.Error("Encode: %v", err)
+ }
+ return nil
+ }); err != nil {
+ return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
}
- return err
- }); err != nil {
- return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
}
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
return err
}
- log.Info("New random avatar created: %d", u.ID)
return nil
}
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
- if u.IsGhost() {
+ if u.IsGhost() || u.IsGiteaActions() {
return avatars.DefaultAvatarLink()
}
diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go
index 1078875ee16..a1cc01316f3 100644
--- a/models/user/avatar_test.go
+++ b/models/user/avatar_test.go
@@ -4,13 +4,19 @@
package user
import (
+ "context"
+ "io"
+ "strings"
"testing"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestUserAvatarLink(t *testing.T) {
@@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) {
link = u.AvatarLink(db.DefaultContext)
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
}
+
+func TestUserAvatarGenerate(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ var err error
+ tmpDir := t.TempDir()
+ storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir})
+ require.NoError(t, err)
+
+ u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
+
+ // there was no avatar, generate a new one
+ assert.Empty(t, u.Avatar)
+ err = GenerateRandomAvatar(db.DefaultContext, u)
+ require.NoError(t, err)
+ assert.NotEmpty(t, u.Avatar)
+
+ // make sure the generated one exists
+ oldAvatarPath := u.CustomAvatarRelativePath()
+ _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+ require.NoError(t, err)
+ // and try to change its content
+ _, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
+ require.NoError(t, err)
+
+ // try to generate again
+ err = GenerateRandomAvatar(db.DefaultContext, u)
+ require.NoError(t, err)
+ assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
+ f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
+ require.NoError(t, err)
+ defer f.Close()
+ content, _ := io.ReadAll(f)
+ assert.Equal(t, "abcd", string(content))
+}
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 74ba5f617a5..2ba6a564502 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -8,7 +8,6 @@ import (
"context"
"fmt"
"net/mail"
- "regexp"
"strings"
"time"
@@ -153,8 +152,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
return err
}
-var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
-
// ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {
@@ -514,7 +511,7 @@ func validateEmailBasic(email string) error {
return ErrEmailInvalid{email}
}
- if !emailRegexp.MatchString(email) {
+ if !globalVars().emailRegexp.MatchString(email) {
return ErrEmailCharIsNotSupported{email}
}
@@ -545,3 +542,13 @@ func IsEmailDomainAllowed(email string) bool {
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
}
+
+func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]string, error) {
+ emails := make([]string, 0, 2)
+ if err := db.GetEngine(ctx).Table("email_address").Select("email").
+ Where("uid=? AND is_activated=?", uid, true).Asc("id").
+ Find(&emails); err != nil {
+ return nil, err
+ }
+ return emails, nil
+}
diff --git a/models/user/openid.go b/models/user/openid.go
index ee4ecabae0b..420c67ca183 100644
--- a/models/user/openid.go
+++ b/models/user/openid.go
@@ -11,9 +11,6 @@ import (
"code.gitea.io/gitea/modules/util"
)
-// ErrOpenIDNotExist openid is not known
-var ErrOpenIDNotExist = util.NewNotExistErrorf("OpenID is unknown")
-
// UserOpenID is the list of all OpenID identities of a user.
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
type UserOpenID struct { //revive:disable-line:exported
@@ -99,7 +96,7 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
if err != nil {
return err
} else if deleted != 1 {
- return ErrOpenIDNotExist
+ return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}
diff --git a/models/user/user.go b/models/user/user.go
index f790392a9bd..e13fb6ab3c3 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -14,6 +14,7 @@ import (
"path/filepath"
"regexp"
"strings"
+ "sync"
"time"
"unicode"
@@ -213,7 +214,7 @@ func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
}
-// GetEmail returns an noreply email, if the user has set to keep his
+// GetEmail returns a noreply email, if the user has set to keep his
// email address private, otherwise the primary email address.
func (u *User) GetEmail() string {
if u.KeepEmailPrivate {
@@ -417,19 +418,9 @@ func (u *User) DisplayName() string {
return u.Name
}
-var emailToReplacer = strings.NewReplacer(
- "\n", "",
- "\r", "",
- "<", "",
- ">", "",
- ",", "",
- ":", "",
- ";", "",
-)
-
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func (u *User) EmailTo() string {
- sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
+ sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
// should be an edge case but nice to have
if sanitizedDisplayName == u.Email {
@@ -526,28 +517,58 @@ func GetUserSalt() (string, error) {
if err != nil {
return "", err
}
- // Returns a 32 bytes long string.
+ // Returns a 32-byte long string.
return hex.EncodeToString(rBytes), nil
}
-// 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
-var (
- customCharsReplacement = strings.NewReplacer("Æ", "AE")
- removeCharsRE = regexp.MustCompile("['`´]")
- transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
- replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
-)
+type globalVarsStruct struct {
+ customCharsReplacement *strings.Replacer
+ removeCharsRE *regexp.Regexp
+ transformDiacritics transform.Transformer
+ replaceCharsHyphenRE *regexp.Regexp
+ emailToReplacer *strings.Replacer
+ emailRegexp *regexp.Regexp
+ systemUserNewFuncs map[int64]func() *User
+}
+
+var globalVars = sync.OnceValue(func() *globalVarsStruct {
+ return &globalVarsStruct{
+ // 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
+ customCharsReplacement: strings.NewReplacer("Æ", "AE"),
+
+ removeCharsRE: regexp.MustCompile("['`´]"),
+ transformDiacritics: transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC),
+ replaceCharsHyphenRE: regexp.MustCompile(`[\s~+]`),
+
+ emailToReplacer: strings.NewReplacer(
+ "\n", "",
+ "\r", "",
+ "<", "",
+ ">", "",
+ ",", "",
+ ":", "",
+ ";", "",
+ ),
+ emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
+
+ systemUserNewFuncs: map[int64]func() *User{
+ GhostUserID: NewGhostUser,
+ ActionsUserID: NewActionsUser,
+ },
+ }
+})
// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
func NormalizeUserName(s string) (string, error) {
+ vars := globalVars()
s, _, _ = strings.Cut(s, "@")
- strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
+ strDiacriticsRemoved, n, err := transform.String(vars.transformDiacritics, vars.customCharsReplacement.Replace(s))
if err != nil {
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 vars.replaceCharsHyphenRE.ReplaceAllLiteralString(vars.removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
}
var (
@@ -963,30 +984,28 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, err
}
-// GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0
+// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
- switch id {
- case GhostUserID:
- return NewGhostUser(), nil
- case ActionsUserID:
- return NewActionsUser(), nil
- case 0:
+ if id < 0 {
+ if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
+ return newFunc(), nil
+ }
+ return nil, ErrUserNotExist{UID: id}
+ } else if id == 0 {
return nil, ErrUserNotExist{}
- default:
- return GetUserByID(ctx, id)
}
+ return GetUserByID(ctx, id)
}
-// GetPossibleUserByIDs returns the users if id > 0 or return system users if id < 0
+// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
uniqueIDs := container.SetOf(ids...)
users := make([]*User, 0, len(ids))
_ = uniqueIDs.Remove(0)
- if uniqueIDs.Remove(GhostUserID) {
- users = append(users, NewGhostUser())
- }
- if uniqueIDs.Remove(ActionsUserID) {
- users = append(users, NewActionsUser())
+ for systemUID, newFunc := range globalVars().systemUserNewFuncs {
+ if uniqueIDs.Remove(systemUID) {
+ users = append(users, newFunc())
+ }
}
res, err := GetUserByIDs(ctx, uniqueIDs.Values())
if err != nil {
@@ -996,7 +1015,7 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, nil
}
-// GetUserByNameCtx returns user by given name.
+// GetUserByName returns user by given name.
func GetUserByName(ctx context.Context, name string) (*User, error) {
if len(name) == 0 {
return nil, ErrUserNotExist{Name: name}
@@ -1027,8 +1046,8 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string {
return mails
}
-// GetMaileableUsersByIDs gets users from ids, but only if they can receive mails
-func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) {
+// GetMailableUsersByIDs gets users from ids, but only if they can receive mails
+func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) {
if len(ids) == 0 {
return nil, nil
}
@@ -1053,17 +1072,6 @@ func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([
Find(&ous)
}
-// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids.
-func GetUserNamesByIDs(ctx context.Context, ids []int64) ([]string, error) {
- unames := make([]string, 0, len(ids))
- err := db.GetEngine(ctx).In("id", ids).
- Table("user").
- Asc("name").
- Cols("name").
- Find(&unames)
- return unames, err
-}
-
// GetUserNameByID returns username for the id
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
var name string
diff --git a/models/user/user_system.go b/models/user/user_system.go
index 612cdb2caef..e54973dc8e5 100644
--- a/models/user/user_system.go
+++ b/models/user/user_system.go
@@ -10,9 +10,8 @@ import (
)
const (
- GhostUserID = -1
- GhostUserName = "Ghost"
- GhostUserLowerName = "ghost"
+ GhostUserID = -1
+ GhostUserName = "Ghost"
)
// NewGhostUser creates and returns a fake user for someone has deleted their account.
@@ -20,10 +19,14 @@ func NewGhostUser() *User {
return &User{
ID: GhostUserID,
Name: GhostUserName,
- LowerName: GhostUserLowerName,
+ LowerName: strings.ToLower(GhostUserName),
}
}
+func IsGhostUserName(name string) bool {
+ return strings.EqualFold(name, GhostUserName)
+}
+
// IsGhost check if user is fake user for a deleted account
func (u *User) IsGhost() bool {
if u == nil {
@@ -32,22 +35,16 @@ func (u *User) IsGhost() bool {
return u.ID == GhostUserID && u.Name == GhostUserName
}
-// NewReplaceUser creates and returns a fake user for external user
-func NewReplaceUser(name string) *User {
- return &User{
- ID: 0,
- Name: name,
- LowerName: strings.ToLower(name),
- }
-}
-
const (
- ActionsUserID = -2
- ActionsUserName = "gitea-actions"
- ActionsFullName = "Gitea Actions"
- ActionsEmail = "teabot@gitea.io"
+ ActionsUserID = -2
+ ActionsUserName = "gitea-actions"
+ ActionsUserEmail = "teabot@gitea.io"
)
+func IsGiteaActionsUserName(name string) bool {
+ return strings.EqualFold(name, ActionsUserName)
+}
+
// NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User {
return &User{
@@ -55,8 +52,8 @@ func NewActionsUser() *User {
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
- FullName: ActionsFullName,
- Email: ActionsEmail,
+ FullName: "Gitea Actions",
+ Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
Type: UserTypeIndividual,
@@ -65,6 +62,16 @@ func NewActionsUser() *User {
}
}
-func (u *User) IsActions() bool {
+func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID
}
+
+func GetSystemUserByName(name string) *User {
+ if IsGhostUserName(name) {
+ return NewGhostUser()
+ }
+ if IsGiteaActionsUserName(name) {
+ return NewActionsUser()
+ }
+ return nil
+}
diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go
new file mode 100644
index 00000000000..97768b509be
--- /dev/null
+++ b/models/user/user_system_test.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSystemUser(t *testing.T) {
+ u, err := GetPossibleUserByID(db.DefaultContext, -1)
+ require.NoError(t, err)
+ assert.Equal(t, "Ghost", u.Name)
+ assert.Equal(t, "ghost", u.LowerName)
+ assert.True(t, u.IsGhost())
+ assert.True(t, IsGhostUserName("gHost"))
+
+ u, err = GetPossibleUserByID(db.DefaultContext, -2)
+ require.NoError(t, err)
+ assert.Equal(t, "gitea-actions", u.Name)
+ assert.Equal(t, "gitea-actions", u.LowerName)
+ assert.True(t, u.IsGiteaActions())
+ assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
+
+ _, err = GetPossibleUserByID(db.DefaultContext, -3)
+ require.Error(t, err)
+}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index cad1a64d6e8..51098417e6d 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -333,14 +333,14 @@ func TestGetUserIDsByNames(t *testing.T) {
func TestGetMaileableUsersByIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- results, err := user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, false)
+ results, err := user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, false)
assert.NoError(t, err)
assert.Len(t, results, 1)
if len(results) > 1 {
assert.Equal(t, 1, results[0].ID)
}
- results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
+ results, err = user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
assert.NoError(t, err)
assert.Len(t, results, 2)
if len(results) > 2 {
diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go
index 2ac744fbf61..729401f7521 100644
--- a/modules/git/commit_submodule_file.go
+++ b/modules/git/commit_submodule_file.go
@@ -46,9 +46,9 @@ func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID
if len(optCommitID) == 2 {
commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
} else if len(optCommitID) == 1 {
- commitLink = sf.repoLink + "/commit/" + optCommitID[0]
+ commitLink = sf.repoLink + "/tree/" + optCommitID[0]
} else {
- commitLink = sf.repoLink + "/commit/" + sf.refID
+ commitLink = sf.repoLink + "/tree/" + sf.refID
}
return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
}
diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go
index 4b5b7676126..98342aa9e97 100644
--- a/modules/git/commit_submodule_file_test.go
+++ b/modules/git/commit_submodule_file_test.go
@@ -15,11 +15,11 @@ func TestCommitSubmoduleLink(t *testing.T) {
wl := sf.SubmoduleWebLink(context.Background())
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
- assert.Equal(t, "https://github.com/user/repo/commit/aaaa", wl.CommitWebLink)
+ assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
wl = sf.SubmoduleWebLink(context.Background(), "1111")
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
- assert.Equal(t, "https://github.com/user/repo/commit/1111", wl.CommitWebLink)
+ assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink)
wl = sf.SubmoduleWebLink(context.Background(), "1111", "2222")
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go
index 9560c2cd946..9a87dd32d99 100644
--- a/modules/git/commit_test.go
+++ b/modules/git/commit_test.go
@@ -357,5 +357,5 @@ func Test_GetCommitBranchStart(t *testing.T) {
startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String())
assert.NoError(t, err)
assert.NotEmpty(t, startCommitID)
- assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID)
+ assert.EqualValues(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
}
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 647894bb213..02d8e163e4f 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -519,6 +519,7 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error
return nil
}
+// GetCommitBranchStart returns the commit where the branch diverged
func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
cmd := NewCommand(repo.Ctx, "log", prettyLogFormat)
cmd.AddDynamicArguments(endCommitID)
@@ -533,7 +534,8 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s
parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'})
- var startCommitID string
+ // check the commits one by one until we find a commit contained by another branch
+ // and we think this commit is the divergence point
for _, commitID := range parts {
branches, err := repo.getBranches(env, string(commitID), 2)
if err != nil {
@@ -541,11 +543,9 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s
}
for _, b := range branches {
if b != branch {
- return startCommitID, nil
+ return string(commitID), nil
}
}
-
- startCommitID = string(commitID)
}
return "", nil
diff --git a/modules/storage/storage.go b/modules/storage/storage.go
index 750ecdfe0db..b0529941e7d 100644
--- a/modules/storage/storage.go
+++ b/modules/storage/storage.go
@@ -93,7 +93,7 @@ func Clean(storage ObjectStorage) error {
}
// SaveFrom saves data to the ObjectStorage with path p from the callback
-func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) error) error {
+func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error {
pr, pw := io.Pipe()
defer pr.Close()
go func() {
@@ -103,7 +103,7 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err
}
}()
- _, err := objStorage.Save(p, pr, -1)
+ _, err := objStorage.Save(path, pr, -1)
return err
}
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index ce5742e5c7c..cef2dbd7129 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -116,14 +116,7 @@ var (
_ Payloader = &PackagePayload{}
)
-// _________ __
-// \_ ___ \_______ ____ _____ _/ |_ ____
-// / \ \/\_ __ \_/ __ \\__ \\ __\/ __ \
-// \ \____| | \/\ ___/ / __ \| | \ ___/
-// \______ /|__| \___ >____ /__| \___ >
-// \/ \/ \/ \/
-
-// CreatePayload FIXME
+// CreatePayload represents a payload information of create event.
type CreatePayload struct {
Sha string `json:"sha"`
Ref string `json:"ref"`
@@ -157,13 +150,6 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) {
return hook, nil
}
-// ________ .__ __
-// \______ \ ____ | | _____/ |_ ____
-// | | \_/ __ \| | _/ __ \ __\/ __ \
-// | ` \ ___/| |_\ ___/| | \ ___/
-// /_______ /\___ >____/\___ >__| \___ >
-// \/ \/ \/ \/
-
// PusherType define the type to push
type PusherType string
@@ -186,13 +172,6 @@ func (p *DeletePayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
-// ___________ __
-// \_ _____/__________| | __
-// | __)/ _ \_ __ \ |/ /
-// | \( <_> ) | \/ <
-// \___ / \____/|__| |__|_ \
-// \/ \/
-
// ForkPayload represents fork payload
type ForkPayload struct {
Forkee *Repository `json:"forkee"`
@@ -232,13 +211,6 @@ func (p *IssueCommentPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
-// __________ .__
-// \______ \ ____ | | ____ _____ ______ ____
-// | _// __ \| | _/ __ \\__ \ / ___// __ \
-// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/
-// |____|_ /\___ >____/\___ >____ /____ >\___ >
-// \/ \/ \/ \/ \/ \/
-
// HookReleaseAction defines hook release action type
type HookReleaseAction string
@@ -302,13 +274,6 @@ func (p *PushPayload) Branch() string {
return strings.ReplaceAll(p.Ref, "refs/heads/", "")
}
-// .___
-// | | ______ ________ __ ____
-// | |/ ___// ___/ | \_/ __ \
-// | |\___ \ \___ \| | /\ ___/
-// |___/____ >____ >____/ \___ >
-// \/ \/ \/
-
// HookIssueAction FIXME
type HookIssueAction string
@@ -371,13 +336,6 @@ type ChangesPayload struct {
Ref *ChangesFromPayload `json:"ref,omitempty"`
}
-// __________ .__ .__ __________ __
-// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_
-// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
-// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | |
-// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__|
-// \/ \/ |__| \/ \/
-
// PullRequestPayload represents a payload information of pull request event.
type PullRequestPayload struct {
Action HookIssueAction `json:"action"`
@@ -402,13 +360,6 @@ type ReviewPayload struct {
Content string `json:"content"`
}
-// __ __.__ __ .__
-// / \ / \__| | _|__|
-// \ \/\/ / | |/ / |
-// \ /| | <| |
-// \__/\ / |__|__|_ \__|
-// \/ \/
-
// HookWikiAction an action that happens to a wiki page
type HookWikiAction string
@@ -435,13 +386,6 @@ func (p *WikiPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
-//__________ .__ __
-//\______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
-// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
-// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
-// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
-// \/ \/|__| \/ \/
-
// HookRepoAction an action that happens to a repo
type HookRepoAction string
@@ -480,7 +424,7 @@ type PackagePayload struct {
Action HookPackageAction `json:"action"`
Repository *Repository `json:"repository"`
Package *Package `json:"package"`
- Organization *User `json:"organization"`
+ Organization *Organization `json:"organization"`
Sender *User `json:"sender"`
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index af6b2d893ac..777e9806121 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1345,6 +1345,8 @@ editor.new_branch_name_desc = New branch name…
editor.cancel = Cancel
editor.filename_cannot_be_empty = The filename cannot be empty.
editor.filename_is_invalid = The filename is invalid: "%s".
+editor.commit_email = Commit email
+editor.invalid_commit_email = The email for the commit is invalid.
editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
editor.branch_already_exists = Branch "%s" already exists in this repository.
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index ced3ed7bd16..a5558eebb0d 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1499,7 +1499,7 @@ issues.remove_labels=a supprimé les labels %s %s.
issues.add_remove_labels=a ajouté le label %s et supprimé %s %s.
issues.add_milestone_at=`a ajouté ça au jalon %s %s.`
issues.add_project_at=`a ajouté ça au projet %s %s.`
-issues.move_to_column_of_project=`a déplacé ça vers %s dans %s sur %s`
+issues.move_to_column_of_project=`a déplacé ça vers %s dans %s %s.`
issues.change_milestone_at=`a remplacé le jalon %s par %s %s.`
issues.change_project_at=`a remplacé le projet %s par %s %s.`
issues.remove_milestone_at=`a supprimé ça du jalon %s %s.`
diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini
index a7309ee6483..805deb618db 100644
--- a/options/locale/locale_ga-IE.ini
+++ b/options/locale/locale_ga-IE.ini
@@ -1646,7 +1646,7 @@ issues.label.filter_sort.by_size=Méid is lú
issues.label.filter_sort.reverse_by_size=Méid is mó
issues.num_participants=%d Rannpháirtithe
issues.attachment.open_tab=`Cliceáil chun "%s" a fheiceáil i gcluaisín nua`
-issues.attachment.download=Cliceáil chun "%s" a íoslódáil
+issues.attachment.download=`Cliceáil chun "%s" a íoslódáil`
issues.subscribe=Liostáil
issues.unsubscribe=Díliostáil
issues.unpin=Díphoráil
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index d3ccc711b22..cc21c5abea0 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -1135,9 +1135,9 @@ file_view_raw=Ver original
file_permalink=Link permanente
file_too_large=O arquivo é muito grande para ser mostrado.
invisible_runes_header=`Este arquivo contém caracteres Unicode invisíveis`
-invisible_runes_description=Este arquivo contém caracteres Unicode invisíveis que são indistinguíveis para humanos, mas que podem ser processados de forma diferente por um computador. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los
+invisible_runes_description=`Este arquivo contém caracteres Unicode invisíveis que são indistinguíveis para humanos, mas que podem ser processados de forma diferente por um computador. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los`
ambiguous_runes_header=`Este arquivo contém caracteres Unicode ambíguos`
-ambiguous_runes_description=Este arquivo contém caracteres Unicode que podem ser confundidos com outros caracteres. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los
+ambiguous_runes_description=`Este arquivo contém caracteres Unicode que podem ser confundidos com outros caracteres. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los`
invisible_runes_line=`Esta linha tem caracteres unicode invisíveis`
ambiguous_runes_line=`Esta linha tem caracteres unicode ambíguos`
diff --git a/package-lock.json b/package-lock.json
index 2c4f79926d0..8afe2b533fa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.5",
- "@github/text-expander-element": "2.8.0",
+ "@github/text-expander-element": "2.9.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
@@ -103,9 +103,11 @@
"markdownlint-cli": "0.43.0",
"nolyfill": "1.0.43",
"postcss-html": "1.8.0",
- "stylelint": "16.13.2",
+ "stylelint": "16.14.1",
+ "stylelint-config-recommended": "15.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.7",
+ "stylelint-define-config": "16.14.0",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2",
"type-fest": "4.33.0",
@@ -2848,9 +2850,9 @@
"license": "MIT"
},
"node_modules/@github/text-expander-element": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.8.0.tgz",
- "integrity": "sha512-kkS2rZ/CG8HGKblpLDQ8vcK/K7l/Jsvzi/N4ovwPAsFSOImcIbJh2MgCv9tzqE3wAm/qXlscvh3Ms4Hh1vtZvw==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.9.0.tgz",
+ "integrity": "sha512-NjoFiQ/3955XyefrkmtUpZvrgDl0MGyncv2QJBrUZ1+oOFOu+UmCR/ybkcuTgNg0O6AGcl8rUEXStUfrRPUCVQ==",
"license": "MIT",
"dependencies": {
"@github/combobox-nav": "^2.0.2",
@@ -13143,9 +13145,9 @@
"license": "ISC"
},
"node_modules/stylelint": {
- "version": "16.13.2",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.13.2.tgz",
- "integrity": "sha512-wDlgh0mRO9RtSa3TdidqHd0nOG8MmUyVKl+dxA6C1j8aZRzpNeEgdhFmU5y4sZx4Fc6r46p0fI7p1vR5O2DZqA==",
+ "version": "16.14.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.14.1.tgz",
+ "integrity": "sha512-oqCL7AC3786oTax35T/nuLL8p2C3k/8rHKAooezrPGRvUX0wX+qqs5kMWh5YYT4PHQgVDobHT4tw55WgpYG6Sw==",
"dev": true,
"funding": [
{
@@ -13177,7 +13179,7 @@
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.3.1",
- "ignore": "^7.0.1",
+ "ignore": "^7.0.3",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
"known-css-properties": "^0.35.0",
@@ -13186,7 +13188,7 @@
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"picocolors": "^1.1.1",
- "postcss": "^8.4.49",
+ "postcss": "^8.5.1",
"postcss-resolve-nested-selector": "^0.1.6",
"postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.0.0",
@@ -13205,6 +13207,29 @@
"node": ">=18.12.0"
}
},
+ "node_modules/stylelint-config-recommended": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-15.0.0.tgz",
+ "integrity": "sha512-9LejMFsat7L+NXttdHdTq94byn25TD+82bzGRiV1Pgasl99pWnwipXS5DguTpp3nP1XjvLXVnEJIuYBfsRjRkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.13.0"
+ }
+ },
"node_modules/stylelint-declaration-block-no-ignored-properties": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/stylelint-declaration-block-no-ignored-properties/-/stylelint-declaration-block-no-ignored-properties-2.8.0.tgz",
@@ -13231,6 +13256,24 @@
"stylelint": ">=7 <=16"
}
},
+ "node_modules/stylelint-define-config": {
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/stylelint-define-config/-/stylelint-define-config-16.14.0.tgz",
+ "integrity": "sha512-5R7/Vv6awCkNaPcedo1GuUp+7YTFvDnexogO4l/C0i349pBDYbefN6XzsDGsGOhU++maQSh2fp3mWNO0F16IjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0",
+ "pnpm": ">=8.6.0"
+ },
+ "peerDependencies": {
+ "stylelint": ">=16.0.0"
+ }
+ },
"node_modules/stylelint-value-no-unknown-custom-properties": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz",
diff --git a/package.json b/package.json
index 97f73b79732..997941f0b48 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.5",
- "@github/text-expander-element": "2.8.0",
+ "@github/text-expander-element": "2.9.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
@@ -102,9 +102,11 @@
"markdownlint-cli": "0.43.0",
"nolyfill": "1.0.43",
"postcss-html": "1.8.0",
- "stylelint": "16.13.2",
+ "stylelint": "16.14.1",
+ "stylelint-config-recommended": "15.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.7",
+ "stylelint-define-config": "16.14.0",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2",
"type-fest": "4.33.0",
diff --git a/poetry.lock b/poetry.lock
index 8c01674966d..ab5bcf05aca 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
[[package]]
name = "click"
@@ -6,6 +6,7 @@ version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
@@ -20,6 +21,7 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -31,6 +33,7 @@ version = "1.15.1"
description = "CSS unobfuscator and beautifier."
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
]
@@ -46,6 +49,7 @@ version = "1.36.4"
description = "HTML Template Linter and Formatter"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"},
{file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"},
@@ -90,6 +94,7 @@ version = "0.17.0"
description = "EditorConfig File Locator and Interpreter for Python"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"},
{file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"},
@@ -101,6 +106,7 @@ version = "1.15.1"
description = "JavaScript unobfuscator and beautifier."
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
]
@@ -115,6 +121,7 @@ version = "0.10.0"
description = "A Python implementation of the JSON5 data format."
optional = false
python-versions = ">=3.8.0"
+groups = ["dev"]
files = [
{file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"},
{file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"},
@@ -129,6 +136,7 @@ version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
@@ -140,6 +148,7 @@ version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
@@ -202,6 +211,7 @@ version = "2024.11.6"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
@@ -305,6 +315,7 @@ version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
@@ -316,6 +327,8 @@ version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -357,6 +370,7 @@ version = "4.67.1"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
{file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
@@ -378,6 +392,8 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
@@ -389,6 +405,7 @@ version = "1.35.1"
description = "A linter for YAML files."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
{file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
@@ -402,6 +419,6 @@ pyyaml = "*"
dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata]
-lock-version = "2.0"
+lock-version = "2.1"
python-versions = "^3.10"
content-hash = "f2e8260efe6e25f77ef387daff9551e41d25027e4794b42bc7a851ed0dfafd85"
diff --git a/poetry.toml b/poetry.toml
index 0299355b5dd..6eda9f8effc 100644
--- a/poetry.toml
+++ b/poetry.toml
@@ -1,4 +1,3 @@
[virtualenvs]
in-project = true
options.no-pip = true
-options.no-setuptools = true
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 3eefd2ae292..045db7a291f 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -489,12 +489,12 @@ func ChangeFiles(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
- Name: apiOpts.Committer.Name,
- Email: apiOpts.Committer.Email,
+ GitUserName: apiOpts.Committer.Name,
+ GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
- Name: apiOpts.Author.Name,
- Email: apiOpts.Author.Email,
+ GitUserName: apiOpts.Author.Name,
+ GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
@@ -586,12 +586,12 @@ func CreateFile(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
- Name: apiOpts.Committer.Name,
- Email: apiOpts.Committer.Email,
+ GitUserName: apiOpts.Committer.Name,
+ GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
- Name: apiOpts.Author.Name,
- Email: apiOpts.Author.Email,
+ GitUserName: apiOpts.Author.Name,
+ GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
@@ -689,12 +689,12 @@ func UpdateFile(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
- Name: apiOpts.Committer.Name,
- Email: apiOpts.Committer.Email,
+ GitUserName: apiOpts.Committer.Name,
+ GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
- Name: apiOpts.Author.Name,
- Email: apiOpts.Author.Email,
+ GitUserName: apiOpts.Author.Name,
+ GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
@@ -848,12 +848,12 @@ func DeleteFile(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
- Name: apiOpts.Committer.Name,
- Email: apiOpts.Committer.Email,
+ GitUserName: apiOpts.Committer.Name,
+ GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
- Name: apiOpts.Author.Name,
- Email: apiOpts.Author.Email,
+ GitUserName: apiOpts.Author.Name,
+ GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 14a1a8d1c4a..f96c432b929 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -132,13 +132,15 @@ func CreateFork(ctx *context.APIContext) {
}
return
}
- isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
- return
- } else if !isMember {
- ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name))
- return
+ if !ctx.Doer.IsAdmin {
+ isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
+ return
+ } else if !isMember {
+ ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name))
+ return
+ }
}
forker = org.AsUser()
}
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index 5e24dcf8912..95d7631da7b 100644
--- a/routers/api/v1/repo/patch.go
+++ b/routers/api/v1/repo/patch.go
@@ -58,12 +58,12 @@ func ApplyDiffPatch(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files.IdentityOptions{
- Name: apiOpts.Committer.Name,
- Email: apiOpts.Committer.Email,
+ GitUserName: apiOpts.Committer.Name,
+ GitUserEmail: apiOpts.Committer.Email,
},
Author: &files.IdentityOptions{
- Name: apiOpts.Author.Name,
- Email: apiOpts.Author.Email,
+ GitUserName: apiOpts.Author.Name,
+ GitUserEmail: apiOpts.Author.Email,
},
Dates: &files.CommitDateOptions{
Author: apiOpts.Dates.Author,
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 576220fea75..eea69a09cfe 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -204,6 +204,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
+ webhook_module.HookEventPackage: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
},
BranchFilter: form.BranchFilter,
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index 6c4cc11ca05..d3dae9503e8 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -43,6 +43,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri
},
Description: commit.Message(),
Content: commit.Message(),
+ Created: commit.Committer.When,
})
}
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 518d995ccbc..407e4fa2d5d 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -55,6 +55,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
},
Description: commit.Message(),
Content: commit.Message(),
+ Created: commit.Committer.When,
})
}
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 85f407ab8d7..48e041fb1dc 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -13,6 +13,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
@@ -102,10 +103,32 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
return treeNames, treePaths
}
-func editFile(ctx *context.Context, isNewFile bool) {
- ctx.Data["PageIsViewCode"] = true
+func getCandidateEmailAddresses(ctx *context.Context) []string {
+ emails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
+ if err != nil {
+ log.Error("getCandidateEmailAddresses: GetActivatedEmailAddresses: %v", err)
+ }
+
+ if ctx.Doer.KeepEmailPrivate {
+ emails = append([]string{ctx.Doer.GetPlaceholderEmail()}, emails...)
+ }
+ return emails
+}
+
+func editFileCommon(ctx *context.Context, isNewFile bool) {
ctx.Data["PageIsEdit"] = true
ctx.Data["IsNewFile"] = isNewFile
+ ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
+ ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
+ ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
+ ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
+ ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
+ ctx.Data["CommitCandidateEmails"] = getCandidateEmailAddresses(ctx)
+ ctx.Data["CommitDefaultEmail"] = ctx.Doer.GetEmail()
+}
+
+func editFile(ctx *context.Context, isNewFile bool) {
+ editFileCommon(ctx, isNewFile)
canCommit := renderCommitRights(ctx)
treePath := cleanUploadFileName(ctx.Repo.TreePath)
@@ -174,28 +197,19 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["FileContent"] = content
}
} else {
- // Append filename from query, or empty string to allow user name the new file.
+ // Append filename from query, or empty string to allow username the new file.
treeNames = append(treeNames, fileName)
}
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
+ ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch)
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
- ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
- ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
+ ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
ctx.HTML(http.StatusOK, tplEditFile)
}
@@ -224,6 +238,9 @@ func NewFile(ctx *context.Context) {
}
func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
+ editFileCommon(ctx, isNewFile)
+ ctx.Data["PageHasPosted"] = true
+
canCommit := renderCommitRights(ctx)
treeNames, treePaths := getParentTreeFields(form.TreePath)
branchName := ctx.Repo.BranchName
@@ -231,21 +248,15 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
branchName = form.NewBranchName
}
- ctx.Data["PageIsEdit"] = true
- ctx.Data["PageHasPosted"] = true
- ctx.Data["IsNewFile"] = isNewFile
ctx.Data["TreePath"] = form.TreePath
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
ctx.Data["FileContent"] = form.Content
ctx.Data["commit_summary"] = form.CommitSummary
ctx.Data["commit_message"] = form.CommitMessage
ctx.Data["commit_choice"] = form.CommitChoice
ctx.Data["new_branch_name"] = form.NewBranchName
ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
if ctx.HasError() {
@@ -253,7 +264,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
return
}
- // Cannot commit to a an existing branch if user doesn't have rights
+ // Cannot commit to an existing branch if user doesn't have rights
if branchName == ctx.Repo.BranchName && !canCommit {
ctx.Data["Err_NewBranchName"] = true
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
@@ -276,6 +287,17 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message += "\n\n" + form.CommitMessage
}
+ gitCommitter := &files_service.IdentityOptions{}
+ if form.CommitEmail != "" {
+ if util.SliceContainsString(getCandidateEmailAddresses(ctx), form.CommitEmail, true) {
+ gitCommitter.GitUserEmail = form.CommitEmail
+ } else {
+ ctx.Data["Err_CommitEmail"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form)
+ return
+ }
+ }
+
operation := "update"
if isNewFile {
operation = "create"
@@ -294,7 +316,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
},
},
- Signoff: form.Signoff,
+ Signoff: form.Signoff,
+ Author: gitCommitter,
+ Committer: gitCommitter,
}); err != nil {
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
if git.IsErrNotExist(err) {
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index 456efb96f6e..6c6e007b50c 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -412,3 +412,9 @@ func Home(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRepoHome)
}
+
+// HomeRedirect redirects from /tree/* to /src/* in order to maintain a similar URL structure.
+func HomeRedirect(ctx *context.Context) {
+ remainder := ctx.PathParam("*")
+ ctx.Redirect(ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(remainder))
+}
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index f77bd602b3d..81c00b3bd42 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -4,7 +4,6 @@
package user
import (
- "strings"
"time"
"code.gitea.io/gitea/models/avatars"
@@ -21,27 +20,18 @@ func cacheableRedirect(ctx *context.Context, location string) {
ctx.Redirect(location)
}
-// AvatarByUserName redirect browser to user avatar of requested size
-func AvatarByUserName(ctx *context.Context) {
- userName := ctx.PathParam("username")
- size := int(ctx.PathParamInt64("size"))
-
- var user *user_model.User
- if strings.ToLower(userName) != user_model.GhostUserLowerName {
+// AvatarByUsernameSize redirect browser to user avatar of requested size
+func AvatarByUsernameSize(ctx *context.Context) {
+ username := ctx.PathParam("username")
+ user := user_model.GetSystemUserByName(username)
+ if user == nil {
var err error
- if user, err = user_model.GetUserByName(ctx, userName); err != nil {
- if user_model.IsErrUserNotExist(err) {
- ctx.NotFound("GetUserByName", err)
- return
- }
- ctx.ServerError("Invalid user: "+userName, err)
+ if user, err = user_model.GetUserByName(ctx, username); err != nil {
+ ctx.NotFoundOrServerError("GetUserByName", user_model.IsErrUserNotExist, err)
return
}
- } else {
- user = user_model.NewGhostUser()
}
-
- cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, size))
+ cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, int(ctx.PathParamInt64("size"))))
}
// AvatarByEmailHash redirects the browser to the email avatar link
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index c4ed242f71b..ff9334da6e2 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -732,7 +732,7 @@ func UsernameSubRoute(ctx *context.Context) {
switch {
case strings.HasSuffix(username, ".png"):
if reloadParam(".png") {
- AvatarByUserName(ctx)
+ AvatarByUsernameSize(ctx)
}
case strings.HasSuffix(username, ".keys"):
if reloadParam(".keys") {
diff --git a/routers/web/web.go b/routers/web/web.go
index 5330b0f3c1b..096f1e6bbeb 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -681,7 +681,7 @@ func registerRoutes(m *web.Router) {
m.Get("/activate", auth.Activate)
m.Post("/activate", auth.ActivatePost)
m.Any("/activate_email", auth.ActivateEmail)
- m.Get("/avatar/{username}/{size}", user.AvatarByUserName)
+ m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize)
m.Get("/recover_account", auth.ResetPasswd)
m.Post("/recover_account", auth.ResetPasswdPost)
m.Get("/forgot_password", auth.ForgotPasswd)
@@ -1584,6 +1584,13 @@ func registerRoutes(m *web.Router) {
m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility
}, repo.SetEditorconfigIfExists)
+ // Add a /tree/* path to redirect to the /src/* path, which
+ // will redirect to the canonical URL for that ref. This is
+ // included so that Gitea's repo URL structure matches what
+ // other forges provide, allowing clients to construct URLs
+ // that work across forges.
+ m.Get("/tree/*", repo.HomeRedirect)
+
m.Get("/forks", context.RepoRef(), repo.Forks)
m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff)
m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit)
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 323c6a76e42..2d8885dc323 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,7 +117,7 @@ func (input *notifyInput) Notify(ctx context.Context) {
func notify(ctx context.Context, input *notifyInput) error {
shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
- if input.Doer.IsActions() {
+ if input.Doer.IsGiteaActions() {
// avoiding triggering cyclically, for example:
// a comment of an issue will trigger the runner to add a new comment as reply,
// and the new comment will trigger the runner again.
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 6a6ccda35f4..67e092d7740 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -721,6 +721,7 @@ type EditRepoFileForm struct {
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
LastCommit string
Signoff bool
+ CommitEmail string
}
// Validate validates the fields
diff --git a/services/gitdiff/submodule_test.go b/services/gitdiff/submodule_test.go
index 89f32c0e0c8..f0eab5557cf 100644
--- a/services/gitdiff/submodule_test.go
+++ b/services/gitdiff/submodule_test.go
@@ -230,7 +230,7 @@ func TestSubmoduleInfo(t *testing.T) {
assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx))
sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234")
- assert.EqualValues(t, `1111`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
+ assert.EqualValues(t, `1111`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
assert.EqualValues(t, `aaaa...bbbb`, sdi.CompareRefIDLinkHTML(ctx))
assert.EqualValues(t, `name`, sdi.SubmoduleRepoLinkHTML(ctx))
}
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index fab3315be21..e269b1ca1e2 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -109,7 +109,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo
}
visited.AddMultiple(ids...)
- unfilteredUsers, err := user_model.GetMaileableUsersByIDs(ctx, unfiltered, false)
+ unfilteredUsers, err := user_model.GetMailableUsersByIDs(ctx, unfiltered, false)
if err != nil {
return err
}
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index 796d63d27ae..31316b0053b 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -35,9 +35,9 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
return
}
- recipients, err := user_model.GetMaileableUsersByIDs(ctx, watcherIDList, false)
+ recipients, err := user_model.GetMailableUsersByIDs(ctx, watcherIDList, false)
if err != nil {
- log.Error("user_model.GetMaileableUsersByIDs: %v", err)
+ log.Error("user_model.GetMailableUsersByIDs: %v", err)
return
}
diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go
index e8a8313625d..88a463e4c61 100644
--- a/services/packages/cargo/index.go
+++ b/services/packages/cargo/index.go
@@ -11,7 +11,6 @@ import (
"io"
"path"
"strconv"
- "time"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
@@ -296,8 +295,13 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re
return err
}
- now := time.Now()
- commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now)
+ commitOpts := &files_service.CommitTreeUserOptions{
+ ParentCommitID: lastCommitID,
+ TreeHash: treeHash,
+ CommitMessage: commitMessage,
+ DoerUser: doer,
+ }
+ commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return err
}
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
index 10545e9e038..34572838039 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -32,15 +32,13 @@ func (err ErrCommitIDDoesNotMatch) Error() string {
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
}
-// CherryPick cherrypicks or reverts a commit to the given repository
+// CherryPick cherry-picks or reverts a commit to the given repository
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
if err := opts.Validate(ctx, repo, doer); err != nil {
return nil, err
}
message := strings.TrimSpace(opts.Message)
- author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
-
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
@@ -112,12 +110,21 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
}
// Now commit the tree
- var commitHash string
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: "HEAD",
+ TreeHash: treeHash,
+ CommitMessage: message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ AuthorIdentity: opts.Author,
+ AuthorTime: nil,
+ CommitterIdentity: opts.Committer,
+ CommitterTime: nil,
+ }
if opts.Dates != nil {
- commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
- } else {
- commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
+ commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
+ commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return nil, err
}
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index d7ca8e79e5b..2caa1b49469 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -11,7 +11,6 @@ import (
"time"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -111,51 +110,6 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap
return fileCommit, nil
}
-// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions
-func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_model.User) (authorUser, committerUser *user_model.User) {
- // Committer and author are optional. If they are not the doer (not same email address)
- // then we use bogus User objects for them to store their FullName and Email.
- // If only one of the two are provided, we set both of them to it.
- // If neither are provided, both are the doer.
- if committer != nil && committer.Email != "" {
- if doer != nil && strings.EqualFold(doer.Email, committer.Email) {
- committerUser = doer // the committer is the doer, so will use their user object
- if committer.Name != "" {
- committerUser.FullName = committer.Name
- }
- } else {
- committerUser = &user_model.User{
- FullName: committer.Name,
- Email: committer.Email,
- }
- }
- }
- if author != nil && author.Email != "" {
- if doer != nil && strings.EqualFold(doer.Email, author.Email) {
- authorUser = doer // the author is the doer, so will use their user object
- if authorUser.Name != "" {
- authorUser.FullName = author.Name
- }
- } else {
- authorUser = &user_model.User{
- FullName: author.Name,
- Email: author.Email,
- }
- }
- }
- if authorUser == nil {
- if committerUser != nil {
- authorUser = committerUser // No valid author was given so use the committer
- } else if doer != nil {
- authorUser = doer // No valid author was given and no valid committer so use the doer
- }
- }
- if committerUser == nil {
- committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above)
- }
- return authorUser, committerUser
-}
-
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
type ErrFilenameInvalid struct {
Path string
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index 38c17b4073d..78c275f01c8 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -126,8 +126,6 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
message := strings.TrimSpace(opts.Message)
- author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
-
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
@@ -187,12 +185,21 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
}
// Now commit the tree
- var commitHash string
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: "HEAD",
+ TreeHash: treeHash,
+ CommitMessage: message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ AuthorIdentity: opts.Author,
+ AuthorTime: nil,
+ CommitterIdentity: opts.Committer,
+ CommitterTime: nil,
+ }
if opts.Dates != nil {
- commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
- } else {
- commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
+ commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
+ commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return nil, err
}
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 138af991f9b..cf1402397b6 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
)
@@ -225,15 +226,53 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro
return strings.TrimSpace(stdout), nil
}
-// CommitTree creates a commit from a given tree for the user with provided message
-func (t *TemporaryUploadRepository) CommitTree(parent string, author, committer *user_model.User, treeHash, message string, signoff bool) (string, error) {
- return t.CommitTreeWithDate(parent, author, committer, treeHash, message, signoff, time.Now(), time.Now())
+type CommitTreeUserOptions struct {
+ ParentCommitID string
+ TreeHash string
+ CommitMessage string
+ SignOff bool
+
+ DoerUser *user_model.User
+
+ AuthorIdentity *IdentityOptions // if nil, use doer
+ AuthorTime *time.Time // if nil, use now
+ CommitterIdentity *IdentityOptions
+ CommitterTime *time.Time
}
-// CommitTreeWithDate creates a commit from a given tree for the user with provided message
-func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, committer *user_model.User, treeHash, message string, signoff bool, authorDate, committerDate time.Time) (string, error) {
- authorSig := author.NewGitSig()
- committerSig := committer.NewGitSig()
+func makeGitUserSignature(doer *user_model.User, identity, other *IdentityOptions) *git.Signature {
+ gitSig := &git.Signature{}
+ if identity != nil {
+ gitSig.Name, gitSig.Email = identity.GitUserName, identity.GitUserEmail
+ }
+ if other != nil {
+ gitSig.Name = util.IfZero(gitSig.Name, other.GitUserName)
+ gitSig.Email = util.IfZero(gitSig.Email, other.GitUserEmail)
+ }
+ if gitSig.Name == "" {
+ gitSig.Name = doer.GitName()
+ }
+ if gitSig.Email == "" {
+ gitSig.Email = doer.GetEmail()
+ }
+ return gitSig
+}
+
+// CommitTree creates a commit from a given tree for the user with provided message
+func (t *TemporaryUploadRepository) CommitTree(opts *CommitTreeUserOptions) (string, error) {
+ authorSig := makeGitUserSignature(opts.DoerUser, opts.AuthorIdentity, opts.CommitterIdentity)
+ committerSig := makeGitUserSignature(opts.DoerUser, opts.CommitterIdentity, opts.AuthorIdentity)
+
+ authorDate := opts.AuthorTime
+ committerDate := opts.CommitterTime
+ if authorDate == nil && committerDate == nil {
+ authorDate = util.ToPointer(time.Now())
+ committerDate = authorDate
+ } else if authorDate == nil {
+ authorDate = committerDate
+ } else if committerDate == nil {
+ committerDate = authorDate
+ }
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
@@ -244,21 +283,21 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
)
messageBytes := new(bytes.Buffer)
- _, _ = messageBytes.WriteString(message)
+ _, _ = messageBytes.WriteString(opts.CommitMessage)
_, _ = messageBytes.WriteString("\n")
- cmdCommitTree := git.NewCommand(t.ctx, "commit-tree").AddDynamicArguments(treeHash)
- if parent != "" {
- cmdCommitTree.AddOptionValues("-p", parent)
+ cmdCommitTree := git.NewCommand(t.ctx, "commit-tree").AddDynamicArguments(opts.TreeHash)
+ if opts.ParentCommitID != "" {
+ cmdCommitTree.AddOptionValues("-p", opts.ParentCommitID)
}
var sign bool
var keyID string
var signer *git.Signature
- if parent != "" {
- sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent)
+ if opts.ParentCommitID != "" {
+ sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID)
} else {
- sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author)
+ sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), opts.DoerUser)
}
if sign {
cmdCommitTree.AddOptionFormat("-S%s", keyID)
@@ -279,7 +318,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
cmdCommitTree.AddArguments("--no-gpg-sign")
}
- if signoff {
+ if opts.SignOff {
// Signed-off-by
_, _ = messageBytes.WriteString("\n")
_, _ = messageBytes.WriteString("Signed-off-by: ")
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index a2763105b0c..a707ea8bb6f 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -27,8 +27,8 @@ import (
// IdentityOptions for a person's identity like an author or committer
type IdentityOptions struct {
- Name string
- Email string
+ GitUserName string // to match "git config user.name"
+ GitUserEmail string // to match "git config user.email"
}
// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
@@ -160,8 +160,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
message := strings.TrimSpace(opts.Message)
- author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
-
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
@@ -262,12 +260,21 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// Now commit the tree
- var commitHash string
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: opts.LastCommitID,
+ TreeHash: treeHash,
+ CommitMessage: message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ AuthorIdentity: opts.Author,
+ AuthorTime: nil,
+ CommitterIdentity: opts.Committer,
+ CommitterTime: nil,
+ }
if opts.Dates != nil {
- commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
- } else {
- commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff)
+ commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
+ commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return nil, err
}
diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go
index cbfaf49d131..af32bc4c85b 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -128,12 +128,15 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return err
}
- // make author and committer the doer
- author := doer
- committer := doer
-
// Now commit the tree
- commitHash, err := t.CommitTree(opts.LastCommitID, author, committer, treeHash, opts.Message, opts.Signoff)
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: opts.LastCommitID,
+ TreeHash: treeHash,
+ CommitMessage: opts.Message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ }
+ commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return err
}
diff --git a/services/repository/fork.go b/services/repository/fork.go
index cff0b1a4036..8d89c2b0b02 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -256,9 +256,11 @@ type findForksOptions struct {
}
func (opts findForksOptions) ToConds() builder.Cond {
- return builder.Eq{"fork_id": opts.RepoID}.And(
- repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
- )
+ cond := builder.Eq{"fork_id": opts.RepoID}
+ if opts.Doer != nil && opts.Doer.IsAdmin {
+ return cond
+ }
+ return cond.And(repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid))
}
// FindForks returns all the forks of the repository
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 91746667ffb..3ea8f50764d 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -196,3 +196,7 @@ func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_
var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest)
+}
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 52829262ae3..43e5e533bff 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -283,6 +283,10 @@ func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m
return newJSONRequest(pc, w, t, true)
}
+func init() {
+ RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest)
+}
+
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
switch event {
case webhook_module.HookEventPullRequestReviewApproved:
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 99d2b52c0f8..639118d2a5e 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -176,3 +176,7 @@ func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mo
var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest)
+}
diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go
index ef1ec7f324c..ec735d785a5 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -319,8 +319,8 @@ func packageTestPayload() *api.PackagePayload {
AvatarURL: "http://localhost:3000/user1/avatar",
},
Repository: nil,
- Organization: &api.User{
- UserName: "org1",
+ Organization: &api.Organization{
+ Name: "org1",
AvatarURL: "http://localhost:3000/org1/avatar",
},
Package: &api.Package{
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index cc51a1d23f1..ec21712837b 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -24,6 +24,10 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
)
+func init() {
+ RegisterWebhookRequester(webhook_module.MATRIX, newMatrixRequest)
+}
+
func newMatrixRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &MatrixMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index a528536b27c..485f695be20 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -363,3 +363,7 @@ func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m
var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest)
+}
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 2fce4b351e3..6c691c21f43 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -8,6 +8,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
@@ -920,10 +921,16 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo
return
}
+ var org *api.Organization
+ if pd.Owner.IsOrganization() {
+ org = convert.ToOrganization(ctx, organization.OrgFromUser(pd.Owner))
+ }
+
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventPackage, &api.PackagePayload{
- Action: action,
- Package: apiPackage,
- Sender: convert.ToUser(ctx, sender, nil),
+ Action: action,
+ Package: apiPackage,
+ Organization: org,
+ Sender: convert.ToUser(ctx, sender, nil),
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 1f0c0c1f63e..6864fc822ab 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -124,3 +124,7 @@ func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook
}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.PACKAGIST, newPackagistRequest)
+}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index aea4ea2615c..80ed747fd1f 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -301,6 +301,10 @@ func newSlackRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mod
return newJSONRequest(pc, w, t, true)
}
+func init() {
+ RegisterWebhookRequester(webhook_module.SLACK, newSlackRequest)
+}
+
var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
// IsValidSlackChannel validates a channel name conforms to what slack expects:
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 6ccaaa82a1d..485e2d990ba 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -193,3 +193,7 @@ func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_
var pc payloadConvertor[TelegramPayload] = telegramConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.TELEGRAM, newTelegramRequest)
+}
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index b4609e8a518..182078b39d4 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -27,16 +27,12 @@ import (
"github.com/gobwas/glob"
)
-var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){
- webhook_module.SLACK: newSlackRequest,
- webhook_module.DISCORD: newDiscordRequest,
- webhook_module.DINGTALK: newDingtalkRequest,
- webhook_module.TELEGRAM: newTelegramRequest,
- webhook_module.MSTEAMS: newMSTeamsRequest,
- webhook_module.FEISHU: newFeishuRequest,
- webhook_module.MATRIX: newMatrixRequest,
- webhook_module.WECHATWORK: newWechatworkRequest,
- webhook_module.PACKAGIST: newPackagistRequest,
+type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
+
+var webhookRequesters = map[webhook_module.HookType]Requester{}
+
+func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) {
+ webhookRequesters[hookType] = requester
}
// IsValidHookTaskType returns true if a webhook registered
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 42b23fd9954..1c834b4020a 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -185,3 +185,7 @@ func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhoo
var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.WECHATWORK, newWechatworkRequest)
+}
diff --git a/stylelint.config.js b/stylelint.config.js
index 977c35d9d5d..1153bf7308b 100644
--- a/stylelint.config.js
+++ b/stylelint.config.js
@@ -1,3 +1,5 @@
+// @ts-check
+import {defineConfig} from 'stylelint-define-config';
import {fileURLToPath} from 'node:url';
const cssVarFiles = [
@@ -6,8 +8,8 @@ const cssVarFiles = [
fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
];
-/** @type {import('stylelint').Config} */
-export default {
+export default defineConfig({
+ extends: 'stylelint-config-recommended',
plugins: [
'stylelint-declaration-strict-value',
'stylelint-declaration-block-no-ignored-properties',
@@ -67,7 +69,7 @@ export default {
'@stylistic/function-comma-space-after': null,
'@stylistic/function-comma-space-before': null,
'@stylistic/function-max-empty-lines': 0,
- '@stylistic/function-parentheses-newline-inside': 'never-multi-line',
+ '@stylistic/function-parentheses-newline-inside': null,
'@stylistic/function-parentheses-space-inside': null,
'@stylistic/function-whitespace-after': null,
'@stylistic/indentation': 2,
@@ -114,134 +116,34 @@ export default {
'@stylistic/value-list-comma-space-after': null,
'@stylistic/value-list-comma-space-before': null,
'@stylistic/value-list-max-empty-lines': 0,
- 'alpha-value-notation': null,
- 'annotation-no-unknown': true,
- 'at-rule-allowed-list': null,
- 'at-rule-disallowed-list': null,
- 'at-rule-empty-line-before': null,
'at-rule-no-unknown': [true, {ignoreAtRules: ['tailwind']}],
'at-rule-no-vendor-prefix': true,
- 'at-rule-property-required-list': null,
- 'block-no-empty': true,
- 'color-function-notation': null,
- 'color-hex-alpha': null,
- 'color-hex-length': null,
- 'color-named': null,
- 'color-no-hex': null,
- 'color-no-invalid-hex': true,
- 'comment-empty-line-before': null,
- 'comment-no-empty': true,
- 'comment-pattern': null,
- 'comment-whitespace-inside': null,
- 'comment-word-disallowed-list': null,
'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}],
- 'custom-media-pattern': null,
- 'custom-property-empty-line-before': null,
- 'custom-property-no-missing-var-function': true,
- 'custom-property-pattern': null,
- 'declaration-block-no-duplicate-custom-properties': true,
'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}],
- 'declaration-block-no-redundant-longhand-properties': [true, {ignoreShorthands: ['flex-flow', 'overflow']}],
- 'declaration-block-no-shorthand-property-overrides': null,
- 'declaration-block-single-line-max-declarations': null,
- 'declaration-empty-line-before': null,
- 'declaration-no-important': null,
- 'declaration-property-max-values': null,
- 'declaration-property-unit-allowed-list': null,
+ 'declaration-block-no-redundant-longhand-properties': [true, {ignoreShorthands: ['flex-flow', 'overflow', 'grid-template']}],
+ // @ts-expect-error - https://github.com/stylelint-types/stylelint-define-config/issues/1
'declaration-property-unit-disallowed-list': {'line-height': ['em']},
- 'declaration-property-value-allowed-list': null,
+ // @ts-expect-error - https://github.com/stylelint-types/stylelint-define-config/issues/1
'declaration-property-value-disallowed-list': {'word-break': ['break-word']},
- 'declaration-property-value-no-unknown': true,
'font-family-name-quotes': 'always-where-recommended',
- 'font-family-no-duplicate-names': true,
- 'font-family-no-missing-generic-family-keyword': true,
- 'font-weight-notation': null,
- 'function-allowed-list': null,
- 'function-calc-no-unspaced-operator': true,
- 'function-disallowed-list': null,
- 'function-linear-gradient-no-nonstandard-direction': true,
'function-name-case': 'lower',
- 'function-no-unknown': true,
- 'function-url-no-scheme-relative': null,
'function-url-quotes': 'always',
- 'function-url-scheme-allowed-list': null,
- 'function-url-scheme-disallowed-list': null,
- 'hue-degree-notation': null,
'import-notation': 'string',
- 'keyframe-block-no-duplicate-selectors': true,
- 'keyframe-declaration-no-important': true,
- 'keyframe-selector-notation': null,
- 'keyframes-name-pattern': null,
- 'length-zero-no-unit': [true, {ignore: ['custom-properties']}, {ignoreFunctions: ['var']}],
- 'max-nesting-depth': null,
- 'media-feature-name-allowed-list': null,
- 'media-feature-name-disallowed-list': null,
- 'media-feature-name-no-unknown': true,
+ 'length-zero-no-unit': [true, {ignore: ['custom-properties'], ignoreFunctions: ['var']}],
'media-feature-name-no-vendor-prefix': true,
- 'media-feature-name-unit-allowed-list': null,
- 'media-feature-name-value-allowed-list': null,
- 'media-feature-name-value-no-unknown': true,
- 'media-feature-range-notation': null,
- 'media-query-no-invalid': true,
- 'named-grid-areas-no-invalid': true,
'no-descending-specificity': null,
- 'no-duplicate-at-import-rules': true,
- 'no-duplicate-selectors': true,
- 'no-empty-source': true,
- 'no-invalid-double-slash-comments': true,
'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
- 'no-irregular-whitespace': true,
'no-unknown-animations': null, // disabled until stylelint supports multi-file linting
'no-unknown-custom-media': null, // disabled until stylelint supports multi-file linting
'no-unknown-custom-properties': null, // disabled until stylelint supports multi-file linting
- 'number-max-precision': null,
'plugin/declaration-block-no-ignored-properties': true,
- 'property-allowed-list': null,
- 'property-disallowed-list': null,
- 'property-no-unknown': true,
- 'property-no-vendor-prefix': null,
- 'rule-empty-line-before': null,
- 'rule-selector-property-disallowed-list': null,
- 'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: false, disableFix: true, expandShorthand: true}],
- 'selector-anb-no-unmatchable': true,
- 'selector-attribute-name-disallowed-list': null,
- 'selector-attribute-operator-allowed-list': null,
- 'selector-attribute-operator-disallowed-list': null,
+ 'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: true, disableFix: true, expandShorthand: true}],
'selector-attribute-quotes': 'always',
- 'selector-class-pattern': null,
- 'selector-combinator-allowed-list': null,
- 'selector-combinator-disallowed-list': null,
- 'selector-disallowed-list': null,
- 'selector-id-pattern': null,
- 'selector-max-attribute': null,
- 'selector-max-class': null,
- 'selector-max-combinators': null,
- 'selector-max-compound-selectors': null,
- 'selector-max-id': null,
- 'selector-max-pseudo-class': null,
- 'selector-max-specificity': null,
- 'selector-max-type': null,
- 'selector-max-universal': null,
- 'selector-nested-pattern': null,
- 'selector-no-qualifying-type': null,
'selector-no-vendor-prefix': true,
- 'selector-not-notation': null,
- 'selector-pseudo-class-allowed-list': null,
- 'selector-pseudo-class-disallowed-list': null,
- 'selector-pseudo-class-no-unknown': true,
- 'selector-pseudo-element-allowed-list': null,
'selector-pseudo-element-colon-notation': 'double',
- 'selector-pseudo-element-disallowed-list': null,
- 'selector-pseudo-element-no-unknown': true,
'selector-type-case': 'lower',
'selector-type-no-unknown': [true, {ignore: ['custom-elements']}],
'shorthand-property-no-redundant-values': true,
- 'string-no-newline': true,
- 'time-min-milliseconds': null,
- 'unit-allowed-list': null,
- 'unit-disallowed-list': null,
- 'unit-no-unknown': true,
- 'value-keyword-case': null,
'value-no-vendor-prefix': [true, {ignoreValues: ['box', 'inline-box']}],
},
-};
+});
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index c050324e933..8f46c47b96b 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -66,6 +66,16 @@
{{end}}
+ {{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
+