mirror of
https://github.com/go-gitea/gitea
synced 2025-12-07 13:28:25 +00:00
Merge branch 'main' into Badge
This commit is contained in:
@@ -361,6 +361,19 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
|
||||
return run, nil
|
||||
}
|
||||
|
||||
func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
|
||||
run := &ActionRun{
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Desc("index").Get(run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("latest run with repo_id %d: %w", repoID, util.ErrNotExist)
|
||||
}
|
||||
return run, nil
|
||||
}
|
||||
|
||||
func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) {
|
||||
var run ActionRun
|
||||
q := db.GetEngine(ctx).Where("repo_id=?", repoID).
|
||||
|
||||
@@ -23,14 +23,25 @@ import (
|
||||
)
|
||||
|
||||
// ActionRunner represents runner machines
|
||||
//
|
||||
// It can be:
|
||||
// 1. global runner, OwnerID is 0 and RepoID is 0
|
||||
// 2. org/user level runner, OwnerID is org/user ID and RepoID is 0
|
||||
// 3. repo level runner, OwnerID is 0 and RepoID is repo ID
|
||||
//
|
||||
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
|
||||
// or it will be complicated to find runners belonging to a specific owner.
|
||||
// For example, conditions like `OwnerID = 1` will also return runner {OwnerID: 1, RepoID: 1},
|
||||
// but it's a repo level runner, not an org/user level runner.
|
||||
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level runners.
|
||||
type ActionRunner struct {
|
||||
ID int64
|
||||
UUID string `xorm:"CHAR(36) UNIQUE"`
|
||||
Name string `xorm:"VARCHAR(255)"`
|
||||
Version string `xorm:"VARCHAR(64)"`
|
||||
OwnerID int64 `xorm:"index"` // org level runner, 0 means system
|
||||
OwnerID int64 `xorm:"index"`
|
||||
Owner *user_model.User `xorm:"-"`
|
||||
RepoID int64 `xorm:"index"` // repo level runner, if OwnerID also is zero, then it's a global
|
||||
RepoID int64 `xorm:"index"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Description string `xorm:"TEXT"`
|
||||
Base int // 0 native 1 docker 2 virtual machine
|
||||
@@ -157,7 +168,7 @@ func init() {
|
||||
type FindRunnerOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
OwnerID int64
|
||||
OwnerID int64 // it will be ignored if RepoID is set
|
||||
Sort string
|
||||
Filter string
|
||||
IsOnline optional.Option[bool]
|
||||
@@ -174,8 +185,7 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
|
||||
c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
|
||||
}
|
||||
cond = cond.And(c)
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
} else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set
|
||||
c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
if opts.WithAvailable {
|
||||
c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
|
||||
@@ -263,6 +273,11 @@ func DeleteRunner(ctx context.Context, id int64) error {
|
||||
|
||||
// CreateRunner creates new runner.
|
||||
func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
||||
if t.OwnerID != 0 && t.RepoID != 0 {
|
||||
// It's trying to create a runner that belongs to a repository, but OwnerID has been set accidentally.
|
||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||
t.OwnerID = 0
|
||||
}
|
||||
return db.Insert(ctx, t)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,23 @@ import (
|
||||
)
|
||||
|
||||
// ActionRunnerToken represents runner tokens
|
||||
//
|
||||
// It can be:
|
||||
// 1. global token, OwnerID is 0 and RepoID is 0
|
||||
// 2. org/user level token, OwnerID is org/user ID and RepoID is 0
|
||||
// 3. repo level token, OwnerID is 0 and RepoID is repo ID
|
||||
//
|
||||
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
|
||||
// or it will be complicated to find tokens belonging to a specific owner.
|
||||
// For example, conditions like `OwnerID = 1` will also return token {OwnerID: 1, RepoID: 1},
|
||||
// but it's a repo level token, not an org/user level token.
|
||||
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level tokens.
|
||||
type ActionRunnerToken struct {
|
||||
ID int64
|
||||
Token string `xorm:"UNIQUE"`
|
||||
OwnerID int64 `xorm:"index"` // org level runner, 0 means system
|
||||
OwnerID int64 `xorm:"index"`
|
||||
Owner *user_model.User `xorm:"-"`
|
||||
RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global
|
||||
RepoID int64 `xorm:"index"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
IsActive bool // true means it can be used
|
||||
|
||||
@@ -58,7 +69,14 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
|
||||
}
|
||||
|
||||
// NewRunnerToken creates a new active runner token and invalidate all old tokens
|
||||
// ownerID will be ignored and treated as 0 if repoID is non-zero.
|
||||
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
|
||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||
ownerID = 0
|
||||
}
|
||||
|
||||
token, err := util.CryptoRandomString(40)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -84,6 +102,12 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
|
||||
|
||||
// GetLatestRunnerToken returns the latest runner token
|
||||
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
// It's trying to get a runner token that belongs to a repository, but OwnerID has been set accidentally.
|
||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||
ownerID = 0
|
||||
}
|
||||
|
||||
var runnerToken ActionRunnerToken
|
||||
has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=?", ownerID, repoID).
|
||||
OrderBy("id DESC").Get(&runnerToken)
|
||||
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// ActionSchedule represents a schedule of a workflow file
|
||||
@@ -53,8 +51,6 @@ func GetReposMapByIDs(ctx context.Context, ids []int64) (map[int64]*repo_model.R
|
||||
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
|
||||
}
|
||||
|
||||
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
|
||||
// CreateScheduleTask creates new schedule task.
|
||||
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
|
||||
// Return early if there are no rows to insert
|
||||
@@ -80,19 +76,21 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
|
||||
now := time.Now()
|
||||
|
||||
for _, spec := range row.Specs {
|
||||
specRow := &ActionScheduleSpec{
|
||||
RepoID: row.RepoID,
|
||||
ScheduleID: row.ID,
|
||||
Spec: spec,
|
||||
}
|
||||
// Parse the spec and check for errors
|
||||
schedule, err := cronParser.Parse(spec)
|
||||
schedule, err := specRow.Parse()
|
||||
if err != nil {
|
||||
continue // skip to the next spec if there's an error
|
||||
}
|
||||
|
||||
specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix())
|
||||
|
||||
// Insert the new schedule spec row
|
||||
if err = db.Insert(ctx, &ActionScheduleSpec{
|
||||
RepoID: row.RepoID,
|
||||
ScheduleID: row.ID,
|
||||
Spec: spec,
|
||||
Next: timeutil.TimeStamp(schedule.Next(now).Unix()),
|
||||
}); err != nil {
|
||||
if err = db.Insert(ctx, specRow); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -32,8 +34,29 @@ type ActionScheduleSpec struct {
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// Parse parses the spec and returns a cron.Schedule
|
||||
// Unlike the default cron parser, Parse uses UTC timezone as the default if none is specified.
|
||||
func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) {
|
||||
return cronParser.Parse(s.Spec)
|
||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
schedule, err := parser.Parse(s.Spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the spec has specified a timezone, use it
|
||||
if strings.HasPrefix(s.Spec, "TZ=") || strings.HasPrefix(s.Spec, "CRON_TZ=") {
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
specSchedule, ok := schedule.(*cron.SpecSchedule)
|
||||
// If it's not a spec schedule, like "@every 5m", timezone is not relevant
|
||||
if !ok {
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
// Set the timezone to UTC
|
||||
specSchedule.Location = time.UTC
|
||||
return specSchedule, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionScheduleSpec_Parse(t *testing.T) {
|
||||
// Mock the local timezone is not UTC
|
||||
local := time.Local
|
||||
tz, err := time.LoadLocation("Asia/Shanghai")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
time.Local = local
|
||||
}()
|
||||
time.Local = tz
|
||||
|
||||
now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
spec string
|
||||
want string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "regular",
|
||||
spec: "0 10 * * *",
|
||||
want: "2024-07-31T10:00:00Z",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
spec: "0 10 * *",
|
||||
want: "",
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "with timezone",
|
||||
spec: "TZ=America/New_York 0 10 * * *",
|
||||
want: "2024-07-31T14:00:00Z",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "timezone irrelevant",
|
||||
spec: "@every 5m",
|
||||
want: "2024-07-31T07:52:55Z",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &ActionScheduleSpec{
|
||||
Spec: tt.spec,
|
||||
}
|
||||
got, err := s.Parse()
|
||||
tt.wantErr(t, err)
|
||||
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.want, got.Next(now).UTC().Format(time.RFC3339))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -35,7 +35,7 @@ type ActionTask struct {
|
||||
RunnerID int64 `xorm:"index"`
|
||||
Status Status `xorm:"index"`
|
||||
Started timeutil.TimeStamp `xorm:"index"`
|
||||
Stopped timeutil.TimeStamp
|
||||
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||
|
||||
RepoID int64 `xorm:"index"`
|
||||
OwnerID int64 `xorm:"index"`
|
||||
@@ -51,8 +51,8 @@ type ActionTask struct {
|
||||
LogInStorage bool // read log from database or from storage
|
||||
LogLength int64 // lines count
|
||||
LogSize int64 // blob size
|
||||
LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset
|
||||
LogExpired bool // files that are too old will be deleted
|
||||
LogIndexes LogIndexes `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"`
|
||||
@@ -470,6 +470,16 @@ func StopTask(ctx context.Context, taskID int64, status Status) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, limit int) ([]*ActionTask, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
tasks := make([]*ActionTask, 0, limit)
|
||||
// Check "stopped > 0" to avoid deleting tasks that are still running
|
||||
return tasks, e.Where("stopped > 0 AND stopped < ? AND log_expired = ?", olderThan, false).
|
||||
Limit(limit).
|
||||
Find(&tasks)
|
||||
}
|
||||
|
||||
func isSubset(set, subset []string) bool {
|
||||
m := make(container.Set[string], len(set))
|
||||
for _, v := range set {
|
||||
@@ -492,7 +502,13 @@ func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp {
|
||||
}
|
||||
|
||||
func logFileName(repoFullName string, taskID int64) string {
|
||||
return fmt.Sprintf("%s/%02x/%d.log", repoFullName, taskID%256, taskID)
|
||||
ret := fmt.Sprintf("%s/%02x/%d.log", repoFullName, taskID%256, taskID)
|
||||
|
||||
if setting.Actions.LogCompression.IsZstd() {
|
||||
ret += ".zst"
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getTaskIDFromCache(token string) int64 {
|
||||
|
||||
+25
-13
@@ -5,7 +5,6 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -15,6 +14,18 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ActionVariable represents a variable that can be used in actions
|
||||
//
|
||||
// It can be:
|
||||
// 1. global variable, OwnerID is 0 and RepoID is 0
|
||||
// 2. org/user level variable, OwnerID is org/user ID and RepoID is 0
|
||||
// 3. repo level variable, OwnerID is 0 and RepoID is repo ID
|
||||
//
|
||||
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
|
||||
// or it will be complicated to find variables belonging to a specific owner.
|
||||
// For example, conditions like `OwnerID = 1` will also return variable {OwnerID: 1, RepoID: 1},
|
||||
// but it's a repo level variable, not an org/user level variable.
|
||||
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level variables.
|
||||
type ActionVariable struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
|
||||
@@ -29,30 +40,26 @@ func init() {
|
||||
db.RegisterModel(new(ActionVariable))
|
||||
}
|
||||
|
||||
func (v *ActionVariable) Validate() error {
|
||||
if v.OwnerID != 0 && v.RepoID != 0 {
|
||||
return errors.New("a variable should not be bound to an owner and a repository at the same time")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
|
||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||
ownerID = 0
|
||||
}
|
||||
|
||||
variable := &ActionVariable{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
Name: strings.ToUpper(name),
|
||||
Data: data,
|
||||
}
|
||||
if err := variable.Validate(); err != nil {
|
||||
return variable, err
|
||||
}
|
||||
return variable, db.Insert(ctx, variable)
|
||||
}
|
||||
|
||||
type FindVariablesOpts struct {
|
||||
db.ListOptions
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
OwnerID int64 // it will be ignored if RepoID is set
|
||||
Name string
|
||||
}
|
||||
|
||||
@@ -60,8 +67,13 @@ func (opts FindVariablesOpts) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
// 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{"owner_id": opts.OwnerID})
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
if opts.RepoID != 0 { // if RepoID is set
|
||||
// ignore OwnerID and treat it as 0
|
||||
cond = cond.And(builder.Eq{"owner_id": 0})
|
||||
} else {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
|
||||
if opts.Name != "" {
|
||||
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
|
||||
|
||||
+39
-10
@@ -450,17 +450,46 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx).Where(cond).
|
||||
Select("`action`.*"). // this line will avoid select other joined table's columns
|
||||
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
|
||||
|
||||
opts.SetDefaultValues()
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
actions := make([]*Action, 0, opts.PageSize)
|
||||
count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
|
||||
var count int64
|
||||
|
||||
if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value.
|
||||
sess := db.GetEngine(ctx).Where(cond).
|
||||
Select("`action`.*"). // this line will avoid select other joined table's columns
|
||||
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
|
||||
|
||||
opts.SetDefaultValues()
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
|
||||
}
|
||||
} else {
|
||||
// First, only query which IDs are necessary, and only then query all actions to speed up the overall query
|
||||
sess := db.GetEngine(ctx).Where(cond).
|
||||
Select("`action`.id").
|
||||
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
|
||||
|
||||
opts.SetDefaultValues()
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
actionIDs := make([]int64, 0, opts.PageSize)
|
||||
if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil {
|
||||
return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err)
|
||||
}
|
||||
|
||||
count, err = db.GetEngine(ctx).Where(cond).
|
||||
Table("action").
|
||||
Cols("`action`.id").
|
||||
Join("INNER", "repository", "`repository`.id = `action`.repo_id").Count()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("Count: %w", err)
|
||||
}
|
||||
|
||||
if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil {
|
||||
return nil, 0, fmt.Errorf("Find: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ActionList(actions).LoadAttributes(ctx); err != nil {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -33,7 +34,8 @@ func TestAction_GetRepoLink(t *testing.T) {
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issue_model.Comment{ID: 2})
|
||||
action := &activities_model.Action{RepoID: repo.ID, CommentID: comment.ID}
|
||||
setting.AppSubURL = "/suburl"
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/suburl/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/suburl")()
|
||||
expected := path.Join(setting.AppSubURL, owner.Name, repo.Name)
|
||||
assert.Equal(t, expected, action.GetRepoLink(db.DefaultContext))
|
||||
assert.Equal(t, repo.HTMLURL(), action.GetRepoAbsoluteLink(db.DefaultContext))
|
||||
|
||||
@@ -286,13 +286,14 @@ type UserIDCount struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
// GetUIDsAndNotificationCounts between the two provided times
|
||||
// GetUIDsAndNotificationCounts returns the unread counts for every user between the two provided times.
|
||||
// It must return all user IDs which appear during the period, including count=0 for users who have read all.
|
||||
func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.TimeStamp) ([]UserIDCount, error) {
|
||||
sql := `SELECT user_id, count(*) AS count FROM notification ` +
|
||||
sql := `SELECT user_id, sum(case when status= ? then 1 else 0 end) AS count FROM notification ` +
|
||||
`WHERE user_id IN (SELECT user_id FROM notification WHERE updated_unix >= ? AND ` +
|
||||
`updated_unix < ?) AND status = ? GROUP BY user_id`
|
||||
`updated_unix < ?) GROUP BY user_id`
|
||||
var res []UserIDCount
|
||||
return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
|
||||
return res, db.GetEngine(ctx).SQL(sql, NotificationStatusUnread, since, until).Find(&res)
|
||||
}
|
||||
|
||||
// SetIssueReadBy sets issue to be read by given user.
|
||||
|
||||
@@ -229,35 +229,26 @@ func UpdatePublicKeyUpdated(ctx context.Context, id int64) error {
|
||||
|
||||
// PublicKeysAreExternallyManaged returns whether the provided KeyID represents an externally managed Key
|
||||
func PublicKeysAreExternallyManaged(ctx context.Context, keys []*PublicKey) ([]bool, error) {
|
||||
sources := make([]*auth.Source, 0, 5)
|
||||
sourceCache := make(map[int64]*auth.Source, len(keys))
|
||||
externals := make([]bool, len(keys))
|
||||
keyloop:
|
||||
|
||||
for i, key := range keys {
|
||||
if key.LoginSourceID == 0 {
|
||||
externals[i] = false
|
||||
continue keyloop
|
||||
continue
|
||||
}
|
||||
|
||||
var source *auth.Source
|
||||
|
||||
sourceloop:
|
||||
for _, s := range sources {
|
||||
if s.ID == key.LoginSourceID {
|
||||
source = s
|
||||
break sourceloop
|
||||
}
|
||||
}
|
||||
|
||||
if source == nil {
|
||||
source, ok := sourceCache[key.LoginSourceID]
|
||||
if !ok {
|
||||
var err error
|
||||
source, err = auth.GetSourceByID(ctx, key.LoginSourceID)
|
||||
if err != nil {
|
||||
if auth.IsErrSourceNotExist(err) {
|
||||
externals[i] = false
|
||||
sources[i] = &auth.Source{
|
||||
sourceCache[key.LoginSourceID] = &auth.Source{
|
||||
ID: key.LoginSourceID,
|
||||
}
|
||||
continue keyloop
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/42wim/sshsig"
|
||||
@@ -26,7 +28,6 @@ func Test_SSHParsePublicKey(t *testing.T) {
|
||||
length int
|
||||
content string
|
||||
}{
|
||||
{"dsa-1024", false, "dsa", 1024, "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment"},
|
||||
{"rsa-1024", false, "rsa", 1024, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n"},
|
||||
{"rsa-2048", false, "rsa", 2048, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMZXh+1OBUwSH9D45wTaxErQIN9IoC9xl7MKJkqvTvv6O5RR9YW/IK9FbfjXgXsppYGhsCZo1hFOOsXHMnfOORqu/xMDx4yPuyvKpw4LePEcg4TDipaDFuxbWOqc/BUZRZcXu41QAWfDLrInwsltWZHSeG7hjhpacl4FrVv9V1pS6Oc5Q1NxxEzTzuNLS/8diZrTm/YAQQ/+B+mzWI3zEtF4miZjjAljWd1LTBPvU23d29DcBmmFahcZ441XZsTeAwGxG/Q6j8NgNXj9WxMeWwxXV2jeAX/EBSpZrCVlCQ1yJswT6xCp8TuBnTiGWYMBNTbOZvPC4e0WI2/yZW/s5F nocomment"},
|
||||
{"ecdsa-256", false, "ecdsa", 256, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFQacN3PrOll7PXmN5B/ZNVahiUIqI05nbBlZk1KXsO3d06ktAWqbNflv2vEmA38bTFTfJ2sbn2B5ksT52cDDbA= nocomment"},
|
||||
@@ -170,7 +171,6 @@ func Test_calcFingerprint(t *testing.T) {
|
||||
fp string
|
||||
content string
|
||||
}{
|
||||
{"dsa-1024", false, "SHA256:fSIHQlpKMDsGPVAXI8BPYfRp+e2sfvSt1sMrPsFiXrc", "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment"},
|
||||
{"rsa-1024", false, "SHA256:vSnDkvRh/xM6kMxPidLgrUhq3mCN7CDaronCEm2joyQ", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n"},
|
||||
{"rsa-2048", false, "SHA256:ZHD//a1b9VuTq9XSunAeYjKeU1xDa2tBFZYrFr2Okkg", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMZXh+1OBUwSH9D45wTaxErQIN9IoC9xl7MKJkqvTvv6O5RR9YW/IK9FbfjXgXsppYGhsCZo1hFOOsXHMnfOORqu/xMDx4yPuyvKpw4LePEcg4TDipaDFuxbWOqc/BUZRZcXu41QAWfDLrInwsltWZHSeG7hjhpacl4FrVv9V1pS6Oc5Q1NxxEzTzuNLS/8diZrTm/YAQQ/+B+mzWI3zEtF4miZjjAljWd1LTBPvU23d29DcBmmFahcZ441XZsTeAwGxG/Q6j8NgNXj9WxMeWwxXV2jeAX/EBSpZrCVlCQ1yJswT6xCp8TuBnTiGWYMBNTbOZvPC4e0WI2/yZW/s5F nocomment"},
|
||||
{"ecdsa-256", false, "SHA256:Bqx/xgWqRKLtkZ0Lr4iZpgb+5lYsFpSwXwVZbPwuTRw", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFQacN3PrOll7PXmN5B/ZNVahiUIqI05nbBlZk1KXsO3d06ktAWqbNflv2vEmA38bTFTfJ2sbn2B5ksT52cDDbA= nocomment"},
|
||||
@@ -503,3 +503,11 @@ func runErr(t *testing.T, stdin []byte, args ...string) {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PublicKeysAreExternallyManaged(t *testing.T) {
|
||||
key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1})
|
||||
externals, err := PublicKeysAreExternallyManaged(db.DefaultContext, []*PublicKey{key1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, externals, 1)
|
||||
assert.False(t, externals[0])
|
||||
}
|
||||
|
||||
@@ -309,6 +309,22 @@ func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// HasAnyScope returns true if any of the scopes is contained in the string
|
||||
func (s AccessTokenScope) HasAnyScope(scopes ...AccessTokenScope) (bool, error) {
|
||||
bitmap, err := s.parse()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, s := range scopes {
|
||||
if has, err := bitmap.hasScope(s); has || err != nil {
|
||||
return has, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// hasScope returns true if the string has the given scope
|
||||
func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
|
||||
expectedBits, ok := allAccessTokenScopeBits[scope]
|
||||
|
||||
+24
-19
@@ -37,10 +37,11 @@ type OAuth2Application struct {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
|
||||
// "Authorization servers MUST record the client type in the client registration details"
|
||||
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
||||
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
|
||||
RedirectURIs []string `xorm:"redirect_uris JSON TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
|
||||
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
RedirectURIs []string `xorm:"redirect_uris JSON TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -251,21 +252,23 @@ func GetOAuth2ApplicationByID(ctx context.Context, id int64) (app *OAuth2Applica
|
||||
|
||||
// CreateOAuth2ApplicationOptions holds options to create an oauth2 application
|
||||
type CreateOAuth2ApplicationOptions struct {
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
RedirectURIs []string
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
SkipSecondaryAuthorization bool
|
||||
RedirectURIs []string
|
||||
}
|
||||
|
||||
// CreateOAuth2Application inserts a new oauth2 application
|
||||
func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) {
|
||||
clientID := uuid.New().String()
|
||||
app := &OAuth2Application{
|
||||
UID: opts.UserID,
|
||||
Name: opts.Name,
|
||||
ClientID: clientID,
|
||||
RedirectURIs: opts.RedirectURIs,
|
||||
ConfidentialClient: opts.ConfidentialClient,
|
||||
UID: opts.UserID,
|
||||
Name: opts.Name,
|
||||
ClientID: clientID,
|
||||
RedirectURIs: opts.RedirectURIs,
|
||||
ConfidentialClient: opts.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: opts.SkipSecondaryAuthorization,
|
||||
}
|
||||
if err := db.Insert(ctx, app); err != nil {
|
||||
return nil, err
|
||||
@@ -275,11 +278,12 @@ func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOp
|
||||
|
||||
// UpdateOAuth2ApplicationOptions holds options to update an oauth2 application
|
||||
type UpdateOAuth2ApplicationOptions struct {
|
||||
ID int64
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
RedirectURIs []string
|
||||
ID int64
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
SkipSecondaryAuthorization bool
|
||||
RedirectURIs []string
|
||||
}
|
||||
|
||||
// UpdateOAuth2Application updates an oauth2 application
|
||||
@@ -305,6 +309,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
|
||||
app.Name = opts.Name
|
||||
app.RedirectURIs = opts.RedirectURIs
|
||||
app.ConfidentialClient = opts.ConfidentialClient
|
||||
app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization
|
||||
|
||||
if err = updateOAuth2Application(ctx, app); err != nil {
|
||||
return nil, err
|
||||
@@ -315,7 +320,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
|
||||
}
|
||||
|
||||
func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error {
|
||||
if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client").Update(app); err != nil {
|
||||
if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client", "skip_secondary_authorization").Update(app); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -210,7 +210,7 @@ func CreateSource(ctx context.Context, source *Source) error {
|
||||
return ErrSourceAlreadyExist{source.Name}
|
||||
}
|
||||
// Synchronization is only available with LDAP for now
|
||||
if !source.IsLDAP() {
|
||||
if !source.IsLDAP() && !source.IsOAuth2() {
|
||||
source.IsSyncEnabled = false
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
|
||||
return had > 0, err
|
||||
}
|
||||
|
||||
// WebAuthnCredentials implementns the webauthn.User interface
|
||||
// WebAuthnCredentials implements the webauthn.User interface
|
||||
func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) {
|
||||
dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID)
|
||||
if err != nil {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 7320
|
||||
size: 7597
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
|
||||
|
||||
+21
-1
@@ -392,6 +392,13 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
||||
return err
|
||||
}
|
||||
|
||||
// 4.1 Update all not merged pull request head branch name
|
||||
if _, err = sess.Table("pull_request").Where("head_repo_id=? AND head_branch=? AND has_merged=?",
|
||||
repo.ID, from, false).
|
||||
Update(map[string]any{"head_branch": to}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. insert renamed branch record
|
||||
renamedBranch := &RenamedBranch{
|
||||
RepoID: repo.ID,
|
||||
@@ -427,7 +434,8 @@ type RecentlyPushedNewBranch struct {
|
||||
|
||||
// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
|
||||
// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
|
||||
// if opts.ListOptions is not set, we will only display top 2 latest branch
|
||||
// if opts.ListOptions is not set, we will only display top 2 latest branches.
|
||||
// Protected branches will be skipped since they are unlikely to be used to create new PRs.
|
||||
func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
|
||||
if doer == nil {
|
||||
return []*RecentlyPushedNewBranch{}, nil
|
||||
@@ -486,6 +494,18 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
|
||||
opts.MaxCount = 2
|
||||
}
|
||||
for _, branch := range branches {
|
||||
// whether the branch is protected
|
||||
protected, err := IsBranchProtected(ctx, branch.RepoID, branch.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("IsBranchProtected: %v", err)
|
||||
}
|
||||
if protected {
|
||||
// Skip protected branches,
|
||||
// since updates to protected branches often come from PR merges,
|
||||
// and they are unlikely to be used to create new PRs.
|
||||
continue
|
||||
}
|
||||
|
||||
// whether branch have already created PR
|
||||
count, err := db.GetEngine(ctx).Table("pull_request").
|
||||
// we should not only use branch name here, because if there are branches with same name in other repos,
|
||||
|
||||
@@ -171,13 +171,17 @@ func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (in
|
||||
return newIdx, nil
|
||||
}
|
||||
|
||||
func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
|
||||
func (status *CommitStatus) loadRepository(ctx context.Context) (err error) {
|
||||
if status.Repo == nil {
|
||||
status.Repo, err = repo_model.GetRepositoryByID(ctx, status.RepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *CommitStatus) loadCreator(ctx context.Context) (err error) {
|
||||
if status.Creator == nil && status.CreatorID > 0 {
|
||||
status.Creator, err = user_model.GetUserByID(ctx, status.CreatorID)
|
||||
if err != nil {
|
||||
@@ -187,6 +191,13 @@ func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
|
||||
if err := status.loadRepository(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return status.loadCreator(ctx)
|
||||
}
|
||||
|
||||
// APIURL returns the absolute APIURL to this commit-status.
|
||||
func (status *CommitStatus) APIURL(ctx context.Context) string {
|
||||
_ = status.loadAttributes(ctx)
|
||||
@@ -198,6 +209,25 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string {
|
||||
return lang.TrString("repo.commitstatus." + status.State.String())
|
||||
}
|
||||
|
||||
// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
|
||||
func (status *CommitStatus) HideActionsURL(ctx context.Context) {
|
||||
if status.RepoID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if status.Repo == nil {
|
||||
if err := status.loadRepository(ctx); err != nil {
|
||||
log.Error("loadRepository: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("%s/actions", status.Repo.Link())
|
||||
if strings.HasPrefix(status.TargetURL, prefix) {
|
||||
status.TargetURL = ""
|
||||
}
|
||||
}
|
||||
|
||||
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
|
||||
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
|
||||
var lastStatus *CommitStatus
|
||||
@@ -506,3 +536,19 @@ func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo
|
||||
repo,
|
||||
)
|
||||
}
|
||||
|
||||
// CommitStatusesHideActionsURL hide Gitea Actions urls
|
||||
func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) {
|
||||
idToRepos := make(map[int64]*repo_model.Repository)
|
||||
for _, status := range statuses {
|
||||
if status == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if status.Repo == nil {
|
||||
status.Repo = idToRepos[status.RepoID]
|
||||
}
|
||||
status.HideActionsURL(ctx)
|
||||
idToRepos[status.RepoID] = status.Repo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -231,3 +233,26 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) {
|
||||
assert.Equal(t, "compliance/lint-backend", contexts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitStatusesHideActionsURL(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791, RepoID: repo.ID})
|
||||
assert.NoError(t, run.LoadAttributes(db.DefaultContext))
|
||||
|
||||
statuses := []*git_model.CommitStatus{
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.Index),
|
||||
},
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
TargetURL: "https://mycicd.org/1",
|
||||
},
|
||||
}
|
||||
|
||||
git_model.CommitStatusesHideActionsURL(db.DefaultContext, statuses)
|
||||
assert.Empty(t, statuses[0].TargetURL)
|
||||
assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL)
|
||||
}
|
||||
|
||||
+38
-7
@@ -6,6 +6,7 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,11 +22,12 @@ import (
|
||||
|
||||
// LFSLock represents a git lfs lock of repository.
|
||||
type LFSLock struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Path string `xorm:"TEXT"`
|
||||
Created time.Time `xorm:"created"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Owner *user_model.User `xorm:"-"`
|
||||
Path string `xorm:"TEXT"`
|
||||
Created time.Time `xorm:"created"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -37,6 +39,35 @@ func (l *LFSLock) BeforeInsert() {
|
||||
l.Path = util.PathJoinRel(l.Path)
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the lock.
|
||||
func (l *LFSLock) LoadAttributes(ctx context.Context) error {
|
||||
// Load owner
|
||||
if err := l.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("load owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOwner loads owner of the lock.
|
||||
func (l *LFSLock) LoadOwner(ctx context.Context) error {
|
||||
if l.Owner != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owner, err := user_model.GetUserByID(ctx, l.OwnerID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
l.Owner = user_model.NewGhostUser()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
l.Owner = owner
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateLFSLock creates a new lock.
|
||||
func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) {
|
||||
dbCtx, committer, err := db.TxContext(ctx)
|
||||
@@ -94,7 +125,7 @@ func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
|
||||
}
|
||||
|
||||
// GetLFSLockByRepoID returns a list of locks of repository.
|
||||
func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) ([]*LFSLock, error) {
|
||||
func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) (LFSLockList, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
if page >= 0 && pageSize > 0 {
|
||||
start := 0
|
||||
@@ -103,7 +134,7 @@ func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) (
|
||||
}
|
||||
e.Limit(pageSize, start)
|
||||
}
|
||||
lfsLocks := make([]*LFSLock, 0, pageSize)
|
||||
lfsLocks := make(LFSLockList, 0, pageSize)
|
||||
return lfsLocks, e.Find(&lfsLocks, &LFSLock{RepoID: repoID})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
)
|
||||
|
||||
// LFSLockList is a list of LFSLock
|
||||
type LFSLockList []*LFSLock
|
||||
|
||||
// LoadAttributes loads the attributes for the given locks
|
||||
func (locks LFSLockList) LoadAttributes(ctx context.Context) error {
|
||||
if len(locks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := locks.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("load owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOwner loads the owner of the locks
|
||||
func (locks LFSLockList) LoadOwner(ctx context.Context) error {
|
||||
if len(locks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
usersIDs := container.FilterSlice(locks, func(lock *LFSLock) (int64, bool) {
|
||||
return lock.OwnerID, true
|
||||
})
|
||||
users := make(map[int64]*user_model.User, len(usersIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", usersIDs).
|
||||
Find(&users); err != nil {
|
||||
return fmt.Errorf("find users: %w", err)
|
||||
}
|
||||
for _, v := range locks {
|
||||
v.Owner = users[v.OwnerID]
|
||||
if v.Owner == nil { // not exist
|
||||
v.Owner = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -44,6 +44,11 @@ type ProtectedBranch struct {
|
||||
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
@@ -143,6 +148,33 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
|
||||
return in
|
||||
}
|
||||
|
||||
// CanUserForcePush returns if some user could force push to this protected branch
|
||||
// Since force-push extends normal push, we also check if user has regular push access
|
||||
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool {
|
||||
if !protectBranch.CanForcePush {
|
||||
return false
|
||||
}
|
||||
|
||||
if !protectBranch.EnableForcePushAllowlist {
|
||||
return protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
||||
return protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ForcePushAllowlistTeamIDs)
|
||||
if err != nil {
|
||||
log.Error("IsUserInTeams: %v", err)
|
||||
return false
|
||||
}
|
||||
return in && protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
||||
func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool {
|
||||
if !protectBranch.EnableMergeWhitelist {
|
||||
@@ -301,6 +333,9 @@ type WhitelistOptions struct {
|
||||
UserIDs []int64
|
||||
TeamIDs []int64
|
||||
|
||||
ForcePushUserIDs []int64
|
||||
ForcePushTeamIDs []int64
|
||||
|
||||
MergeUserIDs []int64
|
||||
MergeTeamIDs []int64
|
||||
|
||||
@@ -328,6 +363,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
|
||||
}
|
||||
protectBranch.WhitelistUserIDs = whitelist
|
||||
|
||||
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.ForcePushAllowlistUserIDs, opts.ForcePushUserIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protectBranch.ForcePushAllowlistUserIDs = whitelist
|
||||
|
||||
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -347,6 +388,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
|
||||
}
|
||||
protectBranch.WhitelistTeamIDs = whitelist
|
||||
|
||||
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ForcePushAllowlistTeamIDs, opts.ForcePushTeamIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protectBranch.ForcePushAllowlistTeamIDs = whitelist
|
||||
|
||||
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -468,43 +515,58 @@ func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserIDFromProtectedBranch remove all user ids from protected branch options
|
||||
// removeIDsFromProtectedBranch is a helper function to remove IDs from protected branch options
|
||||
func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID, teamID int64, columnNames []string) error {
|
||||
lenUserIDs, lenForcePushIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ForcePushAllowlistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
|
||||
lenTeamIDs, lenForcePushTeamIDs, lenApprovalTeamIDs, lenMergeTeamIDs := len(p.WhitelistTeamIDs), len(p.ForcePushAllowlistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
|
||||
|
||||
if userID > 0 {
|
||||
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
|
||||
p.ForcePushAllowlistUserIDs = util.SliceRemoveAll(p.ForcePushAllowlistUserIDs, userID)
|
||||
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
|
||||
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
|
||||
}
|
||||
|
||||
if teamID > 0 {
|
||||
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
|
||||
p.ForcePushAllowlistTeamIDs = util.SliceRemoveAll(p.ForcePushAllowlistTeamIDs, teamID)
|
||||
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
|
||||
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
|
||||
}
|
||||
|
||||
if (lenUserIDs != len(p.WhitelistUserIDs) ||
|
||||
lenForcePushIDs != len(p.ForcePushAllowlistUserIDs) ||
|
||||
lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
|
||||
lenMergeIDs != len(p.MergeWhitelistUserIDs)) ||
|
||||
(lenTeamIDs != len(p.WhitelistTeamIDs) ||
|
||||
lenForcePushTeamIDs != len(p.ForcePushAllowlistTeamIDs) ||
|
||||
lenApprovalTeamIDs != len(p.ApprovalsWhitelistTeamIDs) ||
|
||||
lenMergeTeamIDs != len(p.MergeWhitelistTeamIDs)) {
|
||||
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(columnNames...).Update(p); err != nil {
|
||||
return fmt.Errorf("updateProtectedBranches: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserIDFromProtectedBranch removes all user ids from protected branch options
|
||||
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
|
||||
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
|
||||
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
|
||||
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
|
||||
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
|
||||
|
||||
if lenIDs != len(p.WhitelistUserIDs) || lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
|
||||
lenMergeIDs != len(p.MergeWhitelistUserIDs) {
|
||||
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||
"whitelist_user_i_ds",
|
||||
"merge_whitelist_user_i_ds",
|
||||
"approvals_whitelist_user_i_ds",
|
||||
).Update(p); err != nil {
|
||||
return fmt.Errorf("updateProtectedBranches: %v", err)
|
||||
}
|
||||
columnNames := []string{
|
||||
"whitelist_user_i_ds",
|
||||
"force_push_allowlist_user_i_ds",
|
||||
"merge_whitelist_user_i_ds",
|
||||
"approvals_whitelist_user_i_ds",
|
||||
}
|
||||
return nil
|
||||
return removeIDsFromProtectedBranch(ctx, p, userID, 0, columnNames)
|
||||
}
|
||||
|
||||
// RemoveTeamIDFromProtectedBranch remove all team ids from protected branch options
|
||||
// RemoveTeamIDFromProtectedBranch removes all team ids from protected branch options
|
||||
func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, teamID int64) error {
|
||||
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
|
||||
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
|
||||
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
|
||||
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
|
||||
|
||||
if lenIDs != len(p.WhitelistTeamIDs) ||
|
||||
lenApprovalIDs != len(p.ApprovalsWhitelistTeamIDs) ||
|
||||
lenMergeIDs != len(p.MergeWhitelistTeamIDs) {
|
||||
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||
"whitelist_team_i_ds",
|
||||
"merge_whitelist_team_i_ds",
|
||||
"approvals_whitelist_team_i_ds",
|
||||
).Update(p); err != nil {
|
||||
return fmt.Errorf("updateProtectedBranches: %v", err)
|
||||
}
|
||||
columnNames := []string{
|
||||
"whitelist_team_i_ds",
|
||||
"force_push_allowlist_team_i_ds",
|
||||
"merge_whitelist_team_i_ds",
|
||||
"approvals_whitelist_team_i_ds",
|
||||
}
|
||||
return nil
|
||||
return removeIDsFromProtectedBranch(ctx, p, 0, teamID, columnNames)
|
||||
}
|
||||
|
||||
+50
-28
@@ -222,6 +222,13 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
|
||||
return lang.TrString("repo.issues.role." + string(r) + "_helper")
|
||||
}
|
||||
|
||||
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
|
||||
type CommentMetaData struct {
|
||||
ProjectColumnID int64 `json:"project_column_id,omitempty"`
|
||||
ProjectColumnTitle string `json:"project_column_title,omitempty"`
|
||||
ProjectTitle string `json:"project_title,omitempty"`
|
||||
}
|
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
type Comment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
@@ -295,6 +302,8 @@ type Comment struct {
|
||||
RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves
|
||||
RefIsPull bool
|
||||
|
||||
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
|
||||
|
||||
RefRepo *repo_model.Repository `xorm:"-"`
|
||||
RefIssue *Issue `xorm:"-"`
|
||||
RefComment *Comment `xorm:"-"`
|
||||
@@ -797,6 +806,15 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
|
||||
LabelID = opts.Label.ID
|
||||
}
|
||||
|
||||
var commentMetaData *CommentMetaData
|
||||
if opts.ProjectColumnTitle != "" {
|
||||
commentMetaData = &CommentMetaData{
|
||||
ProjectColumnID: opts.ProjectColumnID,
|
||||
ProjectColumnTitle: opts.ProjectColumnTitle,
|
||||
ProjectTitle: opts.ProjectTitle,
|
||||
}
|
||||
}
|
||||
|
||||
comment := &Comment{
|
||||
Type: opts.Type,
|
||||
PosterID: opts.Doer.ID,
|
||||
@@ -830,6 +848,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
|
||||
RefIsPull: opts.RefIsPull,
|
||||
IsForcePush: opts.IsForcePush,
|
||||
Invalidated: opts.Invalidated,
|
||||
CommentMetaData: commentMetaData,
|
||||
}
|
||||
if _, err = e.Insert(comment); err != nil {
|
||||
return nil, err
|
||||
@@ -982,34 +1001,37 @@ type CreateCommentOptions struct {
|
||||
Issue *Issue
|
||||
Label *Label
|
||||
|
||||
DependentIssueID int64
|
||||
OldMilestoneID int64
|
||||
MilestoneID int64
|
||||
OldProjectID int64
|
||||
ProjectID int64
|
||||
TimeID int64
|
||||
AssigneeID int64
|
||||
AssigneeTeamID int64
|
||||
RemovedAssignee bool
|
||||
OldTitle string
|
||||
NewTitle string
|
||||
OldRef string
|
||||
NewRef string
|
||||
CommitID int64
|
||||
CommitSHA string
|
||||
Patch string
|
||||
LineNum int64
|
||||
TreePath string
|
||||
ReviewID int64
|
||||
Content string
|
||||
Attachments []string // UUIDs of attachments
|
||||
RefRepoID int64
|
||||
RefIssueID int64
|
||||
RefCommentID int64
|
||||
RefAction references.XRefAction
|
||||
RefIsPull bool
|
||||
IsForcePush bool
|
||||
Invalidated bool
|
||||
DependentIssueID int64
|
||||
OldMilestoneID int64
|
||||
MilestoneID int64
|
||||
OldProjectID int64
|
||||
ProjectID int64
|
||||
ProjectTitle string
|
||||
ProjectColumnID int64
|
||||
ProjectColumnTitle string
|
||||
TimeID int64
|
||||
AssigneeID int64
|
||||
AssigneeTeamID int64
|
||||
RemovedAssignee bool
|
||||
OldTitle string
|
||||
NewTitle string
|
||||
OldRef string
|
||||
NewRef string
|
||||
CommitID int64
|
||||
CommitSHA string
|
||||
Patch string
|
||||
LineNum int64
|
||||
TreePath string
|
||||
ReviewID int64
|
||||
Content string
|
||||
Attachments []string // UUIDs of attachments
|
||||
RefRepoID int64
|
||||
RefIssueID int64
|
||||
RefCommentID int64
|
||||
RefAction references.XRefAction
|
||||
RefIsPull bool
|
||||
IsForcePush bool
|
||||
Invalidated bool
|
||||
}
|
||||
|
||||
// GetCommentByID returns the comment by given ID.
|
||||
|
||||
@@ -441,6 +441,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er
|
||||
Join("INNER", "issue", "issue.id = comment.issue_id").
|
||||
In("issue.id", issuesIDs[:limit]).
|
||||
Where(cond).
|
||||
NoAutoCondition().
|
||||
Rows(new(Comment))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,6 +6,7 @@ package issues
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
|
||||
"xorm.io/builder"
|
||||
@@ -116,14 +118,30 @@ func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if opts.LabelIDs[0] == 0 {
|
||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)")
|
||||
} else {
|
||||
for i, labelID := range opts.LabelIDs {
|
||||
// deduplicate the label IDs for inclusion and exclusion
|
||||
includedLabelIDs := make(container.Set[int64])
|
||||
excludedLabelIDs := make(container.Set[int64])
|
||||
for _, labelID := range opts.LabelIDs {
|
||||
if labelID > 0 {
|
||||
sess.Join("INNER", fmt.Sprintf("issue_label il%d", i),
|
||||
fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID))
|
||||
includedLabelIDs.Add(labelID)
|
||||
} else if labelID < 0 { // 0 is not supported here, so just ignore it
|
||||
sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID)
|
||||
excludedLabelIDs.Add(-labelID)
|
||||
}
|
||||
}
|
||||
// ... and use them in a subquery of the form :
|
||||
// where (select count(*) from issue_label where issue_id=issue.id and label_id in (2, 4, 6)) = 3
|
||||
// This equality is guaranteed thanks to unique index (issue_id,label_id) on table issue_label.
|
||||
if len(includedLabelIDs) > 0 {
|
||||
subQuery := builder.Select("count(*)").From("issue_label").Where(builder.Expr("issue_id = issue.id")).
|
||||
And(builder.In("label_id", includedLabelIDs.Values()))
|
||||
sess.Where(builder.Eq{strconv.Itoa(len(includedLabelIDs)): subQuery})
|
||||
}
|
||||
// or (select count(*)...) = 0 for excluded labels
|
||||
if len(excludedLabelIDs) > 0 {
|
||||
subQuery := builder.Select("count(*)").From("issue_label").Where(builder.Expr("issue_id = issue.id")).
|
||||
And(builder.In("label_id", excludedLabelIDs.Values()))
|
||||
sess.Where(builder.Eq{"0": subQuery})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-12
@@ -7,10 +7,12 @@ package issues
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/label"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -142,28 +144,33 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
|
||||
|
||||
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
|
||||
func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) {
|
||||
var labelQuerySlice []string
|
||||
labelQueryParams := container.Set[string]{}
|
||||
labelSelected := false
|
||||
labelID := strconv.FormatInt(l.ID, 10)
|
||||
labelScope := l.ExclusiveScope()
|
||||
for i, s := range currentSelectedLabels {
|
||||
if s == l.ID {
|
||||
exclusiveScope := l.ExclusiveScope()
|
||||
for i, curSel := range currentSelectedLabels {
|
||||
if curSel == l.ID {
|
||||
labelSelected = true
|
||||
} else if -s == l.ID {
|
||||
} else if -curSel == l.ID {
|
||||
labelSelected = true
|
||||
l.IsExcluded = true
|
||||
} else if s != 0 {
|
||||
} else if curSel != 0 {
|
||||
// Exclude other labels in the same scope from selection
|
||||
if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
|
||||
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
|
||||
if curSel < 0 || exclusiveScope == "" || exclusiveScope != currentSelectedExclusiveScopes[i] {
|
||||
labelQueryParams.Add(strconv.FormatInt(curSel, 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !labelSelected {
|
||||
labelQuerySlice = append(labelQuerySlice, labelID)
|
||||
labelQueryParams.Add(strconv.FormatInt(l.ID, 10))
|
||||
}
|
||||
l.IsSelected = labelSelected
|
||||
l.QueryString = strings.Join(labelQuerySlice, ",")
|
||||
|
||||
// Sort and deduplicate the ids to avoid the crawlers asking for the
|
||||
// same thing with simply a different order of parameters
|
||||
labelQuerySliceStrings := labelQueryParams.Values()
|
||||
slices.Sort(labelQuerySliceStrings) // the sort is still needed because the underlying map of Set doesn't guarantee order
|
||||
l.QueryString = strings.Join(labelQuerySliceStrings, ",")
|
||||
}
|
||||
|
||||
// BelongsToOrg returns true if label is an organization label
|
||||
@@ -176,7 +183,7 @@ func (l *Label) BelongsToRepo() bool {
|
||||
return l.RepoID > 0
|
||||
}
|
||||
|
||||
// Return scope substring of label name, or empty string if none exists
|
||||
// ExclusiveScope returns scope substring of label name, or empty string if none exists
|
||||
func (l *Label) ExclusiveScope() string {
|
||||
if !l.Exclusive {
|
||||
return ""
|
||||
|
||||
@@ -23,6 +23,27 @@ func TestLabel_CalOpenIssues(t *testing.T) {
|
||||
assert.EqualValues(t, 2, label.NumOpenIssues)
|
||||
}
|
||||
|
||||
func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
// Loading the label id:8 which have a scope and an exclusivity
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 8})
|
||||
|
||||
// First test : with negative and scope
|
||||
label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"})
|
||||
assert.Equal(t, "1", label.QueryString)
|
||||
assert.Equal(t, true, label.IsSelected)
|
||||
|
||||
// Second test : with duplicates
|
||||
label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"})
|
||||
assert.Equal(t, "1,8", label.QueryString)
|
||||
assert.Equal(t, false, label.IsSelected)
|
||||
|
||||
// Third test : empty set
|
||||
label.LoadSelectedLabelsAfterClick([]int64{}, []string{})
|
||||
assert.False(t, label.IsSelected)
|
||||
assert.Equal(t, "8", label.QueryString)
|
||||
}
|
||||
|
||||
func TestLabel_ExclusiveScope(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
|
||||
|
||||
@@ -70,8 +70,10 @@ func (opts FindMilestoneOptions) ToOrders() string {
|
||||
return "num_issues DESC"
|
||||
case "id":
|
||||
return "id ASC"
|
||||
case "name":
|
||||
return "name DESC"
|
||||
default:
|
||||
return "deadline_unix ASC, id ASC"
|
||||
return "deadline_unix ASC, name ASC"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-1
@@ -27,6 +27,8 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var ErrMustCollaborator = util.NewPermissionDeniedErrorf("user must be a collaborator")
|
||||
|
||||
// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
|
||||
type ErrPullRequestNotExist struct {
|
||||
ID int64
|
||||
@@ -163,6 +165,7 @@ type PullRequest struct {
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
RequestedReviewersTeams []*org_model.Team `xorm:"-"`
|
||||
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||
|
||||
HeadRepoID int64 `xorm:"INDEX"`
|
||||
@@ -303,7 +306,28 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
}
|
||||
pr.isRequestedReviewersLoaded = true
|
||||
for _, review := range reviews {
|
||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||
if review.ReviewerID != 0 {
|
||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadRequestedReviewersTeams loads the requested reviewers teams.
|
||||
func (pr *PullRequest) LoadRequestedReviewersTeams(ctx context.Context) error {
|
||||
reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = reviews.LoadReviewersTeams(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, review := range reviews {
|
||||
if review.ReviewerTeamID != 0 {
|
||||
pr.RequestedReviewersTeams = append(pr.RequestedReviewersTeams, review.ReviewerTeam)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -572,6 +596,12 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrUserMustCollaborator represents an error that the user must be a collaborator to a given repo.
|
||||
type ErrUserMustCollaborator struct {
|
||||
UserID int64
|
||||
RepoName string
|
||||
}
|
||||
|
||||
// GetUnmergedPullRequest returns a pull request that is open and has not been merged
|
||||
// by given head/base and repo/branch.
|
||||
func GetUnmergedPullRequest(ctx context.Context, headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) {
|
||||
|
||||
@@ -214,9 +214,13 @@ func (r *Review) LoadAttributes(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// HTMLTypeColorName returns the color used in the ui indicating the review
|
||||
func (r *Review) HTMLTypeColorName() string {
|
||||
switch r.Type {
|
||||
case ReviewTypeApprove:
|
||||
if !r.Official {
|
||||
return "grey"
|
||||
}
|
||||
if r.Stale {
|
||||
return "yellow"
|
||||
}
|
||||
@@ -231,6 +235,27 @@ func (r *Review) HTMLTypeColorName() string {
|
||||
return "grey"
|
||||
}
|
||||
|
||||
// TooltipContent returns the locale string describing the review type
|
||||
func (r *Review) TooltipContent() string {
|
||||
switch r.Type {
|
||||
case ReviewTypeApprove:
|
||||
if r.Stale {
|
||||
return "repo.issues.review.stale"
|
||||
}
|
||||
if !r.Official {
|
||||
return "repo.issues.review.unofficial"
|
||||
}
|
||||
return "repo.issues.review.official"
|
||||
case ReviewTypeComment:
|
||||
return "repo.issues.review.comment"
|
||||
case ReviewTypeReject:
|
||||
return "repo.issues.review.rejected"
|
||||
case ReviewTypeRequest:
|
||||
return "repo.issues.review.requested"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetReviewByID returns the review by the given ID
|
||||
func GetReviewByID(ctx context.Context, id int64) (*Review, error) {
|
||||
review := new(Review)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
organization_model "code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -37,6 +38,34 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadReviewersTeams loads reviewers teams
|
||||
func (reviews ReviewList) LoadReviewersTeams(ctx context.Context) error {
|
||||
reviewersTeamsIDs := make([]int64, 0)
|
||||
for _, review := range reviews {
|
||||
if review.ReviewerTeamID != 0 {
|
||||
reviewersTeamsIDs = append(reviewersTeamsIDs, review.ReviewerTeamID)
|
||||
}
|
||||
}
|
||||
|
||||
teamsMap := make(map[int64]*organization_model.Team, 0)
|
||||
for _, teamID := range reviewersTeamsIDs {
|
||||
team, err := organization_model.GetTeamByID(ctx, teamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
teamsMap[teamID] = team
|
||||
}
|
||||
|
||||
for _, review := range reviews {
|
||||
if review.ReviewerTeamID != 0 {
|
||||
review.ReviewerTeam = teamsMap[review.ReviewerTeamID]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (reviews ReviewList) LoadIssues(ctx context.Context) error {
|
||||
issueIDs := container.FilterSlice(reviews, func(review *Review) (int64, bool) {
|
||||
return review.IssueID, true
|
||||
|
||||
@@ -591,6 +591,16 @@ var migrations = []Migration{
|
||||
|
||||
// v299 -> v300
|
||||
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
|
||||
// v300 -> v301
|
||||
NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection),
|
||||
// v301 -> v302
|
||||
NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable),
|
||||
// v302 -> v303
|
||||
NewMigration("Add index to action_task stopped log_expired", v1_23.AddIndexToActionTaskStoppedLogExpired),
|
||||
// v303 -> v304
|
||||
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
|
||||
// v304 -> v305
|
||||
NewMigration("Add index for release sha1", v1_23.AddIndexForReleaseSha1),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
||||
@@ -12,5 +12,9 @@ func AddIndexToActionUserID(x *xorm.Engine) error {
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Action))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(Action))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,5 +10,9 @@ func AddIgnoreStaleApprovalsColumnToProtectedBranchTable(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(ProtectedBranch))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,5 +14,9 @@ func AddPreviousDurationToActionRun(x *xorm.Engine) error {
|
||||
PreviousDuration time.Duration
|
||||
}
|
||||
|
||||
return x.Sync(&ActionRun{})
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, &ActionRun{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
||||
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
|
||||
}
|
||||
|
||||
if err := x.Sync(new(Repository)); err != nil {
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(Repository)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ func AddDefaultWikiBranch(x *xorm.Engine) error {
|
||||
ID int64
|
||||
DefaultWikiBranch string
|
||||
}
|
||||
if err := x.Sync(&Repository{}); err != nil {
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, &Repository{}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
|
||||
|
||||
@@ -13,5 +13,12 @@ type HookTask struct {
|
||||
|
||||
func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
|
||||
// create missing column
|
||||
return x.Sync(new(HookTask))
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(HookTask)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := x.Exec("UPDATE hook_task SET payload_version = 1 WHERE payload_version IS NULL")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,5 +10,9 @@ func AddCommentIDIndexofAttachment(x *xorm.Engine) error {
|
||||
CommentID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
return x.Sync(&Attachment{})
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, &Attachment{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddForcePushBranchProtection(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddSkipSeconderyAuthToOAuth2ApplicationTable: add SkipSecondaryAuthorization column, setting existing rows to false
|
||||
func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
|
||||
type oauth2Application struct {
|
||||
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
}
|
||||
return x.Sync(new(oauth2Application))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
|
||||
type ActionTask struct {
|
||||
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||
LogExpired bool `xorm:"index(stopped_log_expired)"`
|
||||
}
|
||||
return x.Sync(new(ActionTask))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
|
||||
type CommentMetaData struct {
|
||||
ProjectColumnID int64 `json:"project_column_id"`
|
||||
ProjectColumnTitle string `json:"project_column_title"`
|
||||
ProjectTitle string `json:"project_title"`
|
||||
}
|
||||
|
||||
func AddCommentMetaDataColumn(x *xorm.Engine) error {
|
||||
type Comment struct {
|
||||
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
|
||||
}
|
||||
|
||||
return x.Sync(new(Comment))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddIndexForReleaseSha1(x *xorm.Engine) error {
|
||||
type Release struct {
|
||||
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
||||
}
|
||||
return x.Sync(new(Release))
|
||||
}
|
||||
+6
-24
@@ -76,30 +76,6 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
|
||||
return int(c)
|
||||
}
|
||||
|
||||
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
issueIDs := util.ValuesOfMap(sortedIssueIDs)
|
||||
|
||||
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int(count) != len(sortedIssueIDs) {
|
||||
return fmt.Errorf("all issues have to be added to a project first")
|
||||
}
|
||||
|
||||
for sorting, issueID := range sortedIssueIDs {
|
||||
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -141,3 +117,9 @@ func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Colum
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAllProjectIssueByIssueIDsAndProjectIDs delete all project's issues by issue's and project's ids
|
||||
func DeleteAllProjectIssueByIssueIDsAndProjectIDs(ctx context.Context, issueIDs, projectIDs []int64) error {
|
||||
_, err := db.GetEngine(ctx).In("project_id", projectIDs).In("issue_id", issueIDs).Delete(&ProjectIssue{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -103,6 +103,13 @@ type Project struct {
|
||||
ClosedDateUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
// Ghost Project is a project which has been deleted
|
||||
const GhostProjectID = -1
|
||||
|
||||
func (p *Project) IsGhost() bool {
|
||||
return p.ID == GhostProjectID
|
||||
}
|
||||
|
||||
func (p *Project) LoadOwner(ctx context.Context) (err error) {
|
||||
if p.Owner != nil {
|
||||
return nil
|
||||
@@ -289,6 +296,12 @@ func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, err
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetAllProjectsIDsByOwnerID returns the all projects ids it owns
|
||||
func GetAllProjectsIDsByOwnerIDAndType(ctx context.Context, ownerID int64, projectType Type) ([]int64, error) {
|
||||
projects := make([]int64, 0)
|
||||
return projects, db.GetEngine(ctx).Table(&Project{}).Where("owner_id=? AND type=?", ownerID, projectType).Cols("id").Find(&projects)
|
||||
}
|
||||
|
||||
// UpdateProject updates project properties
|
||||
func UpdateProject(ctx context.Context, p *Project) error {
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
|
||||
+15
-27
@@ -77,7 +77,7 @@ type Release struct {
|
||||
Target string
|
||||
TargetBehind string `xorm:"-"` // to handle non-existing or empty target
|
||||
Title string
|
||||
Sha1 string `xorm:"VARCHAR(64)"`
|
||||
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
||||
NumCommits int64
|
||||
NumCommitsBehind int64 `xorm:"-"`
|
||||
Note string `xorm:"TEXT"`
|
||||
@@ -398,32 +398,6 @@ func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
type releaseSorter struct {
|
||||
rels []*Release
|
||||
}
|
||||
|
||||
func (rs *releaseSorter) Len() int {
|
||||
return len(rs.rels)
|
||||
}
|
||||
|
||||
func (rs *releaseSorter) Less(i, j int) bool {
|
||||
diffNum := rs.rels[i].NumCommits - rs.rels[j].NumCommits
|
||||
if diffNum != 0 {
|
||||
return diffNum > 0
|
||||
}
|
||||
return rs.rels[i].CreatedUnix > rs.rels[j].CreatedUnix
|
||||
}
|
||||
|
||||
func (rs *releaseSorter) Swap(i, j int) {
|
||||
rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i]
|
||||
}
|
||||
|
||||
// SortReleases sorts releases by number of commits and created time.
|
||||
func SortReleases(rels []*Release) {
|
||||
sorter := &releaseSorter{rels: rels}
|
||||
sort.Sort(sorter)
|
||||
}
|
||||
|
||||
// UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID
|
||||
func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error {
|
||||
_, err := db.GetEngine(ctx).Table("release").
|
||||
@@ -563,3 +537,17 @@ func InsertReleases(ctx context.Context, rels ...*Release) error {
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string) (map[string][]*Release, error) {
|
||||
releases := make([]*Release, 0, len(commitIDs))
|
||||
if err := db.GetEngine(ctx).Where("repo_id=?", repoID).
|
||||
In("sha1", commitIDs).
|
||||
Find(&releases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[string][]*Release, len(releases))
|
||||
for _, r := range releases {
|
||||
res[r.Sha1] = append(res[r.Sha1], r)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -25,3 +25,16 @@ func TestMigrate_InsertReleases(t *testing.T) {
|
||||
err := InsertReleases(db.DefaultContext, r)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_FindTagsByCommitIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
sha1Rels, err := FindTagsByCommitIDs(db.DefaultContext, 1, "65f1bf27bc3bf70f64657658635e66094edbcb4d")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, sha1Rels, 1)
|
||||
rels := sha1Rels["65f1bf27bc3bf70f64657658635e66094edbcb4d"]
|
||||
assert.Len(t, rels, 3)
|
||||
assert.Equal(t, "v1.1", rels[0].TagName)
|
||||
assert.Equal(t, "delete-tag", rels[1].TagName)
|
||||
assert.Equal(t, "v1.0", rels[2].TagName)
|
||||
}
|
||||
|
||||
+14
-8
@@ -18,6 +18,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -321,8 +322,12 @@ func (repo *Repository) FullName() string {
|
||||
}
|
||||
|
||||
// HTMLURL returns the repository HTML URL
|
||||
func (repo *Repository) HTMLURL() string {
|
||||
return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
|
||||
func (repo *Repository) HTMLURL(ctxs ...context.Context) string {
|
||||
ctx := context.TODO()
|
||||
if len(ctxs) > 0 {
|
||||
ctx = ctxs[0]
|
||||
}
|
||||
return httplib.MakeAbsoluteURL(ctx, repo.Link())
|
||||
}
|
||||
|
||||
// CommitLink make link to by commit full ID
|
||||
@@ -740,17 +745,18 @@ func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string
|
||||
|
||||
// GetRepositoryByName returns the repository by given name under user if exists.
|
||||
func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
|
||||
repo := &Repository{
|
||||
OwnerID: ownerID,
|
||||
LowerName: strings.ToLower(name),
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(repo)
|
||||
var repo Repository
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("`owner_id`=?", ownerID).
|
||||
And("`lower_name`=?", strings.ToLower(name)).
|
||||
NoAutoCondition().
|
||||
Get(&repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrRepoNotExist{0, ownerID, "", name}
|
||||
}
|
||||
return repo, err
|
||||
return &repo, err
|
||||
}
|
||||
|
||||
// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
|
||||
|
||||
+30
-16
@@ -5,7 +5,6 @@ package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -22,6 +21,19 @@ import (
|
||||
)
|
||||
|
||||
// Secret represents a secret
|
||||
//
|
||||
// It can be:
|
||||
// 1. org/user level secret, OwnerID is org/user ID and RepoID is 0
|
||||
// 2. repo level secret, OwnerID is 0 and RepoID is repo ID
|
||||
//
|
||||
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
|
||||
// or it will be complicated to find secrets belonging to a specific owner.
|
||||
// For example, conditions like `OwnerID = 1` will also return secret {OwnerID: 1, RepoID: 1},
|
||||
// but it's a repo level secret, not an org/user level secret.
|
||||
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level secrets.
|
||||
//
|
||||
// Please note that it's not acceptable to have both OwnerID and RepoID to zero, global secrets are not supported.
|
||||
// It's for security reasons, admin may be not aware of that the secrets could be stolen by any user when setting them as global.
|
||||
type Secret struct {
|
||||
ID int64
|
||||
OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"`
|
||||
@@ -46,6 +58,15 @@ func (err ErrSecretNotFound) Unwrap() error {
|
||||
|
||||
// InsertEncryptedSecret Creates, encrypts, and validates a new secret with yet unencrypted data and insert into database
|
||||
func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*Secret, error) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
// It's trying to create a secret that belongs to a repository, but OwnerID has been set accidentally.
|
||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||
ownerID = 0
|
||||
}
|
||||
if ownerID == 0 && repoID == 0 {
|
||||
return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -56,9 +77,6 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
|
||||
Name: strings.ToUpper(name),
|
||||
Data: encrypted,
|
||||
}
|
||||
if err := secret.Validate(); err != nil {
|
||||
return secret, err
|
||||
}
|
||||
return secret, db.Insert(ctx, secret)
|
||||
}
|
||||
|
||||
@@ -66,29 +84,25 @@ func init() {
|
||||
db.RegisterModel(new(Secret))
|
||||
}
|
||||
|
||||
func (s *Secret) Validate() error {
|
||||
if s.OwnerID == 0 && s.RepoID == 0 {
|
||||
return errors.New("the secret is not bound to any scope")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindSecretsOptions struct {
|
||||
db.ListOptions
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
OwnerID int64 // it will be ignored if RepoID is set
|
||||
SecretID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
func (opts FindSecretsOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.OwnerID > 0 {
|
||||
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
if opts.RepoID != 0 { // if RepoID is set
|
||||
// ignore OwnerID and treat it as 0
|
||||
cond = cond.And(builder.Eq{"owner_id": 0})
|
||||
} else {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
|
||||
if opts.SecretID != 0 {
|
||||
cond = cond.And(builder.Eq{"id": opts.SecretID})
|
||||
}
|
||||
|
||||
@@ -395,6 +395,7 @@ type SearchEmailOptions struct {
|
||||
|
||||
// SearchEmailResult is an e-mail address found in the user or email_address table
|
||||
type SearchEmailResult struct {
|
||||
ID int64
|
||||
UID int64
|
||||
Email string
|
||||
IsActivated bool
|
||||
|
||||
@@ -160,12 +160,34 @@ func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLogin
|
||||
return err
|
||||
}
|
||||
|
||||
// EnsureLinkExternalToUser link the external user to the user
|
||||
func EnsureLinkExternalToUser(ctx context.Context, external *ExternalLoginUser) error {
|
||||
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
|
||||
"external_id": external.ExternalID,
|
||||
"login_source_id": external.LoginSourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
_, err = db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).Insert(external)
|
||||
return err
|
||||
}
|
||||
|
||||
// FindExternalUserOptions represents an options to find external users
|
||||
type FindExternalUserOptions struct {
|
||||
db.ListOptions
|
||||
Provider string
|
||||
UserID int64
|
||||
OrderBy string
|
||||
Provider string
|
||||
UserID int64
|
||||
LoginSourceID int64
|
||||
HasRefreshToken bool
|
||||
Expired bool
|
||||
OrderBy string
|
||||
}
|
||||
|
||||
func (opts FindExternalUserOptions) ToConds() builder.Cond {
|
||||
@@ -176,9 +198,22 @@ func (opts FindExternalUserOptions) ToConds() builder.Cond {
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{"user_id": opts.UserID})
|
||||
}
|
||||
if opts.Expired {
|
||||
cond = cond.And(builder.Lt{"expires_at": time.Now()})
|
||||
}
|
||||
if opts.HasRefreshToken {
|
||||
cond = cond.And(builder.Neq{"refresh_token": ""})
|
||||
}
|
||||
if opts.LoginSourceID != 0 {
|
||||
cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindExternalUserOptions) ToOrders() string {
|
||||
return opts.OrderBy
|
||||
}
|
||||
|
||||
func IterateExternalLogin(ctx context.Context, opts FindExternalUserOptions, f func(ctx context.Context, u *ExternalLoginUser) error) error {
|
||||
return db.Iterate(ctx, opts.ToConds(), f)
|
||||
}
|
||||
|
||||
+36
-4
@@ -8,6 +8,8 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -413,6 +415,34 @@ func (u *User) DisplayName() string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
var emailToReplacer = strings.NewReplacer(
|
||||
"\n", "",
|
||||
"\r", "",
|
||||
"<", "",
|
||||
">", "",
|
||||
",", "",
|
||||
":", "",
|
||||
";", "",
|
||||
)
|
||||
|
||||
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
|
||||
func (u *User) EmailTo() string {
|
||||
sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
|
||||
|
||||
// should be an edge case but nice to have
|
||||
if sanitizedDisplayName == u.Email {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
to := fmt.Sprintf("%s <%s>", sanitizedDisplayName, u.Email)
|
||||
add, err := mail.ParseAddress(to)
|
||||
if err != nil {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
|
||||
}
|
||||
|
||||
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
|
||||
// returns username otherwise.
|
||||
func (u *User) GetDisplayName() string {
|
||||
@@ -1233,12 +1263,14 @@ func GetOrderByName() string {
|
||||
return "name"
|
||||
}
|
||||
|
||||
// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the
|
||||
// IsFeatureDisabledWithLoginType checks if a user features are disabled, taking into account the login type of the
|
||||
// user if applicable
|
||||
func IsFeatureDisabledWithLoginType(user *User, feature string) bool {
|
||||
func IsFeatureDisabledWithLoginType(user *User, features ...string) bool {
|
||||
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
|
||||
return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) ||
|
||||
setting.Admin.UserDisabledFeatures.Contains(feature)
|
||||
if user != nil && user.LoginType > auth.Plain {
|
||||
return setting.Admin.ExternalUserDisableFeatures.Contains(features...)
|
||||
}
|
||||
return setting.Admin.UserDisabledFeatures.Contains(features...)
|
||||
}
|
||||
|
||||
// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type
|
||||
|
||||
@@ -529,6 +529,29 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailTo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
fullName string
|
||||
mail string
|
||||
result string
|
||||
}{
|
||||
{"Awareness Hub", "awareness@hub.net", "Awareness Hub <awareness@hub.net>"},
|
||||
{"name@example.com", "name@example.com", "name@example.com"},
|
||||
{"Hi Its <Mee>", "ee@mail.box", "Hi Its Mee <ee@mail.box>"},
|
||||
{"Sinéad.O'Connor", "sinead.oconnor@gmail.com", "=?utf-8?q?Sin=C3=A9ad.O'Connor?= <sinead.oconnor@gmail.com>"},
|
||||
{"Æsir", "aesir@gmx.de", "=?utf-8?q?=C3=86sir?= <aesir@gmx.de>"},
|
||||
{"new😀user", "new.user@alo.com", "=?utf-8?q?new=F0=9F=98=80user?= <new.user@alo.com>"},
|
||||
{`"quoted"`, "quoted@test.com", "quoted <quoted@test.com>"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.result, func(t *testing.T) {
|
||||
testUser := &user_model.User{FullName: testCase.fullName, Email: testCase.mail}
|
||||
assert.EqualValues(t, testCase.result, testUser.EmailTo())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisabledUserFeatures(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user