aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/actions/commit_status.go16
-rw-r--r--services/asymkey/commit.go67
-rw-r--r--services/asymkey/commit_test.go54
-rw-r--r--services/asymkey/sign.go167
-rw-r--r--services/auth/source/ldap/source_authenticate.go2
-rw-r--r--services/auth/source/ldap/source_sync.go7
-rw-r--r--services/context/repo.go7
-rw-r--r--services/convert/pull.go22
-rw-r--r--services/convert/pull_test.go7
-rw-r--r--services/convert/status.go38
-rw-r--r--services/doctor/actions.go2
-rw-r--r--services/doctor/dbconsistency.go3
-rw-r--r--services/forms/user_form_test.go23
-rw-r--r--services/git/commit.go8
-rw-r--r--services/issue/issue.go92
-rw-r--r--services/issue/issue_test.go6
-rw-r--r--services/migrations/codecommit.go4
-rw-r--r--services/migrations/gitea_uploader.go2
-rw-r--r--services/oauth2_provider/access_token.go9
-rw-r--r--services/pull/commit_status.go99
-rw-r--r--services/pull/commit_status_test.go92
-rw-r--r--services/pull/merge.go43
-rw-r--r--services/pull/merge_prepare.go6
-rw-r--r--services/pull/merge_squash.go14
-rw-r--r--services/pull/merge_test.go25
-rw-r--r--services/pull/pull.go6
-rw-r--r--services/pull/update.go45
-rw-r--r--services/repository/adopt.go2
-rw-r--r--services/repository/commitstatus/commitstatus.go6
-rw-r--r--services/repository/delete.go5
-rw-r--r--services/repository/files/temp_repo.go11
-rw-r--r--services/repository/fork_test.go3
-rw-r--r--services/repository/generate.go6
-rw-r--r--services/repository/generate_test.go24
-rw-r--r--services/repository/init.go7
-rw-r--r--services/user/update.go39
-rw-r--r--services/user/update_test.go12
-rw-r--r--services/user/user.go5
-rw-r--r--services/webhook/webhook_test.go4
-rw-r--r--services/wiki/wiki.go4
40 files changed, 638 insertions, 356 deletions
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index 4541b3af4c..ef241e5091 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -14,9 +14,9 @@ import (
git_model "code.gitea.io/gitea/models/git"
user_model "code.gitea.io/gitea/models/user"
actions_module "code.gitea.io/gitea/modules/actions"
+ "code.gitea.io/gitea/modules/commitstatus"
git "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
@@ -147,16 +147,18 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &status)
}
-func toCommitStatus(status actions_model.Status) api.CommitStatusState {
+func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState {
switch status {
- case actions_model.StatusSuccess, actions_model.StatusSkipped:
- return api.CommitStatusSuccess
+ case actions_model.StatusSuccess:
+ return commitstatus.CommitStatusSuccess
case actions_model.StatusFailure, actions_model.StatusCancelled:
- return api.CommitStatusFailure
+ return commitstatus.CommitStatusFailure
case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning:
- return api.CommitStatusPending
+ return commitstatus.CommitStatusPending
+ case actions_model.StatusSkipped:
+ return commitstatus.CommitStatusSkipped
default:
- return api.CommitStatusError
+ return commitstatus.CommitStatusError
}
}
diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go
index 105782a93a..148f51fd10 100644
--- a/services/asymkey/commit.go
+++ b/services/asymkey/commit.go
@@ -6,6 +6,7 @@ package asymkey
import (
"context"
"fmt"
+ "slices"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -359,24 +360,39 @@ func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, si
return nil
}
+func verifySSHCommitVerificationByInstanceKey(c *git.Commit, committerUser, signerUser *user_model.User, committerGitEmail, publicKeyContent string) *asymkey_model.CommitVerification {
+ fingerprint, err := asymkey_model.CalcFingerprint(publicKeyContent)
+ if err != nil {
+ log.Error("Error calculating the fingerprint public key %q, err: %v", publicKeyContent, err)
+ return nil
+ }
+ sshPubKey := &asymkey_model.PublicKey{
+ Verified: true,
+ Content: publicKeyContent,
+ Fingerprint: fingerprint,
+ HasUsed: true,
+ }
+ return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail)
+}
+
// ParseCommitWithSSHSignature check if signature is good against keystore.
-func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification {
+func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification {
// Now try to associate the signature with the committer, if present
- if committer.ID != 0 {
+ if committerUser.ID != 0 {
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
- OwnerID: committer.ID,
+ OwnerID: committerUser.ID,
NotKeytype: asymkey_model.KeyTypePrincipal,
})
if err != nil { // Skipping failed to get ssh keys of user
log.Error("ListPublicKeys: %v", err)
return &asymkey_model.CommitVerification{
- CommittingUser: committer,
+ CommittingUser: committerUser,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
- committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committer.ID, user_model.GetEmailAddresses)
+ committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committerUser.ID, user_model.GetEmailAddresses)
if err != nil {
log.Error("GetEmailAddresses: %v", err)
}
@@ -391,7 +407,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
for _, k := range keys {
if k.Verified && activated {
- commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
+ commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committerUser, committerUser, c.Committer.Email)
if commitVerification != nil {
return commitVerification
}
@@ -399,8 +415,45 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
}
}
+ // Try the pre-set trusted keys (for key-rotation purpose)
+ // At the moment, we still use the SigningName&SigningEmail for the rotated keys.
+ // Maybe in the future we can extend the key format to "ssh-xxx .... old-user@example.com" to support different signer emails.
+ for _, k := range setting.Repository.Signing.TrustedSSHKeys {
+ signerUser := &user_model.User{
+ Name: setting.Repository.Signing.SigningName,
+ Email: setting.Repository.Signing.SigningEmail,
+ }
+ commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, c.Committer.Email, k)
+ if commitVerification != nil && commitVerification.Verified {
+ return commitVerification
+ }
+ }
+
+ // Try the configured instance-wide SSH public key
+ if setting.Repository.Signing.SigningFormat == git.SigningKeyFormatSSH && !slices.Contains([]string{"", "default", "none"}, setting.Repository.Signing.SigningKey) {
+ gpgSettings := git.GPGSettings{
+ Sign: true,
+ KeyID: setting.Repository.Signing.SigningKey,
+ Name: setting.Repository.Signing.SigningName,
+ Email: setting.Repository.Signing.SigningEmail,
+ Format: setting.Repository.Signing.SigningFormat,
+ }
+ signerUser := &user_model.User{
+ Name: gpgSettings.Name,
+ Email: gpgSettings.Email,
+ }
+ if err := gpgSettings.LoadPublicKeyContent(); err != nil {
+ log.Error("Error getting instance-wide SSH signing key %q, err: %v", gpgSettings.KeyID, err)
+ } else {
+ commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, gpgSettings.Email, gpgSettings.PublicKeyContent)
+ if commitVerification != nil && commitVerification.Verified {
+ return commitVerification
+ }
+ }
+ }
+
return &asymkey_model.CommitVerification{
- CommittingUser: committer,
+ CommittingUser: committerUser,
Verified: false,
Reason: asymkey_model.NoKeyFound,
}
diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go
new file mode 100644
index 0000000000..0438209a61
--- /dev/null
+++ b/services/asymkey/commit_test.go
@@ -0,0 +1,54 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "strings"
+ "testing"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseCommitWithSSHSignature(t *testing.T) {
+ // Here we only test the TrustedSSHKeys. The complete signing test is in tests/integration/gpg_ssh_git_test.go
+ t.Run("TrustedSSHKey", func(t *testing.T) {
+ defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")()
+ defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")()
+ defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})()
+
+ commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree 9a93ffa76e8b72bdb6431910b3a506fa2b39f42e
+author User Two <user2@example.com> 1749230009 +0200
+committer User Two <user2@example.com> 1749230009 +0200
+gpgsig -----BEGIN SSH SIGNATURE-----
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgfpjiJ1VpbcT5svDW6qgB8kPujl
+ KK74epLnUT2hAs8T0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+ AAAAQDX2t2iHuuLxEWHLJetYXKsgayv3c43r0pJNfAzdLN55Q65pC5M7rG6++gT2bxcpOu
+ Y6EXbpLqia9sunEF3+LQY=
+ -----END SSH SIGNATURE-----
+
+Initial commit with signed file
+`))
+ require.NoError(t, err)
+ committingUser := &user_model.User{
+ ID: 2,
+ Name: "User Two",
+ Email: "user2@example.com",
+ }
+ ret := ParseCommitWithSSHSignature(t.Context(), commit, committingUser)
+ require.NotNil(t, ret)
+ assert.True(t, ret.Verified)
+ assert.False(t, ret.Warning)
+ assert.Equal(t, committingUser, ret.CommittingUser)
+ if assert.NotNil(t, ret.SigningUser) {
+ assert.Equal(t, "gitea", ret.SigningUser.Name)
+ assert.Equal(t, "gitea@fake.local", ret.SigningUser.Email)
+ }
+ })
+}
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
index 2216bca54a..f94462ea46 100644
--- a/services/asymkey/sign.go
+++ b/services/asymkey/sign.go
@@ -6,6 +6,7 @@ package asymkey
import (
"context"
"fmt"
+ "os"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -85,9 +86,9 @@ func IsErrWontSign(err error) bool {
}
// SigningKey returns the KeyID and git Signature for the repo
-func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
+func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Signature) {
if setting.Repository.Signing.SigningKey == "none" {
- return "", nil
+ return nil, nil
}
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
@@ -95,53 +96,77 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
sign, valid := git.ParseBool(strings.TrimSpace(value))
if !sign || !valid {
- return "", nil
+ return nil, nil
}
+ format, _, _ := git.NewCommand("config", "--default", git.SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
signingKey, _, _ := git.NewCommand("config", "--get", "user.signingkey").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
signingName, _, _ := git.NewCommand("config", "--get", "user.name").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
signingEmail, _, _ := git.NewCommand("config", "--get", "user.email").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
- return strings.TrimSpace(signingKey), &git.Signature{
- Name: strings.TrimSpace(signingName),
- Email: strings.TrimSpace(signingEmail),
+
+ if strings.TrimSpace(signingKey) == "" {
+ return nil, nil
}
+
+ return &git.SigningKey{
+ KeyID: strings.TrimSpace(signingKey),
+ Format: strings.TrimSpace(format),
+ }, &git.Signature{
+ Name: strings.TrimSpace(signingName),
+ Email: strings.TrimSpace(signingEmail),
+ }
}
- return setting.Repository.Signing.SigningKey, &git.Signature{
- Name: setting.Repository.Signing.SigningName,
- Email: setting.Repository.Signing.SigningEmail,
+ if setting.Repository.Signing.SigningKey == "" {
+ return nil, nil
}
+
+ return &git.SigningKey{
+ KeyID: setting.Repository.Signing.SigningKey,
+ Format: setting.Repository.Signing.SigningFormat,
+ }, &git.Signature{
+ Name: setting.Repository.Signing.SigningName,
+ Email: setting.Repository.Signing.SigningEmail,
+ }
}
// PublicSigningKey gets the public signing key within a provided repository directory
-func PublicSigningKey(ctx context.Context, repoPath string) (string, error) {
+func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) {
signingKey, _ := SigningKey(ctx, repoPath)
- if signingKey == "" {
- return "", nil
+ if signingKey == nil {
+ return "", "", nil
+ }
+ if signingKey.Format == git.SigningKeyFormatSSH {
+ content, err := os.ReadFile(signingKey.KeyID)
+ if err != nil {
+ log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err)
+ return "", signingKey.Format, err
+ }
+ return string(content), signingKey.Format, nil
}
content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
- "gpg --export -a", "gpg", "--export", "-a", signingKey)
+ "gpg --export -a", "gpg", "--export", "-a", signingKey.KeyID)
if err != nil {
log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
- return "", err
+ return "", signingKey.Format, err
}
- return content, nil
+ return content, signingKey.Format, nil
}
// SignInitialCommit determines if we should sign the initial commit to this repository
-func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, string, *git.Signature, error) {
+func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
signingKey, sig := SigningKey(ctx, repoPath)
- if signingKey == "" {
- return false, "", nil, &ErrWontSign{noKey}
+ if signingKey == nil {
+ return false, nil, nil, &ErrWontSign{noKey}
}
Loop:
for _, rule := range rules {
switch rule {
case never:
- return false, "", nil, &ErrWontSign{never}
+ return false, nil, nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
@@ -150,18 +175,18 @@ Loop:
IncludeSubKeys: true,
})
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if len(keys) == 0 {
- return false, "", nil, &ErrWontSign{pubkey}
+ return false, nil, nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
- return false, "", nil, err
+ return false, nil, nil, err
}
if twofaModel == nil {
- return false, "", nil, &ErrWontSign{twofa}
+ return false, nil, nil, &ErrWontSign{twofa}
}
}
}
@@ -169,19 +194,19 @@ Loop:
}
// SignWikiCommit determines if we should sign the commits to this repository wiki
-func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, string, *git.Signature, error) {
+func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
repoWikiPath := repo.WikiPath()
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey, sig := SigningKey(ctx, repoWikiPath)
- if signingKey == "" {
- return false, "", nil, &ErrWontSign{noKey}
+ if signingKey == nil {
+ return false, nil, nil, &ErrWontSign{noKey}
}
Loop:
for _, rule := range rules {
switch rule {
case never:
- return false, "", nil, &ErrWontSign{never}
+ return false, nil, nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
@@ -190,35 +215,35 @@ Loop:
IncludeSubKeys: true,
})
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if len(keys) == 0 {
- return false, "", nil, &ErrWontSign{pubkey}
+ return false, nil, nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
- return false, "", nil, err
+ return false, nil, nil, err
}
if twofaModel == nil {
- return false, "", nil, &ErrWontSign{twofa}
+ return false, nil, nil, &ErrWontSign{twofa}
}
case parentSigned:
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit("HEAD")
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if commit.Signature == nil {
- return false, "", nil, &ErrWontSign{parentSigned}
+ return false, nil, nil, &ErrWontSign{parentSigned}
}
verification := ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
- return false, "", nil, &ErrWontSign{parentSigned}
+ return false, nil, nil, &ErrWontSign{parentSigned}
}
}
}
@@ -226,18 +251,18 @@ Loop:
}
// SignCRUDAction determines if we should sign a CRUD commit to this repository
-func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
+func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey, sig := SigningKey(ctx, repoPath)
- if signingKey == "" {
- return false, "", nil, &ErrWontSign{noKey}
+ if signingKey == nil {
+ return false, nil, nil, &ErrWontSign{noKey}
}
Loop:
for _, rule := range rules {
switch rule {
case never:
- return false, "", nil, &ErrWontSign{never}
+ return false, nil, nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
@@ -246,35 +271,35 @@ Loop:
IncludeSubKeys: true,
})
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if len(keys) == 0 {
- return false, "", nil, &ErrWontSign{pubkey}
+ return false, nil, nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
- return false, "", nil, err
+ return false, nil, nil, err
}
if twofaModel == nil {
- return false, "", nil, &ErrWontSign{twofa}
+ return false, nil, nil, &ErrWontSign{twofa}
}
case parentSigned:
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit(parentCommit)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if commit.Signature == nil {
- return false, "", nil, &ErrWontSign{parentSigned}
+ return false, nil, nil, &ErrWontSign{parentSigned}
}
verification := ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
- return false, "", nil, &ErrWontSign{parentSigned}
+ return false, nil, nil, &ErrWontSign{parentSigned}
}
}
}
@@ -282,16 +307,16 @@ Loop:
}
// SignMerge determines if we should sign a PR merge commit to the base repository
-func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
+func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) {
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to get Base Repo for pull request")
- return false, "", nil, err
+ return false, nil, nil, err
}
repo := pr.BaseRepo
signingKey, signer := SigningKey(ctx, repo.RepoPath())
- if signingKey == "" {
- return false, "", nil, &ErrWontSign{noKey}
+ if signingKey == nil {
+ return false, nil, nil, &ErrWontSign{noKey}
}
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
@@ -302,7 +327,7 @@ Loop:
for _, rule := range rules {
switch rule {
case never:
- return false, "", nil, &ErrWontSign{never}
+ return false, nil, nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
@@ -311,91 +336,91 @@ Loop:
IncludeSubKeys: true,
})
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if len(keys) == 0 {
- return false, "", nil, &ErrWontSign{pubkey}
+ return false, nil, nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
- return false, "", nil, err
+ return false, nil, nil, err
}
if twofaModel == nil {
- return false, "", nil, &ErrWontSign{twofa}
+ return false, nil, nil, &ErrWontSign{twofa}
}
case approved:
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
if protectedBranch == nil {
- return false, "", nil, &ErrWontSign{approved}
+ return false, nil, nil, &ErrWontSign{approved}
}
if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
- return false, "", nil, &ErrWontSign{approved}
+ return false, nil, nil, &ErrWontSign{approved}
}
case baseSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(baseCommit)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
verification := ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
- return false, "", nil, &ErrWontSign{baseSigned}
+ return false, nil, nil, &ErrWontSign{baseSigned}
}
case headSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
verification := ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
- return false, "", nil, &ErrWontSign{headSigned}
+ return false, nil, nil, &ErrWontSign{headSigned}
}
case commitsSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
verification := ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
- return false, "", nil, &ErrWontSign{commitsSigned}
+ return false, nil, nil, &ErrWontSign{commitsSigned}
}
// need to work out merge-base
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
if err != nil {
- return false, "", nil, err
+ return false, nil, nil, err
}
for _, commit := range commitList {
verification := ParseCommitWithSignature(ctx, commit)
if !verification.Verified {
- return false, "", nil, &ErrWontSign{commitsSigned}
+ return false, nil, nil, &ErrWontSign{commitsSigned}
}
}
}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index a2e8c2b86a..6005a4744a 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -58,7 +58,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
opts := &user_service.UpdateOptions{}
if source.AdminFilter != "" && user.IsAdmin != sr.IsAdmin {
// Change existing admin flag only if AdminFilter option is set
- opts.IsAdmin = optional.Some(sr.IsAdmin)
+ opts.IsAdmin = user_service.UpdateOptionFieldFromSync(sr.IsAdmin)
}
if !sr.IsAdmin && source.RestrictedFilter != "" && user.IsRestricted != sr.IsRestricted {
// Change existing restricted flag only if RestrictedFilter option is set
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 678b6b2b62..7b401c5c96 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -162,7 +162,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
IsActive: optional.Some(true),
}
if source.AdminFilter != "" {
- opts.IsAdmin = optional.Some(su.IsAdmin)
+ opts.IsAdmin = user_service.UpdateOptionFieldFromSync(su.IsAdmin)
}
// Change existing restricted flag only if RestrictedFilter option is set
if !su.IsAdmin && source.RestrictedFilter != "" {
@@ -178,8 +178,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
}
- if usr.IsUploadAvatarChanged(su.Avatar) {
- if err == nil && source.AttributeAvatar != "" {
+ if source.AttributeAvatar != "" {
+ if len(su.Avatar) > 0 && usr.IsUploadAvatarChanged(su.Avatar) {
+ log.Trace("SyncExternalUsers[%s]: Uploading new avatar for %s", source.AuthSource.Name, usr.Name)
_ = user_service.UploadAvatar(ctx, usr, su.Avatar)
}
}
diff --git a/services/context/repo.go b/services/context/repo.go
index 61841aa90b..32d54c88ff 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -101,7 +101,7 @@ type CanCommitToBranchResults struct {
UserCanPush bool
RequireSigned bool
WillSign bool
- SigningKey string
+ SigningKey *git.SigningKey
WontSignReason string
}
@@ -123,7 +123,8 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
- canCommit := r.CanEnableEditor(ctx, doer) && userCanPush
+ canEnableEditor := r.CanEnableEditor(ctx, doer)
+ canCommit := canEnableEditor && userCanPush
if requireSigned {
canCommit = canCommit && sign
}
@@ -139,7 +140,7 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
return CanCommitToBranchResults{
CanCommitToBranch: canCommit,
- EditorEnabled: r.CanEnableEditor(ctx, doer),
+ EditorEnabled: canEnableEditor,
UserCanPush: userCanPush,
RequireSigned: requireSigned,
WillSign: sign,
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 7798bebb08..8f9679f649 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -419,6 +419,9 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
if baseBranch != nil {
apiPullRequest.Base.Sha = baseBranch.CommitID
}
+ if pr.HeadRepoID == pr.BaseRepoID {
+ apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
+ }
// pull request head branch, both repository and branch could not exist
if pr.HeadRepo != nil {
@@ -431,22 +434,19 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
if exist {
apiPullRequest.Head.Ref = pr.HeadBranch
}
+ if pr.HeadRepoID != pr.BaseRepoID {
+ p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err)
+ p.AccessMode = perm.AccessModeNone
+ }
+ apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
+ }
}
if apiPullRequest.Head.Ref == "" {
apiPullRequest.Head.Ref = pr.GetGitRefName()
}
- if pr.HeadRepoID == pr.BaseRepoID {
- apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
- } else {
- p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
- if err != nil {
- log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err)
- p.AccessMode = perm.AccessModeNone
- }
- apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
- }
-
if pr.Flow == issues_model.PullRequestFlowAGit {
apiPullRequest.Head.Name = ""
}
diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go
index cd86283c8a..dfbe24d184 100644
--- a/services/convert/pull_test.go
+++ b/services/convert/pull_test.go
@@ -46,4 +46,11 @@ func TestPullRequest_APIFormat(t *testing.T) {
assert.NotNil(t, apiPullRequest)
assert.Nil(t, apiPullRequest.Head.Repository)
assert.EqualValues(t, -1, apiPullRequest.Head.RepoID)
+
+ apiPullRequests, err := ToAPIPullRequests(git.DefaultContext, pr.BaseRepo, []*issues_model.PullRequest{pr}, nil)
+ assert.NoError(t, err)
+ assert.Len(t, apiPullRequests, 1)
+ assert.NotNil(t, apiPullRequests[0])
+ assert.Nil(t, apiPullRequests[0].Head.Repository)
+ assert.EqualValues(t, -1, apiPullRequests[0].Head.RepoID)
}
diff --git a/services/convert/status.go b/services/convert/status.go
index 6cef63c1cd..b4864a0307 100644
--- a/services/convert/status.go
+++ b/services/convert/status.go
@@ -5,6 +5,7 @@ package convert
import (
"context"
+ "net/url"
git_model "code.gitea.io/gitea/models/git"
user_model "code.gitea.io/gitea/models/user"
@@ -32,34 +33,29 @@ func ToCommitStatus(ctx context.Context, status *git_model.CommitStatus) *api.Co
return apiStatus
}
+func ToCommitStatuses(ctx context.Context, statuses []*git_model.CommitStatus) []*api.CommitStatus {
+ apiStatuses := make([]*api.CommitStatus, len(statuses))
+ for i, status := range statuses {
+ apiStatuses[i] = ToCommitStatus(ctx, status)
+ }
+ return apiStatuses
+}
+
// ToCombinedStatus converts List of CommitStatus to a CombinedStatus
func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, repo *api.Repository) *api.CombinedStatus {
if len(statuses) == 0 {
return nil
}
- retStatus := &api.CombinedStatus{
- SHA: statuses[0].SHA,
+ combinedStatus := git_model.CalcCommitStatus(statuses)
+
+ return &api.CombinedStatus{
+ State: combinedStatus.State,
+ Statuses: ToCommitStatuses(ctx, statuses),
+ SHA: combinedStatus.SHA,
TotalCount: len(statuses),
Repository: repo,
- URL: "",
+ CommitURL: repo.URL + "/commits/" + url.PathEscape(combinedStatus.SHA),
+ URL: repo.URL + "/commits/" + url.PathEscape(combinedStatus.SHA) + "/status",
}
-
- retStatus.Statuses = make([]*api.CommitStatus, 0, len(statuses))
- for _, status := range statuses {
- retStatus.Statuses = append(retStatus.Statuses, ToCommitStatus(ctx, status))
- if retStatus.State == "" || status.State.NoBetterThan(retStatus.State) {
- retStatus.State = status.State
- }
- }
- // According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference
- // > Additionally, a combined state is returned. The state is one of:
- // > failure if any of the contexts report as error or failure
- // > pending if there are no statuses or a context is pending
- // > success if the latest status for all contexts is success
- if retStatus.State.IsError() {
- retStatus.State = api.CommitStatusFailure
- }
-
- return retStatus
}
diff --git a/services/doctor/actions.go b/services/doctor/actions.go
index 7c44fb8392..28e26c88eb 100644
--- a/services/doctor/actions.go
+++ b/services/doctor/actions.go
@@ -19,7 +19,7 @@ func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bo
var reposToFix []*repo_model.Repository
for page := 1; ; page++ {
- repos, _, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: page,
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
index 62326ed07c..d5a133d8b2 100644
--- a/services/doctor/dbconsistency.go
+++ b/services/doctor/dbconsistency.go
@@ -15,6 +15,7 @@ import (
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ issue_service "code.gitea.io/gitea/services/issue"
)
type consistencyCheck struct {
@@ -93,7 +94,7 @@ func prepareDBConsistencyChecks() []consistencyCheck {
// find issues without existing repository
Name: "Orphaned Issues without existing repository",
Counter: issues_model.CountOrphanedIssues,
- Fixer: asFixer(issues_model.DeleteOrphanedIssues),
+ Fixer: asFixer(issue_service.DeleteOrphanedIssues),
},
// find releases without existing repository
genericOrphanCheck("Orphaned Releases without existing repository",
diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go
index b4120f20ed..09e9ec0f65 100644
--- a/services/forms/user_form_test.go
+++ b/services/forms/user_form_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"github.com/gobwas/glob"
"github.com/stretchr/testify/assert"
@@ -26,12 +27,7 @@ func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) {
}
func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io")}
+ defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io")})()
tt := []struct {
email string
@@ -48,12 +44,7 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
}
func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")}
+ defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")})()
tt := []struct {
email string
@@ -76,13 +67,7 @@ func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
}
func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = nil
- setting.Service.EmailDomainBlockList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")}
+ defer test.MockVariableValue(&setting.Service.EmailDomainBlockList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")})()
tt := []struct {
email string
diff --git a/services/git/commit.go b/services/git/commit.go
index d3ebcab569..2e0e8a5096 100644
--- a/services/git/commit.go
+++ b/services/git/commit.go
@@ -34,9 +34,9 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository,
}
for _, c := range oldCommits {
- committer, ok := emailUsers[c.Committer.Email]
- if !ok && c.Committer != nil {
- committer = &user_model.User{
+ committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
+ if committerUser == nil {
+ committerUser = &user_model.User{
Name: c.Committer.Name,
Email: c.Committer.Email,
}
@@ -44,7 +44,7 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository,
signCommit := &asymkey_model.SignCommit{
UserCommit: c,
- Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committer),
+ Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser),
}
isOwnerMemberCollaborator := func(user *user_model.User) (bool, error) {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 455a1ec297..2cb5f2801d 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -190,9 +190,13 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Reposi
}
// delete entries in database
- if err := deleteIssue(ctx, issue); err != nil {
+ attachmentPaths, err := deleteIssue(ctx, issue)
+ if err != nil {
return err
}
+ for _, attachmentPath := range attachmentPaths {
+ system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPath)
+ }
// delete pull request related git data
if issue.IsPull && gitRepo != nil {
@@ -256,45 +260,45 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i
}
// deleteIssue deletes the issue
-func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
+func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
- return err
+ return nil, err
}
defer committer.Close()
- e := db.GetEngine(ctx)
- if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
- return err
+ if _, err := db.GetEngine(ctx).ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
+ return nil, err
}
// update the total issue numbers
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
- return err
+ return nil, err
}
// if the issue is closed, update the closed issue numbers
if issue.IsClosed {
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
- return err
+ return nil, err
}
}
if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
- return fmt.Errorf("error updating counters for milestone id %d: %w",
+ return nil, fmt.Errorf("error updating counters for milestone id %d: %w",
issue.MilestoneID, err)
}
if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil {
- return err
+ return nil, err
}
// find attachments related to this issue and remove them
- if err := issue.LoadAttributes(ctx); err != nil {
- return err
+ if err := issue.LoadAttachments(ctx); err != nil {
+ return nil, err
}
+ var attachmentPaths []string
for i := range issue.Attachments {
- system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath())
+ attachmentPaths = append(attachmentPaths, issue.Attachments[i].RelativePath())
}
// delete all database data still assigned to this issue
@@ -318,8 +322,68 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
&issues_model.Comment{DependentIssueID: issue.ID},
&issues_model.IssuePin{IssueID: issue.ID},
); err != nil {
+ return nil, err
+ }
+
+ if err := committer.Commit(); err != nil {
+ return nil, err
+ }
+ return attachmentPaths, nil
+}
+
+// DeleteOrphanedIssues delete issues without a repo
+func DeleteOrphanedIssues(ctx context.Context) error {
+ var attachmentPaths []string
+ err := db.WithTx(ctx, func(ctx context.Context) error {
+ repoIDs, err := issues_model.GetOrphanedIssueRepoIDs(ctx)
+ if err != nil {
+ return err
+ }
+ for i := range repoIDs {
+ paths, err := DeleteIssuesByRepoID(ctx, repoIDs[i])
+ if err != nil {
+ return err
+ }
+ attachmentPaths = append(attachmentPaths, paths...)
+ }
+ return nil
+ })
+ if err != nil {
return err
}
- return committer.Commit()
+ // Remove issue attachment files.
+ for i := range attachmentPaths {
+ system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
+ }
+ return nil
+}
+
+// DeleteIssuesByRepoID deletes issues by repositories id
+func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
+ for {
+ issues := make([]*issues_model.Issue, 0, db.DefaultMaxInSize)
+ if err := db.GetEngine(ctx).
+ Where("repo_id = ?", repoID).
+ OrderBy("id").
+ Limit(db.DefaultMaxInSize).
+ Find(&issues); err != nil {
+ return nil, err
+ }
+
+ if len(issues) == 0 {
+ break
+ }
+
+ for _, issue := range issues {
+ issueAttachPaths, err := deleteIssue(ctx, issue)
+ if err != nil {
+ return nil, fmt.Errorf("deleteIssue [issue_id: %d]: %w", issue.ID, err)
+ }
+
+ attachmentPaths = append(attachmentPaths, issueAttachPaths...)
+ }
+ }
+
+ return attachmentPaths, err
}
diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go
index b3df8191e1..bad0d65d1e 100644
--- a/services/issue/issue_test.go
+++ b/services/issue/issue_test.go
@@ -44,7 +44,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
ID: issueIDs[2],
}
- err = deleteIssue(db.DefaultContext, issue)
+ _, err = deleteIssue(db.DefaultContext, issue)
assert.NoError(t, err)
issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
assert.NoError(t, err)
@@ -55,7 +55,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
assert.NoError(t, err)
issue, err = issues_model.GetIssueByID(db.DefaultContext, 4)
assert.NoError(t, err)
- err = deleteIssue(db.DefaultContext, issue)
+ _, err = deleteIssue(db.DefaultContext, issue)
assert.NoError(t, err)
assert.Len(t, attachments, 2)
for i := range attachments {
@@ -78,7 +78,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
assert.NoError(t, err)
assert.False(t, left)
- err = deleteIssue(db.DefaultContext, issue2)
+ _, err = deleteIssue(db.DefaultContext, issue2)
assert.NoError(t, err)
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
assert.NoError(t, err)
diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go
index 8b79edc4e5..9317156ab0 100644
--- a/services/migrations/codecommit.go
+++ b/services/migrations/codecommit.go
@@ -210,6 +210,10 @@ func (c *CodeCommitDownloader) GetPullRequests(ctx context.Context, page, perPag
pr.State = "closed"
pr.Closed = orig.LastActivityDate
}
+ if pr.Merged {
+ pr.MergeCommitSHA = *target.MergeMetadata.MergeCommitId
+ pr.MergedTime = orig.LastActivityDate
+ }
_ = CheckAndEnsureSafePR(pr, c.baseURL, c)
prs = append(prs, pr)
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index acb94439fa..737bff24d0 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -765,7 +765,7 @@ func (g *GiteaLocalUploader) newPullRequest(ctx context.Context, pr *base.PullRe
issue := issues_model.Issue{
RepoID: g.repo.ID,
Repo: g.repo,
- Title: prTitle,
+ Title: util.TruncateRunes(prTitle, 255),
Index: pr.Number,
Content: pr.Content,
MilestoneID: milestoneID,
diff --git a/services/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go
index 52a73c9572..4173b0fe87 100644
--- a/services/oauth2_provider/access_token.go
+++ b/services/oauth2_provider/access_token.go
@@ -16,7 +16,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
"github.com/golang-jwt/jwt/v5"
)
@@ -231,12 +233,11 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
}, nil
}
-// returns a list of "org" and "org:team" strings,
-// that the given user is a part of.
+// GetOAuthGroupsForUser returns a list of "org" and "org:team" strings, that the given user is a part of.
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
- UserID: user.ID,
- IncludePrivate: !onlyPublicGroups,
+ UserID: user.ID,
+ IncludeVisibility: util.Iif(onlyPublicGroups, api.VisibleTypePublic, api.VisibleTypePrivate),
})
if err != nil {
return nil, fmt.Errorf("GetUserOrgList: %w", err)
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 553496713f..d3a0f718a7 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -10,93 +10,56 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/structs"
"github.com/gobwas/glob"
"github.com/pkg/errors"
)
// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
-func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) structs.CommitStatusState {
- // matchedCount is the number of `CommitStatus.Context` that match any context of `requiredContexts`
- matchedCount := 0
- returnedStatus := structs.CommitStatusSuccess
-
- if len(requiredContexts) > 0 {
- requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts))
- for _, ctx := range requiredContexts {
- if gp, err := glob.Compile(ctx); err != nil {
- log.Error("glob.Compile %s failed. Error: %v", ctx, err)
- } else {
- requiredContextsGlob[ctx] = gp
- }
- }
-
- for _, gp := range requiredContextsGlob {
- var targetStatus structs.CommitStatusState
- for _, commitStatus := range commitStatuses {
- if gp.Match(commitStatus.Context) {
- targetStatus = commitStatus.State
- matchedCount++
- break
- }
- }
-
- // If required rule not match any action, then it is pending
- if targetStatus == "" {
- if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
- returnedStatus = structs.CommitStatusPending
- }
- break
- }
-
- if targetStatus.NoBetterThan(returnedStatus) {
- returnedStatus = targetStatus
- }
- }
+func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) commitstatus.CommitStatusState {
+ if len(commitStatuses) == 0 {
+ return commitstatus.CommitStatusPending
}
- if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
- status := git_model.CalcCommitStatus(commitStatuses)
- if status != nil {
- return status.State
- }
- return structs.CommitStatusSuccess
+ if len(requiredContexts) == 0 {
+ return git_model.CalcCommitStatus(commitStatuses).State
}
- return returnedStatus
-}
-
-// IsCommitStatusContextSuccess returns true if all required status check contexts succeed.
-func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requiredContexts []string) bool {
- // If no specific context is required, require that last commit status is a success
- if len(requiredContexts) == 0 {
- status := git_model.CalcCommitStatus(commitStatuses)
- if status == nil || status.State != structs.CommitStatusSuccess {
- return false
+ requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts))
+ for _, ctx := range requiredContexts {
+ if gp, err := glob.Compile(ctx); err != nil {
+ log.Error("glob.Compile %s failed. Error: %v", ctx, err)
+ } else {
+ requiredContextsGlob[ctx] = gp
}
- return true
}
- for _, ctx := range requiredContexts {
- var found bool
+ requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses))
+ for _, gp := range requiredContextsGlob {
for _, commitStatus := range commitStatuses {
- if commitStatus.Context == ctx {
- if commitStatus.State != structs.CommitStatusSuccess {
- return false
- }
-
- found = true
+ if gp.Match(commitStatus.Context) {
+ requiredCommitStatuses = append(requiredCommitStatuses, commitStatus)
break
}
}
- if !found {
- return false
- }
}
- return true
+ if len(requiredCommitStatuses) == 0 {
+ return commitstatus.CommitStatusPending
+ }
+
+ returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State
+ if len(requiredCommitStatuses) == len(requiredContexts) {
+ return returnedStatus
+ }
+
+ if returnedStatus == commitstatus.CommitStatusFailure {
+ return commitstatus.CommitStatusFailure
+ }
+ // even if part of success, return pending
+ return commitstatus.CommitStatusPending
}
// IsPullCommitStatusPass returns if all required status checks PASS
@@ -117,7 +80,7 @@ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (
}
// GetPullRequestCommitStatusState returns pull request merged commit status state
-func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
+func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (commitstatus.CommitStatusState, error) {
// Ensure HeadRepo is loaded
if err := pr.LoadHeadRepo(ctx); err != nil {
return "", errors.Wrap(err, "LoadHeadRepo")
diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go
index 592acdd55c..b985a9de8e 100644
--- a/services/pull/commit_status_test.go
+++ b/services/pull/commit_status_test.go
@@ -8,58 +8,76 @@ import (
"testing"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/commitstatus"
"github.com/stretchr/testify/assert"
)
func TestMergeRequiredContextsCommitStatus(t *testing.T) {
- testCases := [][]*git_model.CommitStatus{
+ cases := []struct {
+ commitStatuses []*git_model.CommitStatus
+ requiredContexts []string
+ expected commitstatus.CommitStatusState
+ }{
{
- {Context: "Build 1", State: structs.CommitStatusSuccess},
- {Context: "Build 2", State: structs.CommitStatusSuccess},
- {Context: "Build 3", State: structs.CommitStatusSuccess},
+ commitStatuses: []*git_model.CommitStatus{},
+ requiredContexts: []string{},
+ expected: commitstatus.CommitStatusPending,
},
{
- {Context: "Build 1", State: structs.CommitStatusSuccess},
- {Context: "Build 2", State: structs.CommitStatusSuccess},
- {Context: "Build 2t", State: structs.CommitStatusPending},
+ commitStatuses: []*git_model.CommitStatus{
+ {Context: "Build xxx", State: commitstatus.CommitStatusSkipped},
+ },
+ requiredContexts: []string{"Build*"},
+ expected: commitstatus.CommitStatusSuccess,
},
{
- {Context: "Build 1", State: structs.CommitStatusSuccess},
- {Context: "Build 2", State: structs.CommitStatusSuccess},
- {Context: "Build 2t", State: structs.CommitStatusFailure},
+ commitStatuses: []*git_model.CommitStatus{
+ {Context: "Build 1", State: commitstatus.CommitStatusSkipped},
+ {Context: "Build 2", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 3", State: commitstatus.CommitStatusSuccess},
+ },
+ requiredContexts: []string{"Build*"},
+ expected: commitstatus.CommitStatusSuccess,
},
{
- {Context: "Build 1", State: structs.CommitStatusSuccess},
- {Context: "Build 2", State: structs.CommitStatusSuccess},
- {Context: "Build 2t", State: structs.CommitStatusSuccess},
+ commitStatuses: []*git_model.CommitStatus{
+ {Context: "Build 1", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2t", State: commitstatus.CommitStatusPending},
+ },
+ requiredContexts: []string{"Build*", "Build 2t*"},
+ expected: commitstatus.CommitStatusPending,
},
{
- {Context: "Build 1", State: structs.CommitStatusSuccess},
- {Context: "Build 2", State: structs.CommitStatusSuccess},
- {Context: "Build 2t", State: structs.CommitStatusSuccess},
+ commitStatuses: []*git_model.CommitStatus{
+ {Context: "Build 1", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2t", State: commitstatus.CommitStatusFailure},
+ },
+ requiredContexts: []string{"Build*", "Build 2t*"},
+ expected: commitstatus.CommitStatusFailure,
+ },
+ {
+ commitStatuses: []*git_model.CommitStatus{
+ {Context: "Build 1", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2t", State: commitstatus.CommitStatusSuccess},
+ },
+ requiredContexts: []string{"Build*", "Build 2t*", "Build 3*"},
+ expected: commitstatus.CommitStatusPending,
+ },
+ {
+ commitStatuses: []*git_model.CommitStatus{
+ {Context: "Build 1", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2", State: commitstatus.CommitStatusSuccess},
+ {Context: "Build 2t", State: commitstatus.CommitStatusSuccess},
+ },
+ requiredContexts: []string{"Build*", "Build *", "Build 2t*", "Build 1*"},
+ expected: commitstatus.CommitStatusSuccess,
},
}
- testCasesRequiredContexts := [][]string{
- {"Build*"},
- {"Build*", "Build 2t*"},
- {"Build*", "Build 2t*"},
- {"Build*", "Build 2t*", "Build 3*"},
- {"Build*", "Build *", "Build 2t*", "Build 1*"},
- }
-
- testCasesExpected := []structs.CommitStatusState{
- structs.CommitStatusSuccess,
- structs.CommitStatusPending,
- structs.CommitStatusFailure,
- structs.CommitStatusPending,
- structs.CommitStatusSuccess,
- }
-
- for i, commitStatuses := range testCases {
- if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] {
- assert.Fail(t, "Test case failed", "Test case %d failed", i+1)
- }
+ for i, c := range cases {
+ assert.Equal(t, c.expected, MergeRequiredContextsCommitStatus(c.commitStatuses, c.requiredContexts), "case %d", i)
}
}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 256db847ef..2a2f47e880 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -13,6 +13,7 @@ import (
"regexp"
"strconv"
"strings"
+ "unicode"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@@ -161,6 +162,41 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr
return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil)
}
+func AddCommitMessageTailer(message, tailerKey, tailerValue string) string {
+ tailerLine := tailerKey + ": " + tailerValue
+ message = strings.ReplaceAll(message, "\r\n", "\n")
+ message = strings.ReplaceAll(message, "\r", "\n")
+ if strings.Contains(message, "\n"+tailerLine+"\n") || strings.HasSuffix(message, "\n"+tailerLine) {
+ return message
+ }
+
+ if !strings.HasSuffix(message, "\n") {
+ message += "\n"
+ }
+ pos1 := strings.LastIndexByte(message[:len(message)-1], '\n')
+ pos2 := -1
+ if pos1 != -1 {
+ pos2 = strings.IndexByte(message[pos1:], ':')
+ if pos2 != -1 {
+ pos2 += pos1
+ }
+ }
+ var lastLineKey string
+ if pos1 != -1 && pos2 != -1 {
+ lastLineKey = message[pos1+1 : pos2]
+ }
+
+ isLikelyTailerLine := lastLineKey != "" && unicode.IsUpper(rune(lastLineKey[0])) && strings.Contains(message, "-")
+ for i := 0; isLikelyTailerLine && i < len(lastLineKey); i++ {
+ r := rune(lastLineKey[i])
+ isLikelyTailerLine = unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-'
+ }
+ if !strings.HasSuffix(message, "\n\n") && !isLikelyTailerLine {
+ message += "\n"
+ }
+ return message + tailerLine
+}
+
// ErrInvalidMergeStyle represents an error if merging with disabled merge strategy
type ErrInvalidMergeStyle struct {
ID int64
@@ -396,10 +432,13 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
func commitAndSignNoAuthor(ctx *mergeContext, message string) error {
cmdCommit := git.NewCommand("commit").AddOptionFormat("--message=%s", message)
- if ctx.signKeyID == "" {
+ if ctx.signKey == nil {
cmdCommit.AddArguments("--no-gpg-sign")
} else {
- cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID)
+ if ctx.signKey.Format != "" {
+ cmdCommit.AddConfig("gpg.format", ctx.signKey.Format)
+ }
+ cmdCommit.AddOptionFormat("-S%s", ctx.signKey.KeyID)
}
if err := cmdCommit.Run(ctx, ctx.RunOpts()); err != nil {
log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go
index 719cc6b965..31a1e13734 100644
--- a/services/pull/merge_prepare.go
+++ b/services/pull/merge_prepare.go
@@ -27,7 +27,7 @@ type mergeContext struct {
doer *user_model.User
sig *git.Signature
committer *git.Signature
- signKeyID string // empty for no-sign, non-empty to sign
+ signKey *git.SigningKey
env []string
}
@@ -99,9 +99,9 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
mergeCtx.committer = mergeCtx.sig
// Determine if we should sign
- sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch)
+ sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch)
if sign {
- mergeCtx.signKeyID = keyID
+ mergeCtx.signKey = key
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
mergeCtx.committer = signer
}
diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go
index 72660cd3c5..0049c0b117 100644
--- a/services/pull/merge_squash.go
+++ b/services/pull/merge_squash.go
@@ -5,7 +5,6 @@ package pull
import (
"fmt"
- "strings"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -66,18 +65,19 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() {
// add trailer
- if !strings.Contains(message, "Co-authored-by: "+sig.String()) {
- message += "\nCo-authored-by: " + sig.String()
- }
- message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String())
+ message = AddCommitMessageTailer(message, "Co-authored-by", sig.String())
+ message = AddCommitMessageTailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used
}
cmdCommit := git.NewCommand("commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
AddOptionFormat("--message=%s", message)
- if ctx.signKeyID == "" {
+ if ctx.signKey == nil {
cmdCommit.AddArguments("--no-gpg-sign")
} else {
- cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID)
+ if ctx.signKey.Format != "" {
+ cmdCommit.AddConfig("gpg.format", ctx.signKey.Format)
+ }
+ cmdCommit.AddOptionFormat("-S%s", ctx.signKey.KeyID)
}
if err := cmdCommit.Run(ctx, ctx.RunOpts()); err != nil {
log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
diff --git a/services/pull/merge_test.go b/services/pull/merge_test.go
index 6df6f55d46..91abeb9d9c 100644
--- a/services/pull/merge_test.go
+++ b/services/pull/merge_test.go
@@ -65,3 +65,28 @@ func Test_expandDefaultMergeMessage(t *testing.T) {
})
}
}
+
+func TestAddCommitMessageTailer(t *testing.T) {
+ // add tailer for empty message
+ assert.Equal(t, "\n\nTest-tailer: TestValue", AddCommitMessageTailer("", "Test-tailer", "TestValue"))
+
+ // add tailer for message without newlines
+ assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title", "Test-tailer", "TestValue"))
+ assert.Equal(t, "title\n\nNot tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nNot tailer: xxx", "Test-tailer", "TestValue"))
+ assert.Equal(t, "title\n\nNotTailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nNotTailer: xxx", "Test-tailer", "TestValue"))
+ assert.Equal(t, "title\n\nnot-tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nnot-tailer: xxx", "Test-tailer", "TestValue"))
+
+ // add tailer for message with one EOL
+ assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n", "Test-tailer", "TestValue"))
+
+ // add tailer for message with two EOLs
+ assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\n", "Test-tailer", "TestValue"))
+
+ // add tailer for message with existing tailer (won't duplicate)
+ assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nTest-tailer: TestValue", "Test-tailer", "TestValue"))
+ assert.Equal(t, "title\n\nTest-tailer: TestValue\n", AddCommitMessageTailer("title\n\nTest-tailer: TestValue\n", "Test-tailer", "TestValue"))
+
+ // add tailer for message with existing tailer and different value (will append)
+ assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTailer("title\n\nTest-tailer: v1", "Test-tailer", "v2"))
+ assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTailer("title\n\nTest-tailer: v1\n", "Test-tailer", "v2"))
+}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index f879f61136..701c4f4d32 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -945,12 +945,6 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
return stringBuilder.String()
}
-// GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status
-func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) {
- _, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues)
- return lastStatus, err
-}
-
// GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
if err := issues.LoadPullRequests(ctx); err != nil {
diff --git a/services/pull/update.go b/services/pull/update.go
index 5cc5e2b134..b8f84e3d65 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -41,22 +41,6 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
return fmt.Errorf("HeadBranch of PR %d is up to date", pr.Index)
}
- if rebase {
- defer func() {
- go AddTestPullRequestTask(TestPullRequestOptions{
- RepoID: pr.BaseRepo.ID,
- Doer: doer,
- Branch: pr.BaseBranch,
- IsSync: false,
- IsForcePush: false,
- OldCommitID: "",
- NewCommitID: "",
- })
- }()
-
- return updateHeadByRebaseOnToBase(ctx, pr, doer)
- }
-
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("unable to load BaseRepo for %-v during update-by-merge: %v", pr, err)
return fmt.Errorf("unable to load BaseRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
@@ -74,6 +58,22 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
}
+ defer func() {
+ go AddTestPullRequestTask(TestPullRequestOptions{
+ RepoID: pr.BaseRepo.ID,
+ Doer: doer,
+ Branch: pr.BaseBranch,
+ IsSync: false,
+ IsForcePush: false,
+ OldCommitID: "",
+ NewCommitID: "",
+ })
+ }()
+
+ if rebase {
+ return updateHeadByRebaseOnToBase(ctx, pr, doer)
+ }
+
// TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment
// ideally in the future the "merge" functions should be refactored to decouple from the PullRequest
// now use a fake reverse PR to switch head&base repos/branches
@@ -90,19 +90,6 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
}
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
-
- defer func() {
- go AddTestPullRequestTask(TestPullRequestOptions{
- RepoID: reversePR.HeadRepo.ID,
- Doer: doer,
- Branch: reversePR.HeadBranch,
- IsSync: false,
- IsForcePush: false,
- OldCommitID: "",
- NewCommitID: "",
- })
- }()
-
return err
}
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 97ba22ace5..3b958e0d4c 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -260,7 +260,7 @@ func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesT
}
return err
}
- repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: ctxUser,
Private: true,
ListOptions: db.ListOptions{
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index f369a303e6..44cf61df43 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -14,12 +14,12 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
+ "code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
- api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/notify"
)
@@ -47,7 +47,7 @@ func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheVal
return nil
}
-func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
+func updateCommitStatusCache(repoID int64, branchName string, state commitstatus.CommitStatusState, targetURL string) error {
c := cache.GetCache()
bs, err := json.Marshal(commitStatusCacheValue{
State: state.String(),
@@ -127,7 +127,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
for i, repo := range repos {
if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
results[i] = &git_model.CommitStatus{
- State: api.CommitStatusState(cv.State),
+ State: commitstatus.CommitStatusState(cv.State),
TargetURL: cv.TargetURL,
}
} else {
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 046159722a..06a980c9a8 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/storage"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ issue_service "code.gitea.io/gitea/services/issue"
"xorm.io/builder"
)
@@ -193,7 +194,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
// Delete Issues and related objects
var attachmentPaths []string
- if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil {
+ if attachmentPaths, err = issue_service.DeleteIssuesByRepoID(ctx, repoID); err != nil {
return err
}
@@ -374,7 +375,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
// DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner
func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error {
for {
- repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: 1,
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 493ff9998d..1cf30edc7b 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -293,15 +293,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
}
var sign bool
- var keyID string
+ var key *git.SigningKey
var signer *git.Signature
if opts.ParentCommitID != "" {
- sign, keyID, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID)
+ sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID)
} else {
- sign, keyID, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser)
+ sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser)
}
if sign {
- cmdCommitTree.AddOptionFormat("-S%s", keyID)
+ if key.Format != "" {
+ cmdCommitTree.AddConfig("gpg.format", key.Format)
+ }
+ cmdCommitTree.AddOptionFormat("-S%s", key.KeyID)
if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email {
// Add trailers
diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go
index 9edc0aa39f..35c6effca5 100644
--- a/services/repository/fork_test.go
+++ b/services/repository/fork_test.go
@@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
@@ -38,7 +39,7 @@ func TestForkRepository(t *testing.T) {
assert.False(t, repo_model.IsErrReachLimitOfRepo(err))
// change AllowForkWithoutMaximumLimit to false for the test
- setting.Repository.AllowForkWithoutMaximumLimit = false
+ defer test.MockVariableValue(&setting.Repository.AllowForkWithoutMaximumLimit, false)()
// user has reached maximum limit of repositories
user.MaxRepoCreation = 0
fork2, err := ForkRepository(git.DefaultContext, user, user, ForkRepoOptions{
diff --git a/services/repository/generate.go b/services/repository/generate.go
index b02f7c9482..77a43b4e39 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -42,10 +42,8 @@ type expansion struct {
var defaultTransformers = []transformer{
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
- {Name: "CAMEL", Transform: func(str string) string {
- return xstrings.FirstRuneToLower(xstrings.ToCamelCase(str))
- }},
- {Name: "PASCAL", Transform: xstrings.ToCamelCase},
+ {Name: "CAMEL", Transform: xstrings.ToCamelCase},
+ {Name: "PASCAL", Transform: xstrings.ToPascalCase},
{Name: "LOWER", Transform: strings.ToLower},
{Name: "UPPER", Transform: strings.ToUpper},
{Name: "TITLE", Transform: util.ToTitleCase},
diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go
index b0f97d0ffb..1163c392c9 100644
--- a/services/repository/generate_test.go
+++ b/services/repository/generate_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
var giteaTemplate = []byte(`
@@ -65,3 +66,26 @@ func TestFileNameSanitize(t *testing.T) {
assert.Equal(t, "_", fileNameSanitize("\u0000"))
assert.Equal(t, "目标", fileNameSanitize("目标"))
}
+
+func TestTransformers(t *testing.T) {
+ cases := []struct {
+ name string
+ expected string
+ }{
+ {"SNAKE", "abc_def_xyz"},
+ {"KEBAB", "abc-def-xyz"},
+ {"CAMEL", "abcDefXyz"},
+ {"PASCAL", "AbcDefXyz"},
+ {"LOWER", "abc_def-xyz"},
+ {"UPPER", "ABC_DEF-XYZ"},
+ {"TITLE", "Abc_def-Xyz"},
+ }
+
+ input := "Abc_Def-XYZ"
+ assert.Len(t, defaultTransformers, len(cases))
+ for i, c := range cases {
+ tf := defaultTransformers[i]
+ require.Equal(t, c.name, tf.Name)
+ assert.Equal(t, c.expected, tf.Transform(input), "case %s", c.name)
+ }
+}
diff --git a/services/repository/init.go b/services/repository/init.go
index bd777b8a2f..1eeeb4aa4f 100644
--- a/services/repository/init.go
+++ b/services/repository/init.go
@@ -42,9 +42,12 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
cmd := git.NewCommand("commit", "--message=Initial commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
- sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
+ sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
if sign {
- cmd.AddOptionFormat("-S%s", keyID)
+ if key.Format != "" {
+ cmd.AddConfig("gpg.format", key.Format)
+ }
+ cmd.AddOptionFormat("-S%s", key.KeyID)
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
// need to set the committer to the KeyID owner
diff --git a/services/user/update.go b/services/user/update.go
index 4a39f4f783..c4fd2d60fe 100644
--- a/services/user/update.go
+++ b/services/user/update.go
@@ -15,6 +15,26 @@ import (
"code.gitea.io/gitea/modules/structs"
)
+type UpdateOptionField[T any] struct {
+ FieldValue T
+ FromSync bool
+}
+
+func UpdateOptionFieldFromValue[T any](value T) optional.Option[UpdateOptionField[T]] {
+ return optional.Some(UpdateOptionField[T]{FieldValue: value})
+}
+
+func UpdateOptionFieldFromSync[T any](value T) optional.Option[UpdateOptionField[T]] {
+ return optional.Some(UpdateOptionField[T]{FieldValue: value, FromSync: true})
+}
+
+func UpdateOptionFieldFromPtr[T any](value *T) optional.Option[UpdateOptionField[T]] {
+ if value == nil {
+ return optional.None[UpdateOptionField[T]]()
+ }
+ return UpdateOptionFieldFromValue(*value)
+}
+
type UpdateOptions struct {
KeepEmailPrivate optional.Option[bool]
FullName optional.Option[string]
@@ -32,7 +52,7 @@ type UpdateOptions struct {
DiffViewStyle optional.Option[string]
AllowCreateOrganization optional.Option[bool]
IsActive optional.Option[bool]
- IsAdmin optional.Option[bool]
+ IsAdmin optional.Option[UpdateOptionField[bool]]
EmailNotificationsPreference optional.Option[string]
SetLastLogin bool
RepoAdminChangeTeamAccess optional.Option[bool]
@@ -111,13 +131,18 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
cols = append(cols, "is_restricted")
}
if opts.IsAdmin.Has() {
- if !opts.IsAdmin.Value() && user_model.IsLastAdminUser(ctx, u) {
- return user_model.ErrDeleteLastAdminUser{UID: u.ID}
+ if opts.IsAdmin.Value().FieldValue /* true */ {
+ u.IsAdmin = opts.IsAdmin.Value().FieldValue // set IsAdmin=true
+ cols = append(cols, "is_admin")
+ } else if !user_model.IsLastAdminUser(ctx, u) /* not the last admin */ {
+ u.IsAdmin = opts.IsAdmin.Value().FieldValue // it's safe to change it from false to true (not the last admin)
+ cols = append(cols, "is_admin")
+ } else /* IsAdmin=false but this is the last admin user */ { //nolint
+ if !opts.IsAdmin.Value().FromSync {
+ return user_model.ErrDeleteLastAdminUser{UID: u.ID}
+ }
+ // else: syncing from external-source, this user is the last admin, so skip the "IsAdmin=false" change
}
-
- u.IsAdmin = opts.IsAdmin.Value()
-
- cols = append(cols, "is_admin")
}
if opts.Visibility.Has() {
diff --git a/services/user/update_test.go b/services/user/update_test.go
index fc24a6c212..27513e8040 100644
--- a/services/user/update_test.go
+++ b/services/user/update_test.go
@@ -22,7 +22,11 @@ func TestUpdateUser(t *testing.T) {
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.Error(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{
- IsAdmin: optional.Some(false),
+ IsAdmin: UpdateOptionFieldFromValue(false),
+ }))
+
+ assert.NoError(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{
+ IsAdmin: UpdateOptionFieldFromSync(false),
}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
@@ -38,7 +42,7 @@ func TestUpdateUser(t *testing.T) {
MaxRepoCreation: optional.Some(10),
IsRestricted: optional.Some(true),
IsActive: optional.Some(false),
- IsAdmin: optional.Some(true),
+ IsAdmin: UpdateOptionFieldFromValue(true),
Visibility: optional.Some(structs.VisibleTypePrivate),
KeepActivityPrivate: optional.Some(true),
Language: optional.Some("lang"),
@@ -60,7 +64,7 @@ func TestUpdateUser(t *testing.T) {
assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation)
assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted)
assert.Equal(t, opts.IsActive.Value(), user.IsActive)
- assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin)
+ assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin)
assert.Equal(t, opts.Visibility.Value(), user.Visibility)
assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
assert.Equal(t, opts.Language.Value(), user.Language)
@@ -80,7 +84,7 @@ func TestUpdateUser(t *testing.T) {
assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation)
assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted)
assert.Equal(t, opts.IsActive.Value(), user.IsActive)
- assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin)
+ assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin)
assert.Equal(t, opts.Visibility.Value(), user.Visibility)
assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
assert.Equal(t, opts.Language.Value(), user.Language)
diff --git a/services/user/user.go b/services/user/user.go
index 1aeebff142..c7252430de 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/agit"
asymkey_service "code.gitea.io/gitea/services/asymkey"
@@ -177,8 +178,8 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: 1,
},
- UserID: u.ID,
- IncludePrivate: true,
+ UserID: u.ID,
+ IncludeVisibility: structs.VisibleTypePrivate,
})
if err != nil {
return fmt.Errorf("unable to find org list for %s[%d]. Error: %w", u.Name, u.ID, err)
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 6bac02712b..5a805347e3 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -13,6 +13,7 @@ import (
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/convert"
@@ -84,7 +85,8 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
func TestWebhookUserMail(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
- setting.Service.NoReplyAddress = "no-reply.com"
+ defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")()
+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email)
assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email)
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 9405f7cfc8..0a955406e2 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -194,7 +194,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
if sign {
- commitTreeOpts.KeyID = signingKey
+ commitTreeOpts.Key = signingKey
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
committer = signer
}
@@ -316,7 +316,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
if sign {
- commitTreeOpts.KeyID = signingKey
+ commitTreeOpts.Key = signingKey
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
committer = signer
}