aboutsummaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/actions/artifact.go8
-rw-r--r--models/actions/run.go20
-rw-r--r--models/actions/runner.go9
-rw-r--r--models/actions/schedule.go13
-rw-r--r--models/actions/variable.go22
-rw-r--r--models/activities/action.go34
-rw-r--r--models/activities/action_list.go28
-rw-r--r--models/admin/task.go2
-rw-r--r--models/asymkey/gpg_key.go12
-rw-r--r--models/asymkey/gpg_key_add.go8
-rw-r--r--models/asymkey/gpg_key_commit_verification.go2
-rw-r--r--models/asymkey/gpg_key_common.go12
-rw-r--r--models/asymkey/gpg_key_test.go25
-rw-r--r--models/auth/access_token_scope.go4
-rw-r--r--models/git/branch.go19
-rw-r--r--models/issues/comment_code.go6
-rw-r--r--models/issues/issue.go40
-rw-r--r--models/issues/issue_project.go50
-rw-r--r--models/issues/issue_search.go24
-rw-r--r--models/issues/review.go2
-rw-r--r--models/issues/stopwatch.go4
-rw-r--r--models/migrations/v1_23/v299.go6
-rw-r--r--models/migrations/v1_23/v300.go6
-rw-r--r--models/migrations/v1_23/v301.go6
-rw-r--r--models/migrations/v1_23/v302.go5
-rw-r--r--models/migrations/v1_23/v302_test.go51
-rw-r--r--models/migrations/v1_23/v303.go6
-rw-r--r--models/migrations/v1_23/v304.go5
-rw-r--r--models/migrations/v1_23/v304_test.go40
-rw-r--r--models/migrations/v1_23/v306.go6
-rw-r--r--models/migrations/v1_23/v310.go6
-rw-r--r--models/migrations/v1_23/v311.go7
-rw-r--r--models/packages/package_version.go4
-rw-r--r--models/project/column.go49
-rw-r--r--models/project/column_test.go8
-rw-r--r--models/project/issue.go43
-rw-r--r--models/project/project.go3
-rw-r--r--models/user/avatar.go23
-rw-r--r--models/user/avatar_test.go40
-rw-r--r--models/user/user_system.go20
-rw-r--r--models/user/user_system_test.go32
-rw-r--r--models/webhook/webhook.go6
-rw-r--r--models/webhook/webhook_test.go2
43 files changed, 538 insertions, 180 deletions
diff --git a/models/actions/artifact.go b/models/actions/artifact.go
index 0bc66ba24e..706eb2e43a 100644
--- a/models/actions/artifact.go
+++ b/models/actions/artifact.go
@@ -114,6 +114,12 @@ type FindArtifactsOptions struct {
Status int
}
+func (opts FindArtifactsOptions) ToOrders() string {
+ return "id"
+}
+
+var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
+
func (opts FindArtifactsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
@@ -132,7 +138,7 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
return cond
}
-// ActionArtifactMeta is the meta data of an artifact
+// ActionArtifactMeta is the meta-data of an artifact
type ActionArtifactMeta struct {
ArtifactName string
FileSize int64
diff --git a/models/actions/run.go b/models/actions/run.go
index f40bc1eb3d..a268f760db 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -194,7 +194,7 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
-func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
+func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID,
@@ -204,14 +204,16 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
})
if err != nil {
- return err
+ return nil, err
}
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
if total == 0 {
- return nil
+ return nil, nil
}
+ cancelledJobs := make([]*ActionRunJob, 0, total)
+
// Iterate over each found run and cancel its associated jobs.
for _, run := range runs {
// Find all jobs associated with the current run.
@@ -219,7 +221,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
RunID: run.ID,
})
if err != nil {
- return err
+ return cancelledJobs, err
}
// Iterate over each job and attempt to cancel it.
@@ -238,27 +240,29 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
// Update the job's status and stopped time in the database.
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil {
- return err
+ return cancelledJobs, err
}
// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
if n == 0 {
- return fmt.Errorf("job has changed, try again")
+ return cancelledJobs, fmt.Errorf("job has changed, try again")
}
+ cancelledJobs = append(cancelledJobs, job)
// Continue with the next job.
continue
}
// If the job has an associated task, try to stop the task, effectively cancelling the job.
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
- return err
+ return cancelledJobs, err
}
+ cancelledJobs = append(cancelledJobs, job)
}
}
// Return nil to indicate successful cancellation of all running and waiting jobs.
- return nil
+ return cancelledJobs, nil
}
// InsertRun inserts a run
diff --git a/models/actions/runner.go b/models/actions/runner.go
index b35a76680c..c4e5f9d121 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -167,6 +167,7 @@ func init() {
type FindRunnerOptions struct {
db.ListOptions
+ IDs []int64
RepoID int64
OwnerID int64 // it will be ignored if RepoID is set
Sort string
@@ -178,6 +179,14 @@ type FindRunnerOptions struct {
func (opts FindRunnerOptions) ToConds() builder.Cond {
cond := builder.NewCond()
+ if len(opts.IDs) > 0 {
+ if len(opts.IDs) == 1 {
+ cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+ } else {
+ cond = cond.And(builder.In("id", opts.IDs))
+ }
+ }
+
if opts.RepoID > 0 {
c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
if opts.WithAvailable {
diff --git a/models/actions/schedule.go b/models/actions/schedule.go
index 961ffd0851..cb381dfd43 100644
--- a/models/actions/schedule.go
+++ b/models/actions/schedule.go
@@ -120,21 +120,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
return committer.Commit()
}
-func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
+func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([]*ActionRunJob, error) {
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
// There is no other place we can do this because the app.ini will be changed manually
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
- return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
+ return nil, fmt.Errorf("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
- if err := CancelPreviousJobs(
+ jobs, err := CancelPreviousJobs(
ctx,
repo.ID,
repo.DefaultBranch,
"",
webhook_module.HookEventSchedule,
- ); err != nil {
- return fmt.Errorf("CancelPreviousJobs: %v", err)
+ )
+ if err != nil {
+ return jobs, fmt.Errorf("CancelPreviousJobs: %v", err)
}
- return nil
+ return jobs, nil
}
diff --git a/models/actions/variable.go b/models/actions/variable.go
index d0f917d923..163bb12c93 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin
type FindVariablesOpts struct {
db.ListOptions
+ IDs []int64
RepoID int64
OwnerID int64 // it will be ignored if RepoID is set
Name string
@@ -65,6 +66,15 @@ type FindVariablesOpts struct {
func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
+
+ if len(opts.IDs) > 0 {
+ if len(opts.IDs) == 1 {
+ cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+ } else {
+ cond = cond.And(builder.In("id", opts.IDs))
+ }
+ }
+
// Since we now support instance-level variables,
// there is no need to check for null values for `owner_id` and `repo_id`
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
@@ -85,12 +95,12 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab
return db.Find[ActionVariable](ctx, opts)
}
-func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
- count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
- Update(&ActionVariable{
- Name: variable.Name,
- Data: variable.Data,
- })
+func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
+ variable.Name = strings.ToUpper(variable.Name)
+ count, err := db.GetEngine(ctx).
+ ID(variable.ID).
+ Cols(cols...).
+ Update(variable)
return count != 0, err
}
diff --git a/models/activities/action.go b/models/activities/action.go
index 65d95fbe66..7432e073b9 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -72,9 +72,9 @@ func (at ActionType) String() string {
case ActionRenameRepo:
return "rename_repo"
case ActionStarRepo:
- return "star_repo"
+ return "star_repo" // will not displayed in feeds.tmpl
case ActionWatchRepo:
- return "watch_repo"
+ return "watch_repo" // will not displayed in feeds.tmpl
case ActionCommitRepo:
return "commit_repo"
case ActionCreateIssue:
@@ -454,6 +454,24 @@ func ActivityReadable(user, doer *user_model.User) bool {
doer != nil && (doer.IsAdmin || user.ID == doer.ID)
}
+func FeedDateCond(opts GetFeedsOptions) builder.Cond {
+ cond := builder.NewCond()
+ if opts.Date == "" {
+ return cond
+ }
+
+ dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
+ if err != nil {
+ log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
+ } else {
+ dateHigh := dateLow.Add(86399000000000) // 23h59m59s
+
+ cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
+ cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
+ }
+ return cond
+}
+
func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
cond := builder.NewCond()
@@ -534,17 +552,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
cond = cond.And(builder.Eq{"is_deleted": false})
}
- if opts.Date != "" {
- dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
- if err != nil {
- log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
- } else {
- dateHigh := dateLow.Add(86399000000000) // 23h59m59s
-
- cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
- cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
- }
- }
+ cond = cond.And(FeedDateCond(opts))
return cond, nil
}
diff --git a/models/activities/action_list.go b/models/activities/action_list.go
index 5f9acb8f2a..f7ea48f03e 100644
--- a/models/activities/action_list.go
+++ b/models/activities/action_list.go
@@ -208,9 +208,31 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
}
- cond, err := ActivityQueryCondition(ctx, opts)
- if err != nil {
- return nil, 0, err
+ var err error
+ var cond builder.Cond
+ // if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition
+ if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) {
+ cond = builder.Eq{
+ "user_id": opts.RequestedUser.ID,
+ }.And(
+ FeedDateCond(opts),
+ )
+
+ if !opts.IncludeDeleted {
+ cond = cond.And(builder.Eq{"is_deleted": false})
+ }
+
+ if !opts.IncludePrivate {
+ cond = cond.And(builder.Eq{"is_private": false})
+ }
+ if opts.OnlyPerformedBy {
+ cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
+ }
+ } else {
+ cond, err = ActivityQueryCondition(ctx, opts)
+ if err != nil {
+ return nil, 0, err
+ }
}
actions := make([]*Action, 0, opts.PageSize)
diff --git a/models/admin/task.go b/models/admin/task.go
index 10f8e6d570..0541a8ec78 100644
--- a/models/admin/task.go
+++ b/models/admin/task.go
@@ -44,7 +44,7 @@ func init() {
// TranslatableMessage represents JSON struct that can be translated with a Locale
type TranslatableMessage struct {
Format string
- Args []any `json:"omitempty"`
+ Args []any `json:",omitempty"`
}
// LoadRepo loads repository of the task
diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go
index 5236b2d450..e921340730 100644
--- a/models/asymkey/gpg_key.go
+++ b/models/asymkey/gpg_key.go
@@ -13,8 +13,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
- "github.com/keybase/go-crypto/openpgp"
- "github.com/keybase/go-crypto/openpgp/packet"
+ "github.com/ProtonMail/go-crypto/openpgp"
+ "github.com/ProtonMail/go-crypto/openpgp/packet"
"xorm.io/builder"
)
@@ -141,7 +141,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
// Parse Subkeys
subkeys := make([]*GPGKey, len(e.Subkeys))
for i, k := range e.Subkeys {
- subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
+ subkeyExpiry := expiry
+ if k.Sig.KeyLifetimeSecs != nil {
+ subkeyExpiry = k.PublicKey.CreationTime.Add(time.Duration(*k.Sig.KeyLifetimeSecs) * time.Second)
+ }
+ subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, subkeyExpiry)
if err != nil {
return nil, ErrGPGKeyParsing{ParseError: err}
}
@@ -156,7 +160,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
emails := make([]*user_model.EmailAddress, 0, len(e.Identities))
for _, ident := range e.Identities {
- if ident.Revocation != nil {
+ if ident.Revoked(time.Now()) {
continue
}
email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
diff --git a/models/asymkey/gpg_key_add.go b/models/asymkey/gpg_key_add.go
index 11124b1366..6c0f6e01a7 100644
--- a/models/asymkey/gpg_key_add.go
+++ b/models/asymkey/gpg_key_add.go
@@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
- "github.com/keybase/go-crypto/openpgp"
+ "github.com/ProtonMail/go-crypto/openpgp"
)
// __________________ ________ ____ __.
@@ -83,12 +83,12 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
verified := false
// Handle provided signature
if signature != "" {
- signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature))
+ signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil)
if err != nil {
- signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature))
+ signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil)
}
if err != nil {
- signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature))
+ signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
}
if err != nil {
log.Error("Unable to validate token signature. Error: %v", err)
diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 26fad3bb3f..9219a509df 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -16,7 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/keybase/go-crypto/openpgp/packet"
+ "github.com/ProtonMail/go-crypto/openpgp/packet"
)
// __________________ ________ ____ __.
diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go
index 28cb8f4e76..92c34a2569 100644
--- a/models/asymkey/gpg_key_common.go
+++ b/models/asymkey/gpg_key_common.go
@@ -13,9 +13,9 @@ import (
"strings"
"time"
- "github.com/keybase/go-crypto/openpgp"
- "github.com/keybase/go-crypto/openpgp/armor"
- "github.com/keybase/go-crypto/openpgp/packet"
+ "github.com/ProtonMail/go-crypto/openpgp"
+ "github.com/ProtonMail/go-crypto/openpgp/armor"
+ "github.com/ProtonMail/go-crypto/openpgp/packet"
)
// __________________ ________ ____ __.
@@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) {
return pkey, nil
}
-// getExpiryTime extract the expire time of primary key based on sig
+// getExpiryTime extract the expiry time of primary key based on sig
func getExpiryTime(e *openpgp.Entity) time.Time {
expiry := time.Time{}
// Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
@@ -88,12 +88,12 @@ func getExpiryTime(e *openpgp.Entity) time.Time {
for _, ident := range e.Identities {
if selfSig == nil {
selfSig = ident.SelfSignature
- } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
+ } else if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
selfSig = ident.SelfSignature
break
}
}
- if selfSig.KeyLifetimeSecs != nil {
+ if selfSig != nil && selfSig.KeyLifetimeSecs != nil {
expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
}
return expiry
diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go
index d3fbb01d82..de463dfbe2 100644
--- a/models/asymkey/gpg_key_test.go
+++ b/models/asymkey/gpg_key_test.go
@@ -13,7 +13,8 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
- "github.com/keybase/go-crypto/openpgp/packet"
+ "github.com/ProtonMail/go-crypto/openpgp"
+ "github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/stretchr/testify/assert"
)
@@ -403,3 +404,25 @@ func TestTryGetKeyIDFromSignature(t *testing.T) {
IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
}))
}
+
+func TestParseGPGKey(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ assert.NoError(t, db.Insert(db.DefaultContext, &user_model.EmailAddress{UID: 1, Email: "email1@example.com", IsActivated: true}))
+
+ // create a key for test email
+ e, err := openpgp.NewEntity("name", "comment", "email1@example.com", nil)
+ assert.NoError(t, err)
+ k, err := parseGPGKey(db.DefaultContext, 1, e, true)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, k.KeyID)
+ assert.NotEmpty(t, k.Emails) // the key is valid, matches the email
+
+ // then revoke the key
+ for _, id := range e.Identities {
+ id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)})
+ }
+ k, err = parseGPGKey(db.DefaultContext, 1, e, true)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, k.KeyID)
+ assert.Empty(t, k.Emails) // the key is revoked, matches no email
+}
diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go
index 897ff3fc9e..aa1cc5b9de 100644
--- a/models/auth/access_token_scope.go
+++ b/models/auth/access_token_scope.go
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
return bitmap.toScope(), nil
}
+func (s AccessTokenScope) HasPermissionScope() bool {
+ return s != "" && s != AccessTokenScopePublicOnly
+}
+
// PublicOnly checks if this token scope is limited to public resources
func (s AccessTokenScope) PublicOnly() (bool, error) {
bitmap, err := s.parse()
diff --git a/models/git/branch.go b/models/git/branch.go
index e683ce47e6..9ac6c45578 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -167,9 +167,24 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
BranchName: branchName,
}
}
+ // FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
+ // It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
+ // In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
return &branch, nil
}
+// IsBranchExist returns true if the branch exists in the repository.
+func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
+ var branch Branch
+ has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
+ if err != nil {
+ return false, err
+ } else if !has {
+ return false, nil
+ }
+ return !branch.IsDeleted, nil
+}
+
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
branches := make([]*Branch, 0, len(branchNames))
@@ -440,6 +455,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
}
type RecentlyPushedNewBranch struct {
+ BranchRepo *repo_model.Repository
+ BranchName string
BranchDisplayName string
BranchLink string
BranchCompareURL string
@@ -540,7 +557,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
}
newBranches = append(newBranches, &RecentlyPushedNewBranch{
+ BranchRepo: branch.Repo,
BranchDisplayName: branchDisplayName,
+ BranchName: branch.Name,
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
CommitTime: branch.CommitTime,
diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index 67a77ceb13..b562aab500 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -86,8 +86,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
ids = append(ids, comment.ReviewID)
}
}
- if err := e.In("id", ids).Find(&reviews); err != nil {
- return nil, err
+ if len(ids) > 0 {
+ if err := e.In("id", ids).Find(&reviews); err != nil {
+ return nil, err
+ }
}
n := 0
diff --git a/models/issues/issue.go b/models/issues/issue.go
index fe347c2715..f4b575d804 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -17,6 +17,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -531,6 +532,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
return issue, nil
}
+func isPullToCond(isPull optional.Option[bool]) builder.Cond {
+ if isPull.Has() {
+ return builder.Eq{"is_pull": isPull.Value()}
+ }
+ return builder.NewCond()
+}
+
+func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
+ issues := make([]*Issue, 0, pageSize)
+ err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
+ And(isPullToCond(isPull)).
+ OrderBy("updated_unix DESC").
+ Limit(pageSize).
+ Find(&issues)
+ return issues, err
+}
+
+func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
+ cond := builder.NewCond()
+ if excludedID > 0 {
+ cond = cond.And(builder.Neq{"`id`": excludedID})
+ }
+
+ // It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
+ // The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content"
+ // But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
+ // So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
+ cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
+
+ issues := make([]*Issue, 0, pageSize)
+ err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
+ And(isPullToCond(isPull)).
+ And(cond).
+ OrderBy("updated_unix DESC, `index` DESC").
+ Limit(pageSize).
+ Find(&issues)
+ return issues, err
+}
+
// GetIssueWithAttrsByIndex returns issue by index in a repository.
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
issue, err := GetIssueByIndex(ctx, repoID, index)
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index c4515fd898..0185244783 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -38,13 +38,30 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
}
// ProjectColumnID return project column id if issue was assigned to one
-func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
+func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
var ip project_model.ProjectIssue
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
- if err != nil || !has {
- return 0
+ if err != nil {
+ return 0, err
+ } else if !has {
+ return 0, nil
+ }
+ return ip.ProjectColumnID, nil
+}
+
+func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) {
+ issues := make([]project_model.ProjectIssue, 0)
+ if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil {
+ return nil, err
+ }
+ result := make(map[int64]int64, len(issues))
+ for _, issue := range issues {
+ if issue.ProjectColumnID == 0 {
+ issue.ProjectColumnID = defaultColumnID
+ }
+ result[issue.IssueID] = issue.ProjectColumnID
}
- return ip.ProjectColumnID
+ return result, nil
}
// LoadIssuesFromColumn load issues assigned to this column
@@ -59,11 +76,11 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
}
if b.Default {
- issues, err := Issues(ctx, &IssuesOptions{
- ProjectColumnID: db.NoConditionID,
- ProjectID: b.ProjectID,
- SortType: "project-column-sorting",
- })
+ issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
+ o.ProjectColumnID = db.NoConditionID
+ o.ProjectID = b.ProjectID
+ o.SortType = "project-column-sorting"
+ }))
if err != nil {
return nil, err
}
@@ -77,19 +94,6 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
return issueList, nil
}
-// LoadIssuesFromColumnList load issues assigned to the columns
-func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) {
- issuesMap := make(map[int64]IssueList, len(bs))
- for i := range bs {
- il, err := LoadIssuesFromColumn(ctx, bs[i], opts)
- if err != nil {
- return nil, err
- }
- issuesMap[bs[i].ID] = il
- }
- return issuesMap, nil
-}
-
// IssueAssignOrRemoveProject changes the project associated with an issue
// If newProjectID is 0, the issue is removed from the project
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
@@ -110,7 +114,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
}
if newColumnID == 0 {
- newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
+ newDefaultColumn, err := newProject.MustDefaultColumn(ctx)
if err != nil {
return err
}
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index f1cd125d49..694b918755 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -49,9 +49,9 @@ type IssuesOptions struct { //nolint
// prioritize issues from this repo
PriorityRepoID int64
IsArchived optional.Option[bool]
- Org *organization.Organization // issues permission scope
- Team *organization.Team // issues permission scope
- User *user_model.User // issues permission scope
+ Owner *user_model.User // issues permission scope, it could be an organization or a user
+ Team *organization.Team // issues permission scope
+ Doer *user_model.User // issues permission scope
}
// Copy returns a copy of the options.
@@ -273,8 +273,12 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
applyLabelsCondition(sess, opts)
- if opts.User != nil {
- sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
+ if opts.Owner != nil {
+ sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
+ }
+
+ if opts.Doer != nil && !opts.Doer.IsAdmin {
+ sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value()))
}
}
@@ -321,20 +325,20 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
}
// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table
-func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond {
+func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_model.User, team *organization.Team, isPull bool) builder.Cond {
cond := builder.NewCond()
unitType := unit.TypeIssues
if isPull {
unitType = unit.TypePullRequests
}
- if org != nil {
+ if owner != nil && owner.IsOrganization() {
if team != nil {
- cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos
+ cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
} else {
cond = cond.And(
builder.Or(
- repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos
- repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues
+ repo_model.UserOrgUnitRepoCond(repoIDstr, userID, owner.ID, unitType), // team member repos
+ repo_model.UserOrgPublicUnitRepoCond(userID, owner.ID), // user org public non-member repos, TODO: check repo has issues
),
)
}
diff --git a/models/issues/review.go b/models/issues/review.go
index 3e787273be..d8dc539c3b 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -663,7 +663,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
}
if review != nil {
- // skip it when reviewer hase been request to review
+ // skip it when reviewer has been request to review
if review.Type == ReviewTypeRequest {
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
}
diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go
index 629af95b57..baffe04ace 100644
--- a/models/issues/stopwatch.go
+++ b/models/issues/stopwatch.go
@@ -48,7 +48,7 @@ func (s Stopwatch) Seconds() int64 {
// Duration returns a human-readable duration string based on local server time
func (s Stopwatch) Duration() string {
- return util.SecToTime(s.Seconds())
+ return util.SecToHours(s.Seconds())
}
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
@@ -201,7 +201,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
Doer: user,
Issue: issue,
Repo: issue.Repo,
- Content: util.SecToTime(timediff),
+ Content: util.SecToHours(timediff),
Type: CommentTypeStopTracking,
TimeID: tt.ID,
}); err != nil {
diff --git a/models/migrations/v1_23/v299.go b/models/migrations/v1_23/v299.go
index f6db960c3b..e5fde3749b 100644
--- a/models/migrations/v1_23/v299.go
+++ b/models/migrations/v1_23/v299.go
@@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
}
- return x.Sync(new(Comment), new(Issue))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(Comment), new(Issue))
+ return err
}
diff --git a/models/migrations/v1_23/v300.go b/models/migrations/v1_23/v300.go
index f1f1cccdbf..51de43da5e 100644
--- a/models/migrations/v1_23/v300.go
+++ b/models/migrations/v1_23/v300.go
@@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error {
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
}
- return x.Sync(new(ProtectedBranch))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(ProtectedBranch))
+ return err
}
diff --git a/models/migrations/v1_23/v301.go b/models/migrations/v1_23/v301.go
index b7797f6c6b..99c8e3d8ea 100644
--- a/models/migrations/v1_23/v301.go
+++ b/models/migrations/v1_23/v301.go
@@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
type oauth2Application struct {
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
}
- return x.Sync(new(oauth2Application))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(oauth2Application))
+ return err
}
diff --git a/models/migrations/v1_23/v302.go b/models/migrations/v1_23/v302.go
index d7ea03eb3d..5d2e9b1438 100644
--- a/models/migrations/v1_23/v302.go
+++ b/models/migrations/v1_23/v302.go
@@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
LogExpired bool `xorm:"index(stopped_log_expired)"`
}
- return x.Sync(new(ActionTask))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreDropIndices: true,
+ }, new(ActionTask))
+ return err
}
diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go
new file mode 100644
index 0000000000..29e85ae9d9
--- /dev/null
+++ b/models/migrations/v1_23/v302_test.go
@@ -0,0 +1,51 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/migrations/base"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) {
+ type ActionTask struct {
+ ID int64
+ JobID int64
+ Attempt int64
+ RunnerID int64 `xorm:"index"`
+ Status int `xorm:"index"`
+ Started timeutil.TimeStamp `xorm:"index"`
+ Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
+
+ RepoID int64 `xorm:"index"`
+ OwnerID int64 `xorm:"index"`
+ CommitSHA string `xorm:"index"`
+ IsForkPullRequest bool
+
+ Token string `xorm:"-"`
+ TokenHash string `xorm:"UNIQUE"` // sha256 of token
+ TokenSalt string
+ TokenLastEight string `xorm:"index token_last_eight"`
+
+ LogFilename string // file name of log
+ LogInStorage bool // read log from database or from storage
+ LogLength int64 // lines count
+ LogSize int64 // blob size
+ LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset
+ LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
+
+ Created timeutil.TimeStamp `xorm:"created"`
+ Updated timeutil.TimeStamp `xorm:"updated index"`
+ }
+
+ // Prepare and load the testing database
+ x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask))
+ defer deferable()
+
+ assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x))
+}
diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go
index adfe917d3f..1e36388930 100644
--- a/models/migrations/v1_23/v303.go
+++ b/models/migrations/v1_23/v303.go
@@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error {
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
}
- return x.Sync(new(Comment))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(Comment))
+ return err
}
diff --git a/models/migrations/v1_23/v304.go b/models/migrations/v1_23/v304.go
index 65cffedbd9..e108f47779 100644
--- a/models/migrations/v1_23/v304.go
+++ b/models/migrations/v1_23/v304.go
@@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
type Release struct {
Sha1 string `xorm:"INDEX VARCHAR(64)"`
}
- return x.Sync(new(Release))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreDropIndices: true,
+ }, new(Release))
+ return err
}
diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go
new file mode 100644
index 0000000000..955219d3f9
--- /dev/null
+++ b/models/migrations/v1_23/v304_test.go
@@ -0,0 +1,40 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/migrations/base"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_AddIndexForReleaseSha1(t *testing.T) {
+ type Release struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX UNIQUE(n)"`
+ PublisherID int64 `xorm:"INDEX"`
+ TagName string `xorm:"INDEX UNIQUE(n)"`
+ OriginalAuthor string
+ OriginalAuthorID int64 `xorm:"index"`
+ LowerTagName string
+ Target string
+ Title string
+ Sha1 string `xorm:"VARCHAR(64)"`
+ NumCommits int64
+ Note string `xorm:"TEXT"`
+ IsDraft bool `xorm:"NOT NULL DEFAULT false"`
+ IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
+ IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
+ }
+
+ // Prepare and load the testing database
+ x, deferable := base.PrepareTestEnv(t, 0, new(Release))
+ defer deferable()
+
+ assert.NoError(t, AddIndexForReleaseSha1(x))
+}
diff --git a/models/migrations/v1_23/v306.go b/models/migrations/v1_23/v306.go
index 276b438e95..a1e698fe31 100644
--- a/models/migrations/v1_23/v306.go
+++ b/models/migrations/v1_23/v306.go
@@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error {
type ProtectedBranch struct {
BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
}
- return x.Sync(new(ProtectedBranch))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(ProtectedBranch))
+ return err
}
diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go
index 394417f5a0..c856a708f9 100644
--- a/models/migrations/v1_23/v310.go
+++ b/models/migrations/v1_23/v310.go
@@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error {
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
}
- return x.Sync(new(ProtectedBranch))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(ProtectedBranch))
+ return err
}
diff --git a/models/migrations/v1_23/v311.go b/models/migrations/v1_23/v311.go
index 0fc1ac8c0e..21293d83be 100644
--- a/models/migrations/v1_23/v311.go
+++ b/models/migrations/v1_23/v311.go
@@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}
-
- return x.Sync(new(Issue))
+ _, err := x.SyncWithOptions(xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ }, new(Issue))
+ return err
}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 278e8e3a86..bb7fd895f8 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
default:
e.Desc("package_version.created_unix")
}
-
- // Sort by id for stable order with duplicates in the other field
- e.Asc("package_version.id")
+ e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
}
// SearchVersions gets all versions of packages matching the search options
diff --git a/models/project/column.go b/models/project/column.go
index 222f448599..5f581b5880 100644
--- a/models/project/column.go
+++ b/models/project/column.go
@@ -48,6 +48,8 @@ type Column struct {
ProjectID int64 `xorm:"INDEX NOT NULL"`
CreatorID int64 `xorm:"NOT NULL"`
+ NumIssues int64 `xorm:"-"`
+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
@@ -57,20 +59,6 @@ func (Column) TableName() string {
return "project_board" // TODO: the legacy table name should be project_column
}
-// NumIssues return counter of all issues assigned to the column
-func (c *Column) NumIssues(ctx context.Context) int {
- total, err := db.GetEngine(ctx).Table("project_issue").
- Where("project_id=?", c.ProjectID).
- And("project_board_id=?", c.ID).
- GroupBy("issue_id").
- Cols("issue_id").
- Count()
- if err != nil {
- return 0
- }
- return int(total)
-}
-
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
issues := make([]*ProjectIssue, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
@@ -192,7 +180,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
if err != nil {
return err
}
- defaultColumn, err := project.GetDefaultColumn(ctx)
+ defaultColumn, err := project.MustDefaultColumn(ctx)
if err != nil {
return err
}
@@ -257,8 +245,8 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
return columns, nil
}
-// GetDefaultColumn return default column and ensure only one exists
-func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
+// getDefaultColumn return default column and ensure only one exists
+func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
var column Column
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
@@ -270,6 +258,33 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
if has {
return &column, nil
}
+ return nil, ErrProjectColumnNotExist{ColumnID: 0}
+}
+
+// MustDefaultColumn returns the default column for a project.
+// If one exists, it is returned
+// If none exists, the first column will be elevated to the default column of this project
+func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
+ c, err := p.getDefaultColumn(ctx)
+ if err != nil && !IsErrProjectColumnNotExist(err) {
+ return nil, err
+ }
+ if c != nil {
+ return c, nil
+ }
+
+ var column Column
+ has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ column.Default = true
+ if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil {
+ return nil, err
+ }
+ return &column, nil
+ }
// create a default column if none is found
column = Column{
diff --git a/models/project/column_test.go b/models/project/column_test.go
index 566667e45d..66db23a3e4 100644
--- a/models/project/column_test.go
+++ b/models/project/column_test.go
@@ -20,19 +20,19 @@ func TestGetDefaultColumn(t *testing.T) {
assert.NoError(t, err)
// check if default column was added
- column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
+ column, err := projectWithoutDefault.MustDefaultColumn(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(5), column.ProjectID)
- assert.Equal(t, "Uncategorized", column.Title)
+ assert.Equal(t, "Done", column.Title)
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
assert.NoError(t, err)
// check if multiple defaults were removed
- column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
+ column, err = projectWithMultipleDefaults.MustDefaultColumn(db.DefaultContext)
assert.NoError(t, err)
assert.Equal(t, int64(6), column.ProjectID)
- assert.Equal(t, int64(9), column.ID)
+ assert.Equal(t, int64(9), column.ID) // there are 2 default columns in the test data, use the latest one
// set 8 as default column
assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
diff --git a/models/project/issue.go b/models/project/issue.go
index b4347a9c2b..98eed2a213 100644
--- a/models/project/issue.go
+++ b/models/project/issue.go
@@ -8,7 +8,6 @@ import (
"fmt"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
@@ -34,48 +33,6 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
return err
}
-// NumIssues return counter of all issues assigned to a project
-func (p *Project) NumIssues(ctx context.Context) int {
- c, err := db.GetEngine(ctx).Table("project_issue").
- Where("project_id=?", p.ID).
- GroupBy("issue_id").
- Cols("issue_id").
- Count()
- if err != nil {
- log.Error("NumIssues: %v", err)
- return 0
- }
- return int(c)
-}
-
-// NumClosedIssues return counter of closed issues assigned to a project
-func (p *Project) NumClosedIssues(ctx context.Context) int {
- c, err := db.GetEngine(ctx).Table("project_issue").
- Join("INNER", "issue", "project_issue.issue_id=issue.id").
- Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
- Cols("issue_id").
- Count()
- if err != nil {
- log.Error("NumClosedIssues: %v", err)
- return 0
- }
- return int(c)
-}
-
-// NumOpenIssues return counter of open issues assigned to a project
-func (p *Project) NumOpenIssues(ctx context.Context) int {
- c, err := db.GetEngine(ctx).Table("project_issue").
- Join("INNER", "issue", "project_issue.issue_id=issue.id").
- Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
- Cols("issue_id").
- Count()
- if err != nil {
- log.Error("NumOpenIssues: %v", err)
- return 0
- }
- return int(c)
-}
-
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
if c.ProjectID != newColumn.ProjectID {
return fmt.Errorf("columns have to be in the same project")
diff --git a/models/project/project.go b/models/project/project.go
index 7385efd39d..78cba8b574 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -97,6 +97,9 @@ type Project struct {
Type Type
RenderedContent template.HTML `xorm:"-"`
+ NumOpenIssues int64 `xorm:"-"`
+ NumClosedIssues int64 `xorm:"-"`
+ NumIssues int64 `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
diff --git a/models/user/avatar.go b/models/user/avatar.go
index 5453c78fc6..3d9fc4452f 100644
--- a/models/user/avatar.go
+++ b/models/user/avatar.go
@@ -38,27 +38,32 @@ 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() {
+ // ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
+ // which comes from git configure information
+ if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
return avatars.DefaultAvatarLink()
}
diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go
index 1078875ee1..a1cc01316f 100644
--- a/models/user/avatar_test.go
+++ b/models/user/avatar_test.go
@@ -4,13 +4,19 @@
package user
import (
+ "context"
+ "io"
+ "strings"
"testing"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestUserAvatarLink(t *testing.T) {
@@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) {
link = u.AvatarLink(db.DefaultContext)
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
}
+
+func TestUserAvatarGenerate(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ var err error
+ tmpDir := t.TempDir()
+ storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir})
+ require.NoError(t, err)
+
+ u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
+
+ // there was no avatar, generate a new one
+ assert.Empty(t, u.Avatar)
+ err = GenerateRandomAvatar(db.DefaultContext, u)
+ require.NoError(t, err)
+ assert.NotEmpty(t, u.Avatar)
+
+ // make sure the generated one exists
+ oldAvatarPath := u.CustomAvatarRelativePath()
+ _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+ require.NoError(t, err)
+ // and try to change its content
+ _, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
+ require.NoError(t, err)
+
+ // try to generate again
+ err = GenerateRandomAvatar(db.DefaultContext, u)
+ require.NoError(t, err)
+ assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
+ f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
+ require.NoError(t, err)
+ defer f.Close()
+ content, _ := io.ReadAll(f)
+ assert.Equal(t, "abcd", string(content))
+}
diff --git a/models/user/user_system.go b/models/user/user_system.go
index 612cdb2cae..7ac48f5ea5 100644
--- a/models/user/user_system.go
+++ b/models/user/user_system.go
@@ -24,6 +24,10 @@ func NewGhostUser() *User {
}
}
+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 {
@@ -48,6 +52,10 @@ const (
ActionsEmail = "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{
@@ -65,6 +73,16 @@ func NewActionsUser() *User {
}
}
-func (u *User) IsActions() bool {
+func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID
}
+
+func GetSystemUserByName(name string) *User {
+ if IsGhostUserName(name) {
+ return NewGhostUser()
+ }
+ if IsGiteaActionsUserName(name) {
+ return NewActionsUser()
+ }
+ return nil
+}
diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go
new file mode 100644
index 0000000000..97768b509b
--- /dev/null
+++ b/models/user/user_system_test.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSystemUser(t *testing.T) {
+ u, err := GetPossibleUserByID(db.DefaultContext, -1)
+ require.NoError(t, err)
+ assert.Equal(t, "Ghost", u.Name)
+ assert.Equal(t, "ghost", u.LowerName)
+ assert.True(t, u.IsGhost())
+ assert.True(t, IsGhostUserName("gHost"))
+
+ u, err = GetPossibleUserByID(db.DefaultContext, -2)
+ require.NoError(t, err)
+ assert.Equal(t, "gitea-actions", u.Name)
+ assert.Equal(t, "gitea-actions", u.LowerName)
+ assert.True(t, u.IsGiteaActions())
+ assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
+
+ _, err = GetPossibleUserByID(db.DefaultContext, -3)
+ require.Error(t, err)
+}
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index 894357e36a..a17582c0c9 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -299,6 +299,11 @@ func (w *Webhook) HasPackageEvent() bool {
(w.ChooseEvents && w.HookEvents.Package)
}
+func (w *Webhook) HasStatusEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Status)
+}
+
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
return w.SendEverything ||
@@ -337,6 +342,7 @@ func (w *Webhook) EventCheckers() []struct {
{w.HasReleaseEvent, webhook_module.HookEventRelease},
{w.HasPackageEvent, webhook_module.HookEventPackage},
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
+ {w.HasStatusEvent, webhook_module.HookEventStatus},
}
}
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index c6c3f40d46..5e135369e6 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -74,7 +74,7 @@ func TestWebhook_EventsArray(t *testing.T) {
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
- "package", "pull_request_review_request",
+ "package", "pull_request_review_request", "status",
},
(&Webhook{
HookEvent: &webhook_module.HookEvent{SendEverything: true},