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)}} +
+ + +
+ {{end}}