Merge branch 'main' into lunny/fix_webhook

pull/33320/head
Lunny Xiao 1 day ago
commit 1125967fa0
  1. 8
      models/issues/reaction.go
  2. 21
      models/user/avatar.go
  3. 40
      models/user/avatar_test.go
  4. 15
      models/user/email_address.go
  5. 5
      models/user/openid.go
  6. 112
      models/user/user.go
  7. 47
      models/user/user_system.go
  8. 32
      models/user/user_system_test.go
  9. 4
      models/user/user_test.go
  10. 4
      modules/git/commit_submodule_file.go
  11. 4
      modules/git/commit_submodule_file_test.go
  12. 2
      modules/git/commit_test.go
  13. 8
      modules/git/repo_commit.go
  14. 4
      modules/storage/storage.go
  15. 60
      modules/structs/hook.go
  16. 2
      options/locale/locale_en-US.ini
  17. 2
      options/locale/locale_fr-FR.ini
  18. 2
      options/locale/locale_ga-IE.ini
  19. 4
      options/locale/locale_pt-BR.ini
  20. 63
      package-lock.json
  21. 6
      package.json
  22. 21
      poetry.lock
  23. 1
      poetry.toml
  24. 32
      routers/api/v1/repo/file.go
  25. 16
      routers/api/v1/repo/fork.go
  26. 8
      routers/api/v1/repo/patch.go
  27. 1
      routers/api/v1/utils/hook.go
  28. 1
      routers/web/feed/branch.go
  29. 1
      routers/web/feed/file.go
  30. 68
      routers/web/repo/editor.go
  31. 6
      routers/web/repo/view_home.go
  32. 26
      routers/web/user/avatar.go
  33. 2
      routers/web/user/home.go
  34. 9
      routers/web/web.go
  35. 2
      services/actions/notifier_helper.go
  36. 1
      services/forms/repo_form.go
  37. 2
      services/gitdiff/submodule_test.go
  38. 2
      services/mailer/mail_issue.go
  39. 4
      services/mailer/mail_release.go
  40. 10
      services/packages/cargo/index.go
  41. 21
      services/repository/files/cherry_pick.go
  42. 46
      services/repository/files/file.go
  43. 19
      services/repository/files/patch.go
  44. 69
      services/repository/files/temp_repo.go
  45. 23
      services/repository/files/update.go
  46. 13
      services/repository/files/upload.go
  47. 8
      services/repository/fork.go
  48. 4
      services/webhook/dingtalk.go
  49. 4
      services/webhook/discord.go
  50. 4
      services/webhook/feishu.go
  51. 4
      services/webhook/general_test.go
  52. 4
      services/webhook/matrix.go
  53. 4
      services/webhook/msteams.go
  54. 13
      services/webhook/notifier.go
  55. 4
      services/webhook/packagist.go
  56. 4
      services/webhook/slack.go
  57. 4
      services/webhook/telegram.go
  58. 16
      services/webhook/webhook.go
  59. 4
      services/webhook/wechatwork.go
  60. 120
      stylelint.config.js
  61. 10
      templates/repo/editor/commit_form.tmpl
  62. 72
      tests/integration/actions_trigger_test.go
  63. 33
      tests/integration/api_fork_test.go
  64. 15
      tests/integration/api_repo_test.go
  65. 24
      tests/integration/api_wiki_test.go
  66. 106
      tests/integration/editor_test.go
  67. 35
      tests/integration/feed_repo_test.go
  68. 25
      tests/integration/feed_user_test.go
  69. 69
      tests/integration/git_general_test.go
  70. 2
      tests/integration/issue_test.go
  71. 16
      tests/integration/pull_update_test.go
  72. 3
      tests/integration/repo_fork_test.go
  73. 523
      tests/integration/repo_webhook_test.go
  74. 4
      tests/integration/repofiles_change_test.go
  75. 6
      tests/mssql.ini.tmpl
  76. 6
      tests/mysql.ini.tmpl
  77. 6
      tests/pgsql.ini.tmpl
  78. 6
      tests/sqlite.ini.tmpl
  79. 2
      tsconfig.json
  80. 1
      web_src/css/repo/home-file-list.css
  81. 18
      web_src/js/features/comp/TextExpander.ts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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 <b>%s</b> %s.`
issues.add_project_at=`a ajouté ça au projet <b>%s</b> %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 <b><strike>%s</strike></b> par <b>%s</b> %s.`
issues.change_project_at=`a remplacé le projet <b><strike>%s</strike></b> par <b>%s</b> %s.`
issues.remove_milestone_at=`a supprimé ça du jalon <b>%s</b> %s.`

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

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

63
package-lock.json generated

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

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

21
poetry.lock generated

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

@ -1,4 +1,3 @@
[virtualenvs]
in-project = true
options.no-pip = true
options.no-setuptools = true

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

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

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

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

@ -43,6 +43,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri
},
Description: commit.Message(),
Content: commit.Message(),
Created: commit.Committer.When,
})
}

@ -55,6 +55,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
},
Description: commit.Message(),
Content: commit.Message(),
Created: commit.Committer.When,
})
}

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

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

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

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

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

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

@ -721,6 +721,7 @@ type EditRepoFileForm struct {
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
LastCommit string
Signoff bool
CommitEmail string
}
// Validate validates the fields

@ -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, `<a href="https://github.com/owner/repo/commit/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
assert.EqualValues(t, `<a href="https://github.com/owner/repo/tree/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx))
assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx))
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -66,6 +66,16 @@
</div>
{{end}}
</div>
{{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
<div class="field {{if .Err_CommitEmail}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label>
<select class="ui selection dropdown" name="commit_email">
{{- range $email := .CommitCandidateEmails -}}
<option {{if eq $email $.CommitDefaultEmail}}selected{{end}} value="{{$email}}">{{$email}}</option>
{{- end -}}
</select>
</div>
{{end}}
</div>
<button id="commit-button" type="submit" class="ui primary button">
{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}

@ -81,12 +81,12 @@ func TestPullRequestTargetEvent(t *testing.T) {
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -109,12 +109,12 @@ func TestPullRequestTargetEvent(t *testing.T) {
OldBranch: "main",
NewBranch: "fork-branch-1",
Author: &files_service.IdentityOptions{
Name: user4.Name,
Email: user4.Email,
GitUserName: user4.Name,
GitUserEmail: user4.Email,
},
Committer: &files_service.IdentityOptions{
Name: user4.Name,
Email: user4.Email,
GitUserName: user4.Name,
GitUserEmail: user4.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -164,12 +164,12 @@ func TestPullRequestTargetEvent(t *testing.T) {
OldBranch: "main",
NewBranch: "fork-branch-2",
Author: &files_service.IdentityOptions{
Name: user4.Name,
Email: user4.Email,
GitUserName: user4.Name,
GitUserEmail: user4.Email,
},
Committer: &files_service.IdentityOptions{
Name: user4.Name,
Email: user4.Email,
GitUserName: user4.Name,
GitUserEmail: user4.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -237,12 +237,12 @@ func TestSkipCI(t *testing.T) {
OldBranch: "master",
NewBranch: "master",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -268,12 +268,12 @@ func TestSkipCI(t *testing.T) {
OldBranch: "master",
NewBranch: "master",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -299,12 +299,12 @@ func TestSkipCI(t *testing.T) {
OldBranch: "master",
NewBranch: "test-skip-ci",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -356,12 +356,12 @@ func TestCreateDeleteRefEvent(t *testing.T) {
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -470,12 +470,12 @@ func TestPullRequestCommitStatusEvent(t *testing.T) {
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -576,12 +576,12 @@ func TestPullRequestCommitStatusEvent(t *testing.T) {
OldBranch: testBranch,
NewBranch: testBranch,
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),

@ -10,6 +10,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
@ -81,8 +82,8 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
assert.Len(t, forks, 1)
assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
assert.Len(t, forks, 2)
assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
assert.NoError(t, org_service.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
@ -96,3 +97,31 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
})
}
func TestGetPrivateReposForks(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user1Sess := loginUser(t, "user1")
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // private repository
privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository)
forkedRepoName := "forked-repo"
// create fork from a private repository
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{
Organization: &privateOrg.Name,
Name: &forkedRepoName,
}).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusAccepted)
// test get a private fork without clear permissions
req = NewRequest(t, "GET", "/api/v1/repos/"+repo2.FullName()+"/forks").AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
forks := []*api.Repository{}
DecodeJSON(t, resp, &forks)
assert.Len(t, forks, 1)
assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
assert.EqualValues(t, "forked-repo", forks[0].Name)
assert.EqualValues(t, privateOrg.Name, forks[0].Owner.UserName)
}

@ -471,6 +471,15 @@ func TestAPIMirrorSyncNonMirrorRepo(t *testing.T) {
assert.Equal(t, "Repository is not a mirror", errRespJSON["message"])
}
func testAPIOrgCreateRepo(t *testing.T, session *TestSession, orgName, repoName string, status int) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", orgName), &api.CreateRepoOption{
Name: repoName,
}).AddTokenAuth(token)
MakeRequest(t, req, status)
}
func TestAPIOrgRepoCreate(t *testing.T) {
testCases := []struct {
ctxUserID int64
@ -488,11 +497,7 @@ func TestAPIOrgRepoCreate(t *testing.T) {
for _, testCase := range testCases {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", testCase.orgName), &api.CreateRepoOption{
Name: testCase.repoName,
}).AddTokenAuth(token)
MakeRequest(t, req, testCase.expectedStatus)
testAPIOrgCreateRepo(t, session, testCase.orgName, testCase.repoName, testCase.expectedStatus)
}
}

@ -172,6 +172,19 @@ func TestAPIListWikiPages(t *testing.T) {
assert.Equal(t, dummymeta, meta)
}
func testAPICreateWikiPage(t *testing.T, session *TestSession, userName, repoName, title string, status int) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", userName, repoName)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
Title: title,
ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")),
Message: "",
}).AddTokenAuth(token)
MakeRequest(t, req, status)
}
func TestAPINewWikiPage(t *testing.T) {
for _, title := range []string{
"New page",
@ -180,16 +193,7 @@ func TestAPINewWikiPage(t *testing.T) {
defer tests.PrepareTestEnv(t)()
username := "user2"
session := loginUser(t, username)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", username, "repo1")
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
Title: title,
ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")),
Message: "",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
testAPICreateWikiPage(t, session, username, "repo1", title, http.StatusCreated)
}
}

@ -11,9 +11,16 @@ import (
"path"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateFile(t *testing.T) {
@ -173,3 +180,102 @@ func TestEditFileToNewBranch(t *testing.T) {
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
})
}
func TestEditFileCommitEmail(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.True(t, user.KeepEmailPrivate)
session := loginUser(t, user.Name)
link := "/user2/repo1/_edit/master/README.md"
getLastCommitID := func(t *testing.T) string {
req := NewRequest(t, "GET", link)
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
lastCommit := htmlDoc.GetInputValueByName("last_commit")
require.NotEmpty(t, lastCommit)
return lastCommit
}
newReq := func(t *testing.T, session *TestSession, email, content string) *RequestWrapper {
req := NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"last_commit": getLastCommitID(t),
"tree_path": "README.md",
"content": content,
"commit_choice": "direct",
"commit_email": email,
})
return req
}
t.Run("EmailInactive", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
assert.False(t, email.IsActivated)
req := newReq(t, session, email.Email, "test content")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Contains(t,
htmlDoc.doc.Find(".ui.negative.message").Text(),
translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email"),
)
})
t.Run("EmailInvalid", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
assert.NotEqualValues(t, email.UID, user.ID)
req := newReq(t, session, email.Email, "test content")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Contains(t,
htmlDoc.doc.Find(".ui.negative.message").Text(),
translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email"),
)
})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
defer gitRepo.Close()
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := newReq(t, session, "", "privacy email")
session.MakeRequest(t, req, http.StatusSeeOther)
commit, err := gitRepo.GetCommitByPath("README.md")
assert.NoError(t, err)
fileContent, err := commit.GetFileContent("README.md", 64)
assert.NoError(t, err)
assert.EqualValues(t, "privacy email", fileContent)
assert.EqualValues(t, "User Two", commit.Author.Name)
assert.EqualValues(t, "user2@noreply.example.org", commit.Author.Email)
assert.EqualValues(t, "User Two", commit.Committer.Name)
assert.EqualValues(t, "user2@noreply.example.org", commit.Committer.Email)
})
t.Run("ChooseEmail", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 3, UID: user.ID, IsActivated: true})
req := newReq(t, session, email.Email, "chosen email")
session.MakeRequest(t, req, http.StatusSeeOther)
commit, err := gitRepo.GetCommitByPath("README.md")
assert.NoError(t, err)
fileContent, err := commit.GetFileContent("README.md", 64)
assert.NoError(t, err)
assert.EqualValues(t, "chosen email", fileContent)
assert.EqualValues(t, "User Two", commit.Author.Name)
assert.EqualValues(t, email.Email, commit.Author.Email)
assert.EqualValues(t, "User Two", commit.Committer.Name)
assert.EqualValues(t, email.Email, commit.Committer.Email)
})
})
}

@ -0,0 +1,35 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/xml"
"net/http"
"testing"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestFeedRepo(t *testing.T) {
t.Run("RSS", func(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/user2/repo1.rss")
resp := MakeRequest(t, req, http.StatusOK)
data := resp.Body.String()
assert.Contains(t, data, `<rss version="2.0"`)
var rss RSS
err := xml.Unmarshal(resp.Body.Bytes(), &rss)
assert.NoError(t, err)
assert.Contains(t, rss.Channel.Link, "/user2/repo1")
assert.NotEmpty(t, rss.Channel.PubDate)
assert.Len(t, rss.Channel.Items, 1)
assert.EqualValues(t, "issue5", rss.Channel.Items[0].Description)
assert.NotEmpty(t, rss.Channel.Items[0].PubDate)
})
}

@ -4,6 +4,7 @@
package integration
import (
"encoding/xml"
"net/http"
"testing"
@ -12,7 +13,23 @@ import (
"github.com/stretchr/testify/assert"
)
func TestFeed(t *testing.T) {
// RSS is a struct to unmarshal RSS feeds test only
type RSS struct {
Channel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
Items []struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
} `xml:"item"`
} `xml:"channel"`
}
func TestFeedUser(t *testing.T) {
t.Run("User", func(t *testing.T) {
t.Run("Atom", func(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@ -32,6 +49,12 @@ func TestFeed(t *testing.T) {
data := resp.Body.String()
assert.Contains(t, data, `<rss version="2.0"`)
var rss RSS
err := xml.Unmarshal(resp.Body.Bytes(), &rss)
assert.NoError(t, err)
assert.Contains(t, rss.Channel.Link, "/user2")
assert.NotEmpty(t, rss.Channel.PubDate)
})
})
}

@ -80,6 +80,7 @@ func testGitGeneral(t *testing.T, u *url.URL) {
mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
t.Run("CreateProtectedBranch", doCreateProtectedBranch(&httpContext, dstPath))
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath))
t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
@ -121,6 +122,7 @@ func testGitGeneral(t *testing.T, u *url.URL) {
mediaTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2"))
t.Run("CreateProtectedBranch", doCreateProtectedBranch(&sshContext, dstPath))
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
t.Run("MergeFork", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -325,6 +327,34 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin
return filepath.Base(tmpFile.Name()), err
}
func doCreateProtectedBranch(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
return func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
t.Run("ProtectBranchWithFilePatterns", doProtectBranch(ctx, "release-*", baseCtx.Username, "", "", "config*"))
// push a new branch without any new commits
t.Run("CreateProtectedBranch-NoChanges", doGitCreateBranch(dstPath, "release-v1.0"))
t.Run("PushProtectedBranch-NoChanges", doGitPushTestRepository(dstPath, "origin", "release-v1.0"))
t.Run("CheckoutMaster-NoChanges", doGitCheckoutBranch(dstPath, "master"))
// push a new branch with a new unprotected file
t.Run("CreateProtectedBranch-UnprotectedFile", doGitCreateBranch(dstPath, "release-v2.0"))
_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "abc.txt")
assert.NoError(t, err)
t.Run("PushProtectedBranch-UnprotectedFile", doGitPushTestRepository(dstPath, "origin", "release-v2.0"))
t.Run("CheckoutMaster-UnprotectedFile", doGitCheckoutBranch(dstPath, "master"))
// push a new branch with a new protected file
t.Run("CreateProtectedBranch-ProtectedFile", doGitCreateBranch(dstPath, "release-v3.0"))
_, err = generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "config")
assert.NoError(t, err)
t.Run("PushProtectedBranch-ProtectedFile", doGitPushTestRepositoryFail(dstPath, "origin", "release-v3.0"))
t.Run("CheckoutMaster-ProtectedFile", doGitCheckoutBranch(dstPath, "master"))
}
}
func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
return func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -334,27 +364,23 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
// Protect branch without any whitelisting
t.Run("ProtectBranchNoWhitelist", func(t *testing.T) {
doProtectBranch(ctx, "protected", "", "", "")
})
t.Run("ProtectBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", "", ""))
// Try to push without permissions, which should fail
t.Run("TryPushWithoutPermissions", func(t *testing.T) {
_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
doGitPushTestRepositoryFail(dstPath, "origin", "protected")
doGitPushTestRepositoryFail(dstPath, "origin", "protected")(t)
})
// Set up permissions for normal push but not force push
t.Run("SetupNormalPushPermissions", func(t *testing.T) {
doProtectBranch(ctx, "protected", baseCtx.Username, "", "")
})
t.Run("SetupNormalPushPermissions", doProtectBranch(ctx, "protected", baseCtx.Username, "", "", ""))
// Normal push should work
t.Run("NormalPushWithPermissions", func(t *testing.T) {
_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
doGitPushTestRepository(dstPath, "origin", "protected")
doGitPushTestRepository(dstPath, "origin", "protected")(t)
})
// Try to force push without force push permissions, which should fail
@ -364,30 +390,22 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
assert.NoError(t, err)
})
doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")(t)
})
// Set up permissions for force push but not normal push
t.Run("SetupForcePushPermissions", func(t *testing.T) {
doProtectBranch(ctx, "protected", "", baseCtx.Username, "")
})
t.Run("SetupForcePushPermissions", doProtectBranch(ctx, "protected", "", baseCtx.Username, "", ""))
// Try to force push without normal push permissions, which should fail
t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) {
doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
})
t.Run("ForcePushWithoutNormalPermissions", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected"))
// Set up permissions for normal and force push (both are required to force push)
t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) {
doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "")
})
t.Run("SetupNormalAndForcePushPermissions", doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "", ""))
// Force push should now work
t.Run("ForcePushWithPermissions", func(t *testing.T) {
doGitPushTestRepository(dstPath, "-f", "origin", "protected")
})
t.Run("ForcePushWithPermissions", doGitPushTestRepository(dstPath, "-f", "origin", "protected"))
t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", ""))
t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", "", ""))
t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
var pr api.PullRequest
var err error
@ -409,14 +427,14 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*"))
t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*", ""))
t.Run("GenerateCommit", func(t *testing.T) {
_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "unprotected-file-")
assert.NoError(t, err)
})
t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", ""))
t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", "", ""))
t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
@ -431,7 +449,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
}
}
func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) {
func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns, protectedFilePatterns string) func(t *testing.T) {
// We are going to just use the owner to set the protection.
return func(t *testing.T) {
csrf := GetUserCSRFToken(t, ctx.Session)
@ -440,6 +458,7 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhit
"_csrf": csrf,
"rule_name": branch,
"unprotected_file_patterns": unprotectedFilePatterns,
"protected_file_patterns": protectedFilePatterns,
}
if userToWhitelistPush != "" {

@ -174,7 +174,7 @@ func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content,
htmlDoc = NewHTMLParser(t, resp.Body)
val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
val := strings.TrimSpace(htmlDoc.doc.Find(".comment-list .comment .render-content").Eq(commentCount).Text())
assert.Equal(t, content, val)
idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")

@ -115,12 +115,12 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
OldBranch: "master",
NewBranch: "master",
Author: &files_service.IdentityOptions{
Name: actor.Name,
Email: actor.Email,
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Committer: &files_service.IdentityOptions{
Name: actor.Name,
Email: actor.Email,
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
@ -142,12 +142,12 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
OldBranch: "master",
NewBranch: "newBranch",
Author: &files_service.IdentityOptions{
Name: actor.Name,
Email: actor.Email,
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Committer: &files_service.IdentityOptions{
Name: actor.Name,
Email: actor.Email,
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),

@ -118,7 +118,8 @@ func TestForkListLimitedAndPrivateRepos(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/forks")
resp := user1Sess.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length())
// since user1 is an admin, he can get both of the forked repositories
assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length())
assert.NoError(t, org_service.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
resp = user1Sess.MakeRequest(t, req, http.StatusOK)

@ -4,10 +4,22 @@
package integration
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery"
@ -39,3 +51,514 @@ func TestNewWebHookLink(t *testing.T) {
})
}
}
func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, repoName, url, event string) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+userName+"/"+repoName+"/hooks", api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": url,
},
Events: []string{event},
Active: true,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
}
func testAPICreateWebhookForOrg(t *testing.T, session *TestSession, userName, url, event string) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/"+userName+"/hooks", api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": url,
},
Events: []string{event},
Active: true,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
}
type mockWebhookProvider struct {
server *httptest.Server
}
func newMockWebhookProvider(callback func(r *http.Request), status int) *mockWebhookProvider {
m := &mockWebhookProvider{}
m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callback(r)
w.WriteHeader(status)
}))
return m
}
func (m *mockWebhookProvider) URL() string {
if m.server == nil {
return ""
}
return m.server.URL
}
// Close closes the mock webhook http server
func (m *mockWebhookProvider) Close() {
if m.server != nil {
m.server.Close()
m.server = nil
}
}
func Test_WebhookCreate(t *testing.T) {
var payloads []api.CreatePayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.CreatePayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = string(webhook_module.HookEventCreate)
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "create")
// 2. trigger the webhook
testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
// 3. validate the webhook is triggered
assert.Len(t, payloads, 1)
assert.EqualValues(t, string(webhook_module.HookEventCreate), triggeredEvent)
assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
assert.EqualValues(t, "master2", payloads[0].Ref)
assert.EqualValues(t, "branch", payloads[0].RefType)
})
}
func Test_WebhookDelete(t *testing.T) {
var payloads []api.DeletePayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.DeletePayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "delete"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "delete")
// 2. trigger the webhook
testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
testAPIDeleteBranch(t, "master2", http.StatusNoContent)
// 3. validate the webhook is triggered
assert.EqualValues(t, "delete", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
assert.EqualValues(t, "master2", payloads[0].Ref)
assert.EqualValues(t, "branch", payloads[0].RefType)
})
}
func Test_WebhookFork(t *testing.T) {
var payloads []api.ForkPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.ForkPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "fork"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user1")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "fork")
// 2. trigger the webhook
testRepoFork(t, session, "user2", "repo1", "user1", "repo1-fork", "master")
// 3. validate the webhook is triggered
assert.EqualValues(t, "fork", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "repo1-fork", payloads[0].Repo.Name)
assert.EqualValues(t, "user1/repo1-fork", payloads[0].Repo.FullName)
assert.EqualValues(t, "repo1", payloads[0].Forkee.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Forkee.FullName)
})
}
func Test_WebhookIssueComment(t *testing.T) {
var payloads []api.IssueCommentPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.IssueCommentPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "issue_comment"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment")
// 2. trigger the webhook
issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2")
testIssueAddComment(t, session, issueURL, "issue title2 comment1", "")
// 3. validate the webhook is triggered
assert.EqualValues(t, "issue_comment", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "created", payloads[0].Action)
assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
assert.EqualValues(t, "Title2", payloads[0].Issue.Title)
assert.EqualValues(t, "Description2", payloads[0].Issue.Body)
assert.EqualValues(t, "issue title2 comment1", payloads[0].Comment.Body)
})
}
func Test_WebhookRelease(t *testing.T) {
var payloads []api.ReleasePayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.ReleasePayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "release"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "release")
// 2. trigger the webhook
createNewRelease(t, session, "/user2/repo1", "v0.0.99", "v0.0.99", false, false)
// 3. validate the webhook is triggered
assert.EqualValues(t, "release", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "repo1", payloads[0].Repository.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Repository.FullName)
assert.EqualValues(t, "v0.0.99", payloads[0].Release.TagName)
assert.False(t, payloads[0].Release.IsDraft)
assert.False(t, payloads[0].Release.IsPrerelease)
})
}
func Test_WebhookPush(t *testing.T) {
var payloads []api.PushPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.PushPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "push"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push")
// 2. trigger the webhook
testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push")
// 3. validate the webhook is triggered
assert.EqualValues(t, "push", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
assert.Len(t, payloads[0].Commits, 1)
assert.EqualValues(t, []string{"test_webhook_push.md"}, payloads[0].Commits[0].Added)
})
}
func Test_WebhookIssue(t *testing.T) {
var payloads []api.IssuePayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.IssuePayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "issues"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues")
// 2. trigger the webhook
testNewIssue(t, session, "user2", "repo1", "Title1", "Description1")
// 3. validate the webhook is triggered
assert.EqualValues(t, "issues", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "opened", payloads[0].Action)
assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
assert.EqualValues(t, "Title1", payloads[0].Issue.Title)
assert.EqualValues(t, "Description1", payloads[0].Issue.Body)
})
}
func Test_WebhookPullRequest(t *testing.T) {
var payloads []api.PullRequestPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.PullRequestPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "pull_request"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request")
testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
// 2. trigger the webhook
repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request")
// 3. validate the webhook is triggered
assert.EqualValues(t, "pull_request", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "repo1", payloads[0].PullRequest.Base.Repository.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Base.Repository.FullName)
assert.EqualValues(t, "repo1", payloads[0].PullRequest.Head.Repository.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Head.Repository.FullName)
assert.EqualValues(t, 0, payloads[0].PullRequest.Additions)
})
}
func Test_WebhookPullRequestComment(t *testing.T) {
var payloads []api.IssueCommentPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.IssueCommentPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "pull_request_comment"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request_comment")
// 2. trigger the webhook
testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
prID := testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request")
testIssueAddComment(t, session, "/user2/repo1/pulls/"+prID, "pull title2 comment1", "")
// 3. validate the webhook is triggered
assert.EqualValues(t, "pull_request_comment", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "created", payloads[0].Action)
assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
assert.EqualValues(t, "first pull request", payloads[0].Issue.Title)
assert.EqualValues(t, "", payloads[0].Issue.Body)
assert.EqualValues(t, "pull title2 comment1", payloads[0].Comment.Body)
})
}
func Test_WebhookWiki(t *testing.T) {
var payloads []api.WikiPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.WikiPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "wiki"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "wiki")
// 2. trigger the webhook
testAPICreateWikiPage(t, session, "user2", "repo1", "Test Wiki Page", http.StatusCreated)
// 3. validate the webhook is triggered
assert.EqualValues(t, "wiki", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "created", payloads[0].Action)
assert.EqualValues(t, "repo1", payloads[0].Repository.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Repository.FullName)
assert.EqualValues(t, "Test-Wiki-Page", payloads[0].Page)
})
}
func Test_WebhookRepository(t *testing.T) {
var payloads []api.RepositoryPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.RepositoryPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "repository"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user1")
testAPICreateWebhookForOrg(t, session, "org3", provider.URL(), "repository")
// 2. trigger the webhook
testAPIOrgCreateRepo(t, session, "org3", "repo_new", http.StatusCreated)
// 3. validate the webhook is triggered
assert.EqualValues(t, "repository", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "created", payloads[0].Action)
assert.EqualValues(t, "org3", payloads[0].Organization.UserName)
assert.EqualValues(t, "repo_new", payloads[0].Repository.Name)
assert.EqualValues(t, "org3/repo_new", payloads[0].Repository.FullName)
})
}
func Test_WebhookPackage(t *testing.T) {
var payloads []api.PackagePayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
content, _ := io.ReadAll(r.Body)
var payload api.PackagePayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "package"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user1")
testAPICreateWebhookForOrg(t, session, "org3", provider.URL(), "package")
// 2. trigger the webhook
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s", "org3", "gitea", "v1.24.0")
req := NewRequestWithBody(t, "PUT", url+"/gitea", strings.NewReader("This is a dummy file")).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// 3. validate the webhook is triggered
assert.EqualValues(t, "package", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, "created", payloads[0].Action)
assert.EqualValues(t, "gitea", payloads[0].Package.Name)
assert.EqualValues(t, "generic", payloads[0].Package.Type)
assert.EqualValues(t, "org3", payloads[0].Organization.UserName)
assert.EqualValues(t, "v1.24.0", payloads[0].Package.Version)
})
}
func Test_WebhookStatus(t *testing.T) {
var payloads []api.CommitStatusPayload
var triggeredEvent string
provider := newMockWebhookProvider(func(r *http.Request) {
assert.Contains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should contain status")
assert.Contains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should contain status")
assert.Contains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should contain status")
content, _ := io.ReadAll(r.Body)
var payload api.CommitStatusPayload
err := json.Unmarshal(content, &payload)
assert.NoError(t, err)
payloads = append(payloads, payload)
triggeredEvent = "status"
}, http.StatusOK)
defer provider.Close()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "status")
repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
gitRepo1, err := gitrepo.OpenRepository(context.Background(), repo1)
assert.NoError(t, err)
commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch)
assert.NoError(t, err)
// 2. trigger the webhook
testCtx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeAll)
// update a status for a commit via API
doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
State: api.CommitStatusSuccess,
TargetURL: "http://test.ci/",
Description: "",
Context: "testci",
})(t)
// 3. validate the webhook is triggered
assert.EqualValues(t, "status", triggeredEvent)
assert.Len(t, payloads, 1)
assert.EqualValues(t, commitID, payloads[0].Commit.ID)
assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
assert.EqualValues(t, "testci", payloads[0].Context)
assert.EqualValues(t, commitID, payloads[0].SHA)
})
}

@ -71,8 +71,8 @@ func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.Chang
NewBranch: repo.DefaultBranch,
Message: "Deletes README.md",
Author: &files_service.IdentityOptions{
Name: "Bob Smith",
Email: "bob@smith.com",
GitUserName: "Bob Smith",
GitUserEmail: "bob@smith.com",
},
Committer: nil,
}

@ -26,6 +26,9 @@ TYPE = immediate
[queue.push_update]
TYPE = immediate
[queue.webhook_sender]
TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories
@ -111,3 +114,6 @@ ENABLED = true
[actions]
ENABLED = true
[webhook]
ALLOWED_HOST_LIST = 127.0.0.1

@ -28,6 +28,9 @@ TYPE = immediate
[queue.push_update]
TYPE = immediate
[queue.webhook_sender]
TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/gitea-repositories
@ -118,3 +121,6 @@ REPLY_TO_ADDRESS = incoming+%{token}@localhost
[actions]
ENABLED = true
[webhook]
ALLOWED_HOST_LIST = 127.0.0.1

@ -27,6 +27,9 @@ TYPE = immediate
[queue.push_update]
TYPE = immediate
[queue.webhook_sender]
TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/gitea-repositories
@ -127,3 +130,6 @@ ENABLED = true
[actions]
ENABLED = true
[webhook]
ALLOWED_HOST_LIST = 127.0.0.1

@ -22,6 +22,9 @@ TYPE = immediate
[queue.push_update]
TYPE = immediate
[queue.webhook_sender]
TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea-repositories
@ -116,3 +119,6 @@ RENDER_CONTENT_MODE=sanitized
[actions]
ENABLED = true
[webhook]
ALLOWED_HOST_LIST = 127.0.0.1

@ -22,6 +22,8 @@
"verbatimModuleSyntax": true,
"stripInternal": true,
"strict": false,
"strictBindCallApply": true,
"strictBuiltinIteratorReturn": true,
"strictFunctionTypes": true,
"noImplicitAny": true,
"noImplicitThis": true,

@ -65,6 +65,7 @@
}
#repo-files-table .repo-file-last-commit {
min-width: 0; /* otherwise the flex axis is not limited and the text might overflow in Pale Moon */
background: var(--color-box-header);
}

@ -6,21 +6,9 @@ import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'
import {getIssueColor, getIssueIcon} from '../issue.ts';
import {debounce} from 'perfect-debounce';
import type TextExpanderElement from '@github/text-expander-element';
import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element/dist/text-expander-element.d.ts';
type TextExpanderProvideResult = {
matched: boolean,
fragment?: HTMLElement,
}
type TextExpanderChangeEvent = Event & {
detail?: {
key: string,
text: string,
provide: (result: TextExpanderProvideResult | Promise<TextExpanderProvideResult>) => void,
}
}
async function fetchIssueSuggestions(key: string, text: string): Promise<TextExpanderProvideResult> {
async function fetchIssueSuggestions(key: string, text: string): Promise<TextExpanderResult> {
const issuePathInfo = parseIssueHref(window.location.href);
if (!issuePathInfo.ownerName) {
const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
@ -59,7 +47,7 @@ export function initTextExpander(expander: TextExpanderElement) {
return keyStart > lineStart;
};
const debouncedIssueSuggestions = debounce(async (key: string, text: string): Promise<TextExpanderProvideResult> => {
const debouncedIssueSuggestions = debounce(async (key: string, text: string): Promise<TextExpanderResult> => {
// https://github.com/github/text-expander-element/issues/71
// Upstream bug: when using "multiword+promise", TextExpander will get wrong "key" position.
// To reproduce, comment out the "shouldShowIssueSuggestions" check, use the "await sleep" below,

Loading…
Cancel
Save