mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-17 02:10:23 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de7026528b | ||
|
|
ee3f5e8fac | ||
|
|
b2707bcd18 | ||
|
|
0512b02b01 | ||
|
|
99545ae2fd | ||
|
|
7697df9f93 | ||
|
|
d17f8ffcc1 | ||
|
|
5e9cc919cf | ||
|
|
cc6ec56738 | ||
|
|
76bd60fc1d | ||
|
|
744f7c8200 | ||
|
|
da33b708af | ||
|
|
8fa3925874 | ||
|
|
7794ff0874 | ||
|
|
7c17d0a73e | ||
|
|
a014d071e4 | ||
|
|
312565e3c2 | ||
|
|
58daaf66e8 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -4,6 +4,34 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
|
||||
|
||||
* SECURITY
|
||||
* Enhance routers for the Actions variable operations (#33547) (#33553)
|
||||
* Enhance routers for the Actions runner operations (#33549) (#33555)
|
||||
* Fix project issues list and counting (#33594) #33619
|
||||
* PERFORMANCES
|
||||
* Performance optimization for pull request files loading comments attachments (#33585) (#33592)
|
||||
* BUGFIXES
|
||||
* Add a transaction to `pickTask` (#33543) (#33563)
|
||||
* Fix mirror bug (#33597) (#33607)
|
||||
* Use default Git timeout when checking repo health (#33593) (#33598)
|
||||
* Fix PR's target branch dropdown (#33589) (#33591)
|
||||
* Fix various problems (artifact order, api empty slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569) (#33577)
|
||||
* Rework suggestion backend (#33538) (#33546)
|
||||
* Fix context usage (#33554) (#33557)
|
||||
* Only show the latest version in the Arch index (#33262) (#33580)
|
||||
* Skip deletion error for action artifacts (#33476) (#33568)
|
||||
* Make actions URL in commit status webhooks absolute (#33620) #33632
|
||||
* Add missing locale (#33641) #33642
|
||||
|
||||
## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/v1.23.3) - 2025-02-06
|
||||
|
||||
* Security
|
||||
* Build Gitea with Golang v1.23.6 to fix security bugs
|
||||
* BUGFIXES
|
||||
* Fix a bug caused by status webhook template #33512
|
||||
|
||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
|
||||
|
||||
* BREAKING
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -49,6 +49,21 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
|
||||
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 result, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromColumn load issues assigned to this column
|
||||
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
|
||||
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||
@@ -61,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
|
||||
}
|
||||
@@ -79,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 {
|
||||
@@ -112,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -73,9 +73,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||
UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
|
||||
PriorityRepoID: 0,
|
||||
IsArchived: options.IsArchived,
|
||||
Org: nil,
|
||||
Owner: nil,
|
||||
Team: nil,
|
||||
User: nil,
|
||||
Doer: nil,
|
||||
}
|
||||
|
||||
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
|
||||
|
||||
@@ -71,3 +71,10 @@ func KeysOfMap[K comparable, V any](m map[K]V) []K {
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func SliceNilAsEmpty[T any](a []T) []T {
|
||||
if a == nil {
|
||||
return []T{}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -1690,6 +1690,8 @@ issues.start_tracking_history = started working %s
|
||||
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
|
||||
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
|
||||
issues.stop_tracking_history = worked for <b>%s</b> %s
|
||||
issues.stop_tracking = Stop Timer
|
||||
issues.cancel_tracking = Discard
|
||||
issues.cancel_tracking_history = `canceled time tracking %s`
|
||||
issues.del_time = Delete this time log
|
||||
issues.add_time_history = added spent time <b>%s</b> %s
|
||||
|
||||
@@ -156,7 +156,7 @@ func (s *Service) FetchTask(
|
||||
// if the task version in request is not equal to the version in db,
|
||||
// it means there may still be some tasks not be assgined.
|
||||
// try to pick a task for the runner that send the request.
|
||||
if t, ok, err := pickTask(ctx, runner); err != nil {
|
||||
if t, ok, err := actions_service.PickTask(ctx, runner); err != nil {
|
||||
log.Error("pick task failed: %v", err)
|
||||
return nil, status.Errorf(codes.Internal, "pick task: %v", err)
|
||||
} else if ok {
|
||||
|
||||
@@ -450,7 +450,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
|
||||
if opt.Name == "" {
|
||||
opt.Name = ctx.PathParam("variablename")
|
||||
}
|
||||
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
|
||||
|
||||
v.Name = opt.Name
|
||||
v.Data = opt.Value
|
||||
|
||||
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
|
||||
} else {
|
||||
|
||||
@@ -414,7 +414,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
|
||||
if opt.Name == "" {
|
||||
opt.Name = ctx.PathParam("variablename")
|
||||
}
|
||||
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
|
||||
|
||||
v.Name = opt.Name
|
||||
v.Data = opt.Value
|
||||
|
||||
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
|
||||
} else {
|
||||
|
||||
@@ -212,7 +212,11 @@ func UpdateVariable(ctx *context.APIContext) {
|
||||
if opt.Name == "" {
|
||||
opt.Name = ctx.PathParam("variablename")
|
||||
}
|
||||
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
|
||||
|
||||
v.Name = opt.Name
|
||||
v.Data = opt.Value
|
||||
|
||||
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,7 @@ func PrepareCodeSearch(ctx *context.Context) (ret struct {
|
||||
}
|
||||
isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault)
|
||||
if isFuzzy && !fuzzyAllow {
|
||||
ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons")
|
||||
ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons", true)
|
||||
isFuzzy = false
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ func Projects(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
|
||||
ctx.ServerError("LoadIssueNumbersForProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
IsClosed: optional.Some(!isShowClosed),
|
||||
@@ -328,6 +333,10 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.NotFound("", nil)
|
||||
return
|
||||
}
|
||||
if err := project.LoadOwner(ctx); err != nil {
|
||||
ctx.ServerError("LoadOwner", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
@@ -341,14 +350,21 @@ func ViewProject(ctx *context.Context) {
|
||||
}
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||
opts := issues_model.IssuesOptions{
|
||||
LabelIDs: labelIDs,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
})
|
||||
Owner: project.Owner,
|
||||
Doer: ctx.Doer,
|
||||
}
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
return
|
||||
}
|
||||
for _, column := range columns {
|
||||
column.NumIssues = int64(len(issuesMap[column.ID]))
|
||||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
|
||||
|
||||
@@ -276,13 +276,16 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
||||
}
|
||||
pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
|
||||
|
||||
// prepare assignees
|
||||
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
|
||||
inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
|
||||
if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) {
|
||||
ctx.NotFound("", nil)
|
||||
return ret
|
||||
var assigneeIDStrings []string
|
||||
for _, inputAssigneeID := range inputAssigneeIDs {
|
||||
if candidateAssignees.Contains(inputAssigneeID) {
|
||||
assigneeIDStrings = append(assigneeIDStrings, strconv.FormatInt(inputAssigneeID, 10))
|
||||
}
|
||||
}
|
||||
pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs
|
||||
pageMetaData.AssigneesData.SelectedAssigneeIDs = strings.Join(assigneeIDStrings, ",")
|
||||
|
||||
// Check if the passed reviewers (user/team) actually exist
|
||||
var reviewers []*user_model.User
|
||||
|
||||
@@ -79,16 +79,29 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
|
||||
return data
|
||||
}
|
||||
|
||||
data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
|
||||
if !data.CanModifyIssueOrPull {
|
||||
// it sets "Branches" template data,
|
||||
// it is used to render the "edit PR target branches" dropdown, and the "branch selector" in the issue's sidebar.
|
||||
PrepareBranchList(ctx)
|
||||
if ctx.Written() {
|
||||
return data
|
||||
}
|
||||
|
||||
data.retrieveAssigneesDataForIssueWriter(ctx)
|
||||
// it sets the "Assignees" template data, and the data is also used to "mention" users.
|
||||
data.retrieveAssigneesData(ctx)
|
||||
if ctx.Written() {
|
||||
return data
|
||||
}
|
||||
|
||||
// TODO: the issue/pull permissions are quite complex and unclear
|
||||
// A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch)
|
||||
// A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
|
||||
// For non-creator users, only writers could update some meta (eg: assignees, milestone, project)
|
||||
// Need to clarify the logic and add some tests in the future
|
||||
data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
|
||||
if !data.CanModifyIssueOrPull {
|
||||
return data
|
||||
}
|
||||
|
||||
data.retrieveMilestonesDataForIssueWriter(ctx)
|
||||
if ctx.Written() {
|
||||
return data
|
||||
@@ -99,11 +112,6 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
|
||||
return data
|
||||
}
|
||||
|
||||
PrepareBranchList(ctx)
|
||||
if ctx.Written() {
|
||||
return data
|
||||
}
|
||||
|
||||
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
|
||||
return data
|
||||
}
|
||||
@@ -131,7 +139,7 @@ func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Co
|
||||
}
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) {
|
||||
func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
|
||||
var err error
|
||||
d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,13 +6,10 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
// IssueSuggestions returns a list of issue suggestions
|
||||
@@ -29,54 +26,11 @@ func IssueSuggestions(ctx *context.Context) {
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
|
||||
searchOpt := &issue_indexer.SearchOptions{
|
||||
Paginator: &db.ListOptions{
|
||||
Page: 0,
|
||||
PageSize: 5,
|
||||
},
|
||||
Keyword: keyword,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
IsPull: isPull,
|
||||
IsClosed: nil,
|
||||
SortBy: issue_indexer.SortByUpdatedDesc,
|
||||
}
|
||||
|
||||
ids, _, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||
suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull, keyword)
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchIssues", err)
|
||||
ctx.ServerError("GetSuggestion", err)
|
||||
return
|
||||
}
|
||||
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindIssuesByIDs", err)
|
||||
return
|
||||
}
|
||||
|
||||
suggestions := make([]*structs.Issue, 0, len(issues))
|
||||
|
||||
for _, issue := range issues {
|
||||
suggestion := &structs.Issue{
|
||||
ID: issue.ID,
|
||||
Index: issue.Index,
|
||||
Title: issue.Title,
|
||||
State: issue.State(),
|
||||
}
|
||||
|
||||
if issue.IsPull {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
ctx.ServerError("LoadPullRequest", err)
|
||||
return
|
||||
}
|
||||
if issue.PullRequest != nil {
|
||||
suggestion.PullRequest = &structs.PullRequestMeta{
|
||||
HasMerged: issue.PullRequest.HasMerged,
|
||||
IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suggestions = append(suggestions, suggestion)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, suggestions)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,11 @@ func Projects(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
|
||||
ctx.ServerError("LoadIssueNumbersForProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range projects {
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
|
||||
projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description)
|
||||
@@ -312,7 +317,8 @@ func ViewProject(ctx *context.Context) {
|
||||
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
LabelIDs: labelIDs,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
})
|
||||
@@ -320,6 +326,9 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
return
|
||||
}
|
||||
for _, column := range columns {
|
||||
column.NumIssues = int64(len(issuesMap[column.ID]))
|
||||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
|
||||
|
||||
@@ -785,18 +785,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
||||
return
|
||||
}
|
||||
|
||||
allComments := issues_model.CommentList{}
|
||||
for _, file := range diff.Files {
|
||||
for _, section := range file.Sections {
|
||||
for _, line := range section.Lines {
|
||||
for _, comment := range line.Comments {
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
allComments = append(allComments, line.Comments...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := allComments.LoadAttachments(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO: Separate secrets from runners when layout is ready
|
||||
tplRepoRunners base.TplName = "repo/settings/actions"
|
||||
tplOrgRunners base.TplName = "org/settings/actions"
|
||||
tplAdminRunners base.TplName = "admin/actions"
|
||||
tplUserRunners base.TplName = "user/settings/actions"
|
||||
tplRepoRunnerEdit base.TplName = "repo/settings/runner_edit"
|
||||
tplOrgRunnerEdit base.TplName = "org/settings/runners_edit"
|
||||
tplAdminRunnerEdit base.TplName = "admin/runners/edit"
|
||||
tplUserRunnerEdit base.TplName = "user/settings/runner_edit"
|
||||
)
|
||||
|
||||
type runnersCtx struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
IsOrg bool
|
||||
IsAdmin bool
|
||||
IsUser bool
|
||||
RunnersTemplate base.TplName
|
||||
RunnerEditTemplate base.TplName
|
||||
RedirectLink string
|
||||
}
|
||||
|
||||
func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &runnersCtx{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
OwnerID: 0,
|
||||
IsRepo: true,
|
||||
RunnersTemplate: tplRepoRunners,
|
||||
RunnerEditTemplate: tplRepoRunnerEdit,
|
||||
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
err := shared_user.LoadHeaderCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadHeaderCount", err)
|
||||
return nil, nil
|
||||
}
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
IsOrg: true,
|
||||
RunnersTemplate: tplOrgRunners,
|
||||
RunnerEditTemplate: tplOrgRunnerEdit,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsAdmin"] == true {
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
OwnerID: 0,
|
||||
IsAdmin: true,
|
||||
RunnersTemplate: tplAdminRunners,
|
||||
RunnerEditTemplate: tplAdminRunnerEdit,
|
||||
RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &runnersCtx{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
RunnersTemplate: tplUserRunners,
|
||||
RunnerEditTemplate: tplUserRunnerEdit,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to set Runners context")
|
||||
}
|
||||
|
||||
// Runners render settings/actions/runners page for repo level
|
||||
func Runners(ctx *context.Context) {
|
||||
ctx.Data["PageIsSharedSettingsRunners"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||
ctx.Data["PageType"] = "runners"
|
||||
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
opts := actions_model.FindRunnerOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 100,
|
||||
},
|
||||
Sort: ctx.Req.URL.Query().Get("sort"),
|
||||
Filter: ctx.Req.URL.Query().Get("q"),
|
||||
}
|
||||
if rCtx.IsRepo {
|
||||
opts.RepoID = rCtx.RepoID
|
||||
opts.WithAvailable = true
|
||||
} else if rCtx.IsOrg || rCtx.IsUser {
|
||||
opts.OwnerID = rCtx.OwnerID
|
||||
opts.WithAvailable = true
|
||||
}
|
||||
actions_shared.RunnersList(ctx, opts)
|
||||
|
||||
ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
|
||||
}
|
||||
|
||||
// RunnersEdit renders runner edit page for repository level
|
||||
func RunnersEdit(ctx *context.Context) {
|
||||
ctx.Data["PageIsSharedSettingsRunners"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
actions_shared.RunnerDetails(ctx, page,
|
||||
ctx.PathParamInt64(":runnerid"), rCtx.OwnerID, rCtx.RepoID,
|
||||
)
|
||||
ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
|
||||
}
|
||||
|
||||
func RunnersEditPost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
actions_shared.RunnerDetailsEditPost(ctx, ctx.PathParamInt64(":runnerid"),
|
||||
rCtx.OwnerID, rCtx.RepoID,
|
||||
rCtx.RedirectLink+url.PathEscape(ctx.PathParam(":runnerid")))
|
||||
}
|
||||
|
||||
func ResetRunnerRegistrationToken(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
actions_shared.RunnerResetRegistrationToken(ctx, rCtx.OwnerID, rCtx.RepoID, rCtx.RedirectLink)
|
||||
}
|
||||
|
||||
// RunnerDeletePost response for deleting runner
|
||||
func RunnerDeletePost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
actions_shared.RunnerDeletePost(ctx, ctx.PathParamInt64(":runnerid"), rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.PathParam(":runnerid")))
|
||||
}
|
||||
|
||||
func RedirectToDefaultSetting(ctx *context.Context) {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
shared "code.gitea.io/gitea/routers/web/shared/actions"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplRepoVariables base.TplName = "repo/settings/actions"
|
||||
tplOrgVariables base.TplName = "org/settings/actions"
|
||||
tplUserVariables base.TplName = "user/settings/actions"
|
||||
tplAdminVariables base.TplName = "admin/actions"
|
||||
)
|
||||
|
||||
type variablesCtx struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
IsOrg bool
|
||||
IsUser bool
|
||||
IsGlobal bool
|
||||
VariablesTemplate base.TplName
|
||||
RedirectLink string
|
||||
}
|
||||
|
||||
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: 0,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsRepo: true,
|
||||
VariablesTemplate: tplRepoVariables,
|
||||
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
err := shared_user.LoadHeaderCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadHeaderCount", err)
|
||||
return nil, nil
|
||||
}
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
RepoID: 0,
|
||||
IsOrg: true,
|
||||
VariablesTemplate: tplOrgVariables,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
VariablesTemplate: tplUserVariables,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsAdmin"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: 0,
|
||||
RepoID: 0,
|
||||
IsGlobal: true,
|
||||
VariablesTemplate: tplAdminVariables,
|
||||
RedirectLink: setting.AppSubURL + "/-/admin/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to set Variables context")
|
||||
}
|
||||
|
||||
func Variables(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.variables")
|
||||
ctx.Data["PageType"] = "variables"
|
||||
ctx.Data["PageIsSharedSettingsVariables"] = true
|
||||
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
|
||||
}
|
||||
|
||||
func VariableCreate(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() { // form binding validation error
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func VariableUpdate(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() { // form binding validation error
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
shared.UpdateVariable(ctx, vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func VariableDelete(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
shared.DeleteVariable(ctx, vCtx.RedirectLink)
|
||||
}
|
||||
@@ -5,18 +5,131 @@ package actions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
)
|
||||
|
||||
// RunnersList prepares data for runners list
|
||||
func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
|
||||
const (
|
||||
// TODO: Separate secrets from runners when layout is ready
|
||||
tplRepoRunners base.TplName = "repo/settings/actions"
|
||||
tplOrgRunners base.TplName = "org/settings/actions"
|
||||
tplAdminRunners base.TplName = "admin/actions"
|
||||
tplUserRunners base.TplName = "user/settings/actions"
|
||||
tplRepoRunnerEdit base.TplName = "repo/settings/runner_edit"
|
||||
tplOrgRunnerEdit base.TplName = "org/settings/runners_edit"
|
||||
tplAdminRunnerEdit base.TplName = "admin/runners/edit"
|
||||
tplUserRunnerEdit base.TplName = "user/settings/runner_edit"
|
||||
)
|
||||
|
||||
type runnersCtx struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
IsOrg bool
|
||||
IsAdmin bool
|
||||
IsUser bool
|
||||
RunnersTemplate base.TplName
|
||||
RunnerEditTemplate base.TplName
|
||||
RedirectLink string
|
||||
}
|
||||
|
||||
func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &runnersCtx{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
OwnerID: 0,
|
||||
IsRepo: true,
|
||||
RunnersTemplate: tplRepoRunners,
|
||||
RunnerEditTemplate: tplRepoRunnerEdit,
|
||||
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
err := shared_user.LoadHeaderCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadHeaderCount", err)
|
||||
return nil, nil
|
||||
}
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
IsOrg: true,
|
||||
RunnersTemplate: tplOrgRunners,
|
||||
RunnerEditTemplate: tplOrgRunnerEdit,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsAdmin"] == true {
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
OwnerID: 0,
|
||||
IsAdmin: true,
|
||||
RunnersTemplate: tplAdminRunners,
|
||||
RunnerEditTemplate: tplAdminRunnerEdit,
|
||||
RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &runnersCtx{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
RunnersTemplate: tplUserRunners,
|
||||
RunnerEditTemplate: tplUserRunnerEdit,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to set Runners context")
|
||||
}
|
||||
|
||||
// Runners render settings/actions/runners page for repo level
|
||||
func Runners(ctx *context.Context) {
|
||||
ctx.Data["PageIsSharedSettingsRunners"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||
ctx.Data["PageType"] = "runners"
|
||||
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
opts := actions_model.FindRunnerOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 100,
|
||||
},
|
||||
Sort: ctx.Req.URL.Query().Get("sort"),
|
||||
Filter: ctx.Req.URL.Query().Get("q"),
|
||||
}
|
||||
if rCtx.IsRepo {
|
||||
opts.RepoID = rCtx.RepoID
|
||||
opts.WithAvailable = true
|
||||
} else if rCtx.IsOrg || rCtx.IsUser {
|
||||
opts.OwnerID = rCtx.OwnerID
|
||||
opts.WithAvailable = true
|
||||
}
|
||||
|
||||
runners, count, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountRunners", err)
|
||||
@@ -53,10 +166,29 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
|
||||
}
|
||||
|
||||
// RunnerDetails prepares data for runners edit page
|
||||
func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int64) {
|
||||
// RunnersEdit renders runner edit page for repository level
|
||||
func RunnersEdit(ctx *context.Context) {
|
||||
ctx.Data["PageIsSharedSettingsRunners"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
runnerID := ctx.PathParamInt64("runnerid")
|
||||
ownerID := rCtx.OwnerID
|
||||
repoID := rCtx.RepoID
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunnerByID", err)
|
||||
@@ -97,10 +229,22 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
|
||||
ctx.Data["Tasks"] = tasks
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
|
||||
}
|
||||
|
||||
// RunnerDetailsEditPost response for edit runner details
|
||||
func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, redirectTo string) {
|
||||
func RunnersEditPost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
runnerID := ctx.PathParamInt64("runnerid")
|
||||
ownerID := rCtx.OwnerID
|
||||
repoID := rCtx.RepoID
|
||||
redirectTo := rCtx.RedirectLink
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
|
||||
@@ -129,10 +273,18 @@ func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
|
||||
// RunnerResetRegistrationToken reset registration token
|
||||
func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) {
|
||||
_, err := actions_model.NewRunnerToken(ctx, ownerID, repoID)
|
||||
func ResetRunnerRegistrationToken(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
ownerID := rCtx.OwnerID
|
||||
repoID := rCtx.RepoID
|
||||
redirectTo := rCtx.RedirectLink
|
||||
|
||||
if _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID); err != nil {
|
||||
ctx.ServerError("ResetRunnerRegistrationToken", err)
|
||||
return
|
||||
}
|
||||
@@ -140,11 +292,28 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
|
||||
ctx.JSONRedirect(redirectTo)
|
||||
}
|
||||
|
||||
// RunnerDeletePost response for deleting a runner
|
||||
func RunnerDeletePost(ctx *context.Context, runnerID int64,
|
||||
successRedirectTo, failedRedirectTo string,
|
||||
) {
|
||||
if err := actions_model.DeleteRunner(ctx, runnerID); err != nil {
|
||||
// RunnerDeletePost response for deleting runner
|
||||
func RunnerDeletePost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
runner := findActionsRunner(ctx, rCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) {
|
||||
ctx.NotFound("RunnerDeletePost", util.NewPermissionDeniedErrorf("no permission to delete this runner"))
|
||||
return
|
||||
}
|
||||
|
||||
successRedirectTo := rCtx.RedirectLink
|
||||
failedRedirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid"))
|
||||
|
||||
if err := actions_model.DeleteRunner(ctx, runner.ID); err != nil {
|
||||
log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
|
||||
ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner_failed"))
|
||||
|
||||
@@ -158,3 +327,41 @@ func RunnerDeletePost(ctx *context.Context, runnerID int64,
|
||||
|
||||
ctx.JSONRedirect(successRedirectTo)
|
||||
}
|
||||
|
||||
func RedirectToDefaultSetting(ctx *context.Context) {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
|
||||
}
|
||||
|
||||
func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner {
|
||||
runnerID := ctx.PathParamInt64("runnerid")
|
||||
opts := &actions_model.FindRunnerOptions{
|
||||
IDs: []int64{runnerID},
|
||||
}
|
||||
switch {
|
||||
case rCtx.IsRepo:
|
||||
opts.RepoID = rCtx.RepoID
|
||||
if opts.RepoID == 0 {
|
||||
panic("repoID is 0")
|
||||
}
|
||||
case rCtx.IsOrg, rCtx.IsUser:
|
||||
opts.OwnerID = rCtx.OwnerID
|
||||
if opts.OwnerID == 0 {
|
||||
panic("ownerID is 0")
|
||||
}
|
||||
case rCtx.IsAdmin:
|
||||
// do nothing
|
||||
default:
|
||||
panic("invalid actions runner context")
|
||||
}
|
||||
|
||||
got, err := db.Find[actions_model.ActionRunner](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunner", err)
|
||||
return nil
|
||||
} else if len(got) == 0 {
|
||||
ctx.NotFound("FindRunner", errors.New("runner not found"))
|
||||
return nil
|
||||
}
|
||||
|
||||
return got[0]
|
||||
}
|
||||
|
||||
@@ -4,31 +4,127 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
)
|
||||
|
||||
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
|
||||
const (
|
||||
tplRepoVariables base.TplName = "repo/settings/actions"
|
||||
tplOrgVariables base.TplName = "org/settings/actions"
|
||||
tplUserVariables base.TplName = "user/settings/actions"
|
||||
tplAdminVariables base.TplName = "admin/actions"
|
||||
)
|
||||
|
||||
type variablesCtx struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
IsOrg bool
|
||||
IsUser bool
|
||||
IsGlobal bool
|
||||
VariablesTemplate base.TplName
|
||||
RedirectLink string
|
||||
}
|
||||
|
||||
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: 0,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsRepo: true,
|
||||
VariablesTemplate: tplRepoVariables,
|
||||
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
err := shared_user.LoadHeaderCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadHeaderCount", err)
|
||||
return nil, nil
|
||||
}
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
RepoID: 0,
|
||||
IsOrg: true,
|
||||
VariablesTemplate: tplOrgVariables,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
VariablesTemplate: tplUserVariables,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsAdmin"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: 0,
|
||||
RepoID: 0,
|
||||
IsGlobal: true,
|
||||
VariablesTemplate: tplAdminVariables,
|
||||
RedirectLink: setting.AppSubURL + "/-/admin/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to set Variables context")
|
||||
}
|
||||
|
||||
func Variables(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.variables")
|
||||
ctx.Data["PageType"] = "variables"
|
||||
ctx.Data["PageIsSharedSettingsVariables"] = true
|
||||
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
OwnerID: vCtx.OwnerID,
|
||||
RepoID: vCtx.RepoID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindVariables", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Variables"] = variables
|
||||
|
||||
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
|
||||
}
|
||||
|
||||
func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
func VariableCreate(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() { // form binding validation error
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||
|
||||
v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
|
||||
v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data)
|
||||
if err != nil {
|
||||
log.Error("CreateVariable: %v", err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
||||
@@ -36,30 +132,92 @@ func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL str
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func UpdateVariable(ctx *context.Context, redirectURL string) {
|
||||
id := ctx.PathParamInt64(":variable_id")
|
||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||
func VariableUpdate(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
|
||||
if ctx.HasError() { // form binding validation error
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.PathParamInt64("variable_id")
|
||||
|
||||
variable := findActionsVariable(ctx, id, vCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||
variable.Name = form.Name
|
||||
variable.Data = form.Data
|
||||
|
||||
if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
|
||||
log.Error("UpdateVariable: %v", err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func DeleteVariable(ctx *context.Context, redirectURL string) {
|
||||
id := ctx.PathParamInt64(":variable_id")
|
||||
func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *actions_model.ActionVariable {
|
||||
opts := actions_model.FindVariablesOpts{
|
||||
IDs: []int64{id},
|
||||
}
|
||||
switch {
|
||||
case vCtx.IsRepo:
|
||||
opts.RepoID = vCtx.RepoID
|
||||
if opts.RepoID == 0 {
|
||||
panic("RepoID is 0")
|
||||
}
|
||||
case vCtx.IsOrg, vCtx.IsUser:
|
||||
opts.OwnerID = vCtx.OwnerID
|
||||
if opts.OwnerID == 0 {
|
||||
panic("OwnerID is 0")
|
||||
}
|
||||
case vCtx.IsGlobal:
|
||||
// do nothing
|
||||
default:
|
||||
panic("invalid actions variable")
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
|
||||
got, err := actions_model.FindVariables(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindVariables", err)
|
||||
return nil
|
||||
} else if len(got) == 0 {
|
||||
ctx.NotFound("FindVariables", nil)
|
||||
return nil
|
||||
}
|
||||
return got[0]
|
||||
}
|
||||
|
||||
func VariableDelete(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.PathParamInt64("variable_id")
|
||||
|
||||
variable := findActionsVariable(ctx, id, vCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil {
|
||||
log.Error("Delete variable [%d] failed: %v", id, err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
IsPull: optional.Some(isPullList),
|
||||
SortType: sortType,
|
||||
IsArchived: optional.Some(false),
|
||||
User: ctx.Doer,
|
||||
Doer: ctx.Doer,
|
||||
}
|
||||
// --------------------------------------------------------------------------
|
||||
// Build opts (IssuesOptions), which contains filter information.
|
||||
@@ -431,7 +431,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
|
||||
// Get repository IDs where User/Org/Team has access.
|
||||
if ctx.Org != nil && ctx.Org.Organization != nil {
|
||||
opts.Org = ctx.Org.Organization
|
||||
opts.Owner = ctx.Org.Organization.AsUser()
|
||||
opts.Team = ctx.Org.Team
|
||||
|
||||
issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
|
||||
shared_actions "code.gitea.io/gitea/routers/web/shared/actions"
|
||||
"code.gitea.io/gitea/routers/web/shared/project"
|
||||
"code.gitea.io/gitea/routers/web/user"
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
@@ -442,10 +443,10 @@ func registerRoutes(m *web.Router) {
|
||||
|
||||
addSettingsVariablesRoutes := func() {
|
||||
m.Group("/variables", func() {
|
||||
m.Get("", repo_setting.Variables)
|
||||
m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate)
|
||||
m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate)
|
||||
m.Post("/{variable_id}/delete", repo_setting.VariableDelete)
|
||||
m.Get("", shared_actions.Variables)
|
||||
m.Post("/new", web.Bind(forms.EditVariableForm{}), shared_actions.VariableCreate)
|
||||
m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), shared_actions.VariableUpdate)
|
||||
m.Post("/{variable_id}/delete", shared_actions.VariableDelete)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -459,11 +460,11 @@ func registerRoutes(m *web.Router) {
|
||||
|
||||
addSettingsRunnersRoutes := func() {
|
||||
m.Group("/runners", func() {
|
||||
m.Get("", repo_setting.Runners)
|
||||
m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
|
||||
Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
|
||||
m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
|
||||
m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
|
||||
m.Get("", shared_actions.Runners)
|
||||
m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
|
||||
Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
|
||||
m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
|
||||
m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1132,7 +1133,7 @@ func registerRoutes(m *web.Router) {
|
||||
})
|
||||
})
|
||||
m.Group("/actions", func() {
|
||||
m.Get("", repo_setting.RedirectToDefaultSetting)
|
||||
m.Get("", shared_actions.RedirectToDefaultSetting)
|
||||
addSettingsRunnersRoutes()
|
||||
addSettingsSecretsRoutes()
|
||||
addSettingsVariablesRoutes()
|
||||
@@ -1634,7 +1635,7 @@ func registerRoutes(m *web.Router) {
|
||||
}
|
||||
|
||||
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.GetWebContext(req)
|
||||
ctx := context.GetWebContext(req.Context())
|
||||
routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))
|
||||
ctx.NotFound("", nil)
|
||||
})
|
||||
|
||||
@@ -52,9 +52,9 @@ func cleanExpiredArtifacts(taskCtx context.Context) error {
|
||||
}
|
||||
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
|
||||
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
|
||||
continue
|
||||
// go on
|
||||
}
|
||||
log.Info("Artifact %d set expired", artifact.ID)
|
||||
log.Info("Artifact %d is deleted (due to expiration)", artifact.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -76,9 +76,9 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
|
||||
}
|
||||
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
|
||||
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
|
||||
continue
|
||||
// go on
|
||||
}
|
||||
log.Info("Artifact %d set deleted", artifact.ID)
|
||||
log.Info("Artifact %d is deleted (due to pending deletion)", artifact.ID)
|
||||
}
|
||||
if len(artifacts) < deleteArtifactBatchSize {
|
||||
log.Debug("No more artifacts pending deletion")
|
||||
@@ -103,8 +103,7 @@ func CleanupLogs(ctx context.Context) error {
|
||||
for _, task := range tasks {
|
||||
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
|
||||
log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
|
||||
// do not return error here, continue to next task
|
||||
continue
|
||||
// do not return error here, go on
|
||||
}
|
||||
task.LogIndexes = nil // clear log indexes since it's a heavy field
|
||||
task.LogExpired = true
|
||||
|
||||
@@ -17,9 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
FixtureFiles: []string{"action_runner_token.yml"},
|
||||
})
|
||||
unittest.MainTest(m)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,50 +16,67 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/actions"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
|
||||
t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err)
|
||||
func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
|
||||
var (
|
||||
task *runnerv1.Task
|
||||
job *actions_model.ActionRunJob
|
||||
)
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateTaskForRunner: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := t.LoadAttributes(ctx); err != nil {
|
||||
return fmt.Errorf("task LoadAttributes: %w", err)
|
||||
}
|
||||
job = t.Job
|
||||
|
||||
secrets, err := secret_model.GetSecretsOfTask(ctx, t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetSecretsOfTask: %w", err)
|
||||
}
|
||||
|
||||
vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetVariablesOfRun: %w", err)
|
||||
}
|
||||
|
||||
needs, err := findTaskNeeds(ctx, job)
|
||||
if err != nil {
|
||||
return fmt.Errorf("findTaskNeeds: %w", err)
|
||||
}
|
||||
|
||||
taskContext := generateTaskContext(t)
|
||||
|
||||
task = &runnerv1.Task{
|
||||
Id: t.ID,
|
||||
WorkflowPayload: t.Job.WorkflowPayload,
|
||||
Context: taskContext,
|
||||
Secrets: secrets,
|
||||
Vars: vars,
|
||||
Needs: needs,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !ok {
|
||||
|
||||
if task == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
secrets, err := secret_model.GetSecretsOfTask(ctx, t)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
|
||||
}
|
||||
|
||||
vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
|
||||
}
|
||||
|
||||
actions.CreateCommitStatus(ctx, t.Job)
|
||||
|
||||
task := &runnerv1.Task{
|
||||
Id: t.ID,
|
||||
WorkflowPayload: t.Job.WorkflowPayload,
|
||||
Context: generateTaskContext(t),
|
||||
Secrets: secrets,
|
||||
Vars: vars,
|
||||
}
|
||||
|
||||
if needs, err := findTaskNeeds(ctx, t); err != nil {
|
||||
log.Error("Cannot find needs for task %v: %v", t.ID, err)
|
||||
// Go on with empty needs.
|
||||
// If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner.
|
||||
// In contrast, missing needs is less serious.
|
||||
// And the task will fail and the runner will report the error in the logs.
|
||||
} else {
|
||||
task.Needs = needs
|
||||
}
|
||||
CreateCommitStatus(ctx, job)
|
||||
|
||||
return task, true, nil
|
||||
}
|
||||
@@ -95,7 +112,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
|
||||
|
||||
refName := git.RefName(ref)
|
||||
|
||||
giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
|
||||
giteaRuntimeToken, err := CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
|
||||
if err != nil {
|
||||
log.Error("actions.CreateAuthorizationToken failed: %v", err)
|
||||
}
|
||||
@@ -148,16 +165,13 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
|
||||
return taskContext
|
||||
}
|
||||
|
||||
func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) {
|
||||
if err := task.LoadAttributes(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadAttributes: %w", err)
|
||||
}
|
||||
if len(task.Job.Needs) == 0 {
|
||||
func findTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*runnerv1.TaskNeed, error) {
|
||||
if len(job.Needs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
needs := container.SetOf(task.Job.Needs...)
|
||||
needs := container.SetOf(job.Needs...)
|
||||
|
||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID})
|
||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: job.RunID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FindRunJobs: %w", err)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -17,8 +17,9 @@ func Test_findTaskNeeds(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
||||
|
||||
ret, err := findTaskNeeds(context.Background(), task)
|
||||
ret, err := findTaskNeeds(context.Background(), job)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, ret, 1)
|
||||
assert.Contains(t, ret, "job1")
|
||||
@@ -6,7 +6,6 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -31,20 +30,18 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
|
||||
if err := secret_service.ValidateName(name); err != nil {
|
||||
func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) {
|
||||
if err := secret_service.ValidateName(variable.Name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := envNameCIRegexMatch(name); err != nil {
|
||||
if err := envNameCIRegexMatch(variable.Name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
|
||||
ID: variableID,
|
||||
Name: strings.ToUpper(name),
|
||||
Data: util.ReserveLineBreakForTextarea(data),
|
||||
})
|
||||
variable.Data = util.ReserveLineBreakForTextarea(variable.Data)
|
||||
|
||||
return actions_model.UpdateVariableCols(ctx, variable, "name", "data")
|
||||
}
|
||||
|
||||
func DeleteVariableByID(ctx context.Context, variableID int64) error {
|
||||
|
||||
@@ -104,7 +104,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
|
||||
middleware.SetLocaleCookie(resp, user.Language, 0)
|
||||
|
||||
// force to generate a new CSRF token
|
||||
if ctx := gitea_context.GetWebContext(req); ctx != nil {
|
||||
if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil {
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
|
||||
store.GetData()["EnableSSPI"] = true
|
||||
// in this case, the Verify function is called in Gitea's web context
|
||||
// FIXME: it doesn't look good to render the page here, why not redirect?
|
||||
gitea_context.GetWebContext(req).HTML(http.StatusUnauthorized, tplSignIn)
|
||||
gitea_context.GetWebContext(req.Context()).HTML(http.StatusUnauthorized, tplSignIn)
|
||||
return nil, err
|
||||
}
|
||||
if outToken != "" {
|
||||
|
||||
@@ -77,9 +77,9 @@ type webContextKeyType struct{}
|
||||
|
||||
var WebContextKey = webContextKeyType{}
|
||||
|
||||
func GetWebContext(req *http.Request) *Context {
|
||||
ctx, _ := req.Context().Value(WebContextKey).(*Context)
|
||||
return ctx
|
||||
func GetWebContext(ctx context.Context) *Context {
|
||||
webCtx, _ := ctx.Value(WebContextKey).(*Context)
|
||||
return webCtx
|
||||
}
|
||||
|
||||
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
|
||||
@@ -133,6 +133,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
||||
}
|
||||
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
|
||||
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
|
||||
ctx.AppendContextValue(WebContextKey, ctx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
||||
apiIssue.Labels = util.SliceNilAsEmpty(ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner))
|
||||
apiIssue.Repo = &api.RepositoryMeta{
|
||||
ID: issue.Repo.ID,
|
||||
Name: issue.Repo.Name,
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ToAPIPullRequest assumes following fields have been assigned with valid values:
|
||||
@@ -77,7 +78,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
Labels: apiIssue.Labels,
|
||||
Milestone: apiIssue.Milestone,
|
||||
Assignee: apiIssue.Assignee,
|
||||
Assignees: apiIssue.Assignees,
|
||||
Assignees: util.SliceNilAsEmpty(apiIssue.Assignees),
|
||||
State: apiIssue.State,
|
||||
Draft: pr.IsWorkInProgress(ctx),
|
||||
IsLocked: apiIssue.IsLocked,
|
||||
@@ -94,6 +95,10 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
|
||||
PinOrder: apiIssue.PinOrder,
|
||||
|
||||
// output "[]" rather than null to align to github outputs
|
||||
RequestedReviewers: []*api.User{},
|
||||
RequestedReviewersTeams: []*api.Team{},
|
||||
|
||||
AllowMaintainerEdit: pr.AllowMaintainerEdit,
|
||||
|
||||
Base: &api.PRBranchInfo{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ToRepo converts a Repository to api.Repository
|
||||
@@ -241,7 +242,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
MirrorInterval: mirrorInterval,
|
||||
MirrorUpdated: mirrorUpdated,
|
||||
RepoTransfer: transfer,
|
||||
Topics: repo.Topics,
|
||||
Topics: util.SliceNilAsEmpty(repo.Topics),
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
Licenses: repoLicenses.StringList(),
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func registerRepoHealthCheck() {
|
||||
RunAtStart: false,
|
||||
Schedule: "@midnight",
|
||||
},
|
||||
Timeout: 60 * time.Second,
|
||||
Timeout: time.Duration(setting.Git.Timeout.Default) * time.Second,
|
||||
Args: []string{},
|
||||
}, func(ctx context.Context, _ *user_model.User, config Config) error {
|
||||
rhcConfig := config.(*RepoHealthCheckConfig)
|
||||
|
||||
@@ -80,7 +80,7 @@ type DiffLine struct {
|
||||
Match int
|
||||
Type DiffLineType
|
||||
Content string
|
||||
Comments []*issues_model.Comment
|
||||
Comments issues_model.CommentList
|
||||
SectionInfo *DiffLineSectionInfo
|
||||
}
|
||||
|
||||
|
||||
73
services/issue/suggestion.go
Normal file
73
services/issue/suggestion.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool], keyword string) ([]*structs.Issue, error) {
|
||||
var issues issues_model.IssueList
|
||||
var err error
|
||||
pageSize := 5
|
||||
if keyword == "" {
|
||||
issues, err = issues_model.FindLatestUpdatedIssues(ctx, repo.ID, isPull, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
indexKeyword, _ := strconv.ParseInt(keyword, 10, 64)
|
||||
var issueByIndex *issues_model.Issue
|
||||
var excludedID int64
|
||||
if indexKeyword > 0 {
|
||||
issueByIndex, err = issues_model.GetIssueByIndex(ctx, repo.ID, indexKeyword)
|
||||
if err != nil && !issues_model.IsErrIssueNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if issueByIndex != nil {
|
||||
excludedID = issueByIndex.ID
|
||||
pageSize--
|
||||
}
|
||||
}
|
||||
|
||||
issues, err = issues_model.FindIssuesSuggestionByKeyword(ctx, repo.ID, keyword, isPull, excludedID, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if issueByIndex != nil {
|
||||
issues = append([]*issues_model.Issue{issueByIndex}, issues...)
|
||||
}
|
||||
}
|
||||
|
||||
if err := issues.LoadPullRequests(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
suggestions := make([]*structs.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
suggestion := &structs.Issue{
|
||||
ID: issue.ID,
|
||||
Index: issue.Index,
|
||||
Title: issue.Title,
|
||||
State: issue.State(),
|
||||
}
|
||||
|
||||
if issue.IsPull && issue.PullRequest != nil {
|
||||
suggestion.PullRequest = &structs.PullRequestMeta{
|
||||
HasMerged: issue.PullRequest.HasMerged,
|
||||
IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
|
||||
}
|
||||
}
|
||||
suggestions = append(suggestions, suggestion)
|
||||
}
|
||||
|
||||
return suggestions, nil
|
||||
}
|
||||
57
services/issue/suggestion_test.go
Normal file
57
services/issue/suggestion_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Suggestion(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
testCases := []struct {
|
||||
keyword string
|
||||
isPull optional.Option[bool]
|
||||
expectedIndexes []int64
|
||||
}{
|
||||
{
|
||||
keyword: "",
|
||||
expectedIndexes: []int64{5, 1, 4, 2, 3},
|
||||
},
|
||||
{
|
||||
keyword: "1",
|
||||
expectedIndexes: []int64{1},
|
||||
},
|
||||
{
|
||||
keyword: "issue",
|
||||
expectedIndexes: []int64{4, 1, 2, 3},
|
||||
},
|
||||
{
|
||||
keyword: "pull",
|
||||
expectedIndexes: []int64{5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.keyword, func(t *testing.T) {
|
||||
issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull, testCase.keyword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issueIndexes := make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
issueIndexes = append(issueIndexes, issue.Index)
|
||||
}
|
||||
assert.EqualValues(t, testCase.expectedIndexes, issueIndexes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@ func ProcessorHelper() *markup.RenderHelperFuncs {
|
||||
return false
|
||||
}
|
||||
|
||||
giteaCtx, ok := ctx.(*gitea_context.Context)
|
||||
if !ok {
|
||||
giteaCtx := gitea_context.GetWebContext(ctx)
|
||||
if giteaCtx == nil {
|
||||
// when using general context, use user's visibility to check
|
||||
return mentionedUser.Visibility.IsPublic()
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
|
||||
return "", err
|
||||
}
|
||||
|
||||
webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
|
||||
if !ok {
|
||||
webCtx := gitea_context.GetWebContext(ctx)
|
||||
if webCtx == nil {
|
||||
return "", fmt.Errorf("context is not a web context")
|
||||
}
|
||||
doer := webCtx.Doer
|
||||
|
||||
@@ -136,7 +136,9 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
|
||||
case strings.HasPrefix(lines[i], " - "): // Delete reference
|
||||
isTag := !strings.HasPrefix(refName, remoteName+"/")
|
||||
var refFullName git.RefName
|
||||
if isTag {
|
||||
if strings.HasPrefix(refName, "refs/") {
|
||||
refFullName = git.RefName(refName)
|
||||
} else if isTag {
|
||||
refFullName = git.RefNameFromTag(refName)
|
||||
} else {
|
||||
refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
|
||||
@@ -159,8 +161,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
|
||||
log.Error("Expect two SHAs but not what found: %q", lines[i])
|
||||
continue
|
||||
}
|
||||
var refFullName git.RefName
|
||||
if strings.HasPrefix(refName, "refs/") {
|
||||
refFullName = git.RefName(refName)
|
||||
} else {
|
||||
refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
|
||||
}
|
||||
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")),
|
||||
refName: refFullName,
|
||||
oldCommitID: shas[0],
|
||||
newCommitID: shas[1],
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/proxy"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -167,11 +168,13 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
||||
|
||||
log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName)
|
||||
|
||||
envs := proxy.EnvWithProxy(remoteURL.URL)
|
||||
if err := git.Push(ctx, path, git.PushOptions{
|
||||
Remote: m.RemoteName,
|
||||
Force: true,
|
||||
Mirror: true,
|
||||
Timeout: timeout,
|
||||
Env: envs,
|
||||
}); err != nil {
|
||||
log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err)
|
||||
|
||||
|
||||
@@ -17,9 +17,13 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
|
||||
- [deleted] (none) -> tag1
|
||||
+ f895a1e...957a993 test2 -> origin/test2 (forced update)
|
||||
957a993..a87ba5f test3 -> origin/test3
|
||||
* [new ref] refs/pull/26595/head -> refs/pull/26595/head
|
||||
* [new ref] refs/pull/26595/merge -> refs/pull/26595/merge
|
||||
e0639e38fb..6db2410489 refs/pull/25873/head -> refs/pull/25873/head
|
||||
+ 1c97ebc746...976d27d52f refs/pull/25873/merge -> refs/pull/25873/merge (forced update)
|
||||
`
|
||||
results := parseRemoteUpdateOutput(output, "origin")
|
||||
assert.Len(t, results, 6)
|
||||
assert.Len(t, results, 10)
|
||||
assert.EqualValues(t, "refs/tags/v0.1.8", results[0].refName.String())
|
||||
assert.EqualValues(t, gitShortEmptySha, results[0].oldCommitID)
|
||||
assert.EqualValues(t, "", results[0].newCommitID)
|
||||
@@ -43,4 +47,20 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
|
||||
assert.EqualValues(t, "refs/heads/test3", results[5].refName.String())
|
||||
assert.EqualValues(t, "957a993", results[5].oldCommitID)
|
||||
assert.EqualValues(t, "a87ba5f", results[5].newCommitID)
|
||||
|
||||
assert.EqualValues(t, "refs/pull/26595/head", results[6].refName.String())
|
||||
assert.EqualValues(t, gitShortEmptySha, results[6].oldCommitID)
|
||||
assert.EqualValues(t, "", results[6].newCommitID)
|
||||
|
||||
assert.EqualValues(t, "refs/pull/26595/merge", results[7].refName.String())
|
||||
assert.EqualValues(t, gitShortEmptySha, results[7].oldCommitID)
|
||||
assert.EqualValues(t, "", results[7].newCommitID)
|
||||
|
||||
assert.EqualValues(t, "refs/pull/25873/head", results[8].refName.String())
|
||||
assert.EqualValues(t, "e0639e38fb", results[8].oldCommitID)
|
||||
assert.EqualValues(t, "6db2410489", results[8].newCommitID)
|
||||
|
||||
assert.EqualValues(t, "refs/pull/25873/merge", results[9].refName.String())
|
||||
assert.EqualValues(t, "1c97ebc746", results[9].oldCommitID)
|
||||
assert.EqualValues(t, "976d27d52f", results[9].newCommitID)
|
||||
}
|
||||
|
||||
@@ -235,6 +235,28 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
|
||||
return packages_service.DeletePackageFile(ctx, pf)
|
||||
}
|
||||
|
||||
vpfs := make(map[int64]*entryOptions)
|
||||
for _, pf := range pfs {
|
||||
current := &entryOptions{
|
||||
File: pf,
|
||||
}
|
||||
current.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// here we compare the versions but not using SearchLatestVersions because we shouldn't allow "downgrading" to a older version by "latest" one.
|
||||
// https://wiki.archlinux.org/title/Downgrading_packages : randomly downgrading can mess up dependencies:
|
||||
// If a downgrade involves a soname change, all dependencies may need downgrading or rebuilding too.
|
||||
if old, ok := vpfs[current.Version.PackageID]; ok {
|
||||
if compareVersions(old.Version.Version, current.Version.Version) == -1 {
|
||||
vpfs[current.Version.PackageID] = current
|
||||
}
|
||||
} else {
|
||||
vpfs[current.Version.PackageID] = current
|
||||
}
|
||||
}
|
||||
|
||||
indexContent, _ := packages_module.NewHashedBuffer()
|
||||
defer indexContent.Close()
|
||||
|
||||
@@ -243,15 +265,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
|
||||
|
||||
cache := make(map[int64]*packages_model.Package)
|
||||
|
||||
for _, pf := range pfs {
|
||||
opts := &entryOptions{
|
||||
File: pf,
|
||||
}
|
||||
|
||||
opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, opts := range vpfs {
|
||||
if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -263,12 +277,12 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
|
||||
}
|
||||
cache[opts.Package.ID] = opts.Package
|
||||
}
|
||||
opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID)
|
||||
opts.Blob, err = packages_model.GetBlobByID(ctx, opts.File.BlobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature)
|
||||
sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertySignature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -277,7 +291,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
|
||||
}
|
||||
opts.Signature = sig[0].Value
|
||||
|
||||
meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata)
|
||||
meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertyMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
113
services/packages/arch/vercmp.go
Normal file
113
services/packages/arch/vercmp.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// https://gitlab.archlinux.org/pacman/pacman/-/blob/d55b47e5512808b67bc944feb20c2bcc6c1a4c45/lib/libalpm/version.c
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func parseEVR(evr string) (epoch, version, release string) {
|
||||
if before, after, f := strings.Cut(evr, ":"); f {
|
||||
epoch = before
|
||||
evr = after
|
||||
} else {
|
||||
epoch = "0"
|
||||
}
|
||||
|
||||
if before, after, f := strings.Cut(evr, "-"); f {
|
||||
version = before
|
||||
release = after
|
||||
} else {
|
||||
version = evr
|
||||
release = "1"
|
||||
}
|
||||
return epoch, version, release
|
||||
}
|
||||
|
||||
func compareSegments(a, b []string) int {
|
||||
lenA, lenB := len(a), len(b)
|
||||
var l int
|
||||
if lenA > lenB {
|
||||
l = lenB
|
||||
} else {
|
||||
l = lenA
|
||||
}
|
||||
for i := 0; i < l; i++ {
|
||||
if r := compare(a[i], b[i]); r != 0 {
|
||||
return r
|
||||
}
|
||||
}
|
||||
if lenA == lenB {
|
||||
return 0
|
||||
} else if l == lenA {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func compare(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
|
||||
aNumeric := isNumeric(a)
|
||||
bNumeric := isNumeric(b)
|
||||
|
||||
if aNumeric && bNumeric {
|
||||
aInt, _ := strconv.Atoi(a)
|
||||
bInt, _ := strconv.Atoi(b)
|
||||
switch {
|
||||
case aInt < bInt:
|
||||
return -1
|
||||
case aInt > bInt:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
if aNumeric {
|
||||
return 1
|
||||
}
|
||||
if bNumeric {
|
||||
return -1
|
||||
}
|
||||
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func isNumeric(s string) bool {
|
||||
for _, c := range s {
|
||||
if !unicode.IsDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func compareVersions(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
|
||||
epochA, versionA, releaseA := parseEVR(a)
|
||||
epochB, versionB, releaseB := parseEVR(b)
|
||||
|
||||
if res := compareSegments([]string{epochA}, []string{epochB}); res != 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
if res := compareSegments(strings.Split(versionA, "."), strings.Split(versionB, ".")); res != 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
return compareSegments([]string{releaseA}, []string{releaseB})
|
||||
}
|
||||
27
services/packages/arch/vercmp_test.go
Normal file
27
services/packages/arch/vercmp_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
// https://man.archlinux.org/man/vercmp.8.en
|
||||
checks := [][]string{
|
||||
{"1.0a", "1.0b", "1.0beta", "1.0p", "1.0pre", "1.0rc", "1.0", "1.0.a", "1.0.1"},
|
||||
{"1", "1.0", "1.1", "1.1.1", "1.2", "2.0", "3.0.0"},
|
||||
}
|
||||
for _, check := range checks {
|
||||
for i := 0; i < len(check)-1; i++ {
|
||||
require.Equal(t, -1, compareVersions(check[i], check[i+1]))
|
||||
require.Equal(t, 1, compareVersions(check[i+1], check[i]))
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, compareVersions("1.0-2", "1.0"))
|
||||
require.Equal(t, 0, compareVersions("0:1.0-1", "1.0"))
|
||||
require.Equal(t, 1, compareVersions("1:1.0-1", "2.0"))
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
|
||||
@@ -84,3 +85,123 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// LoadIssuesFromProject load issues assigned to each project column inside the given project
|
||||
func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (map[int64]issues_model.IssueList, error) {
|
||||
issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
|
||||
o.ProjectID = project.ID
|
||||
o.SortType = "project-column-sorting"
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issueList.LoadComments(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultColumn, err := project.MustDefaultColumn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueColumnMap, err := issues_model.LoadProjectIssueColumnMap(ctx, project.ID, defaultColumn.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(map[int64]issues_model.IssueList)
|
||||
for _, issue := range issueList {
|
||||
projectColumnID, ok := issueColumnMap[issue.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := results[projectColumnID]; !ok {
|
||||
results[projectColumnID] = make(issues_model.IssueList, 0)
|
||||
}
|
||||
results[projectColumnID] = append(results[projectColumnID], issue)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// NumClosedIssues return counter of closed issues assigned to a project
|
||||
func loadNumClosedIssues(ctx context.Context, p *project_model.Project) error {
|
||||
cnt, 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 {
|
||||
return err
|
||||
}
|
||||
p.NumClosedIssues = cnt
|
||||
return nil
|
||||
}
|
||||
|
||||
// NumOpenIssues return counter of open issues assigned to a project
|
||||
func loadNumOpenIssues(ctx context.Context, p *project_model.Project) error {
|
||||
cnt, 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 {
|
||||
return err
|
||||
}
|
||||
p.NumOpenIssues = cnt
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadIssueNumbersForProjects(ctx context.Context, projects []*project_model.Project, doer *user_model.User) error {
|
||||
for _, project := range projects {
|
||||
if err := LoadIssueNumbersForProject(ctx, project, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Project, doer *user_model.User) error {
|
||||
// for repository project, just get the numbers
|
||||
if project.OwnerID == 0 {
|
||||
if err := loadNumClosedIssues(ctx, project); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadNumOpenIssues(ctx, project); err != nil {
|
||||
return err
|
||||
}
|
||||
project.NumIssues = project.NumClosedIssues + project.NumOpenIssues
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := project.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// for user or org projects, we need to check access permissions
|
||||
opts := issues_model.IssuesOptions{
|
||||
ProjectID: project.ID,
|
||||
Doer: doer,
|
||||
AllPublic: doer == nil,
|
||||
Owner: project.Owner,
|
||||
}
|
||||
|
||||
var err error
|
||||
project.NumOpenIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
|
||||
o.IsClosed = optional.Some(false)
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project.NumClosedIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
|
||||
o.IsClosed = optional.Some(true)
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project.NumIssues = project.NumClosedIssues + project.NumOpenIssues
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
210
services/projects/issue_test.go
Normal file
210
services/projects/issue_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Projects(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
t.Run("User projects", func(t *testing.T) {
|
||||
pi1 := project_model.ProjectIssue{
|
||||
ProjectID: 4,
|
||||
IssueID: 1,
|
||||
ProjectColumnID: 4,
|
||||
}
|
||||
err := db.Insert(db.DefaultContext, &pi1)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi1.ID)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
pi2 := project_model.ProjectIssue{
|
||||
ProjectID: 4,
|
||||
IssueID: 4,
|
||||
ProjectColumnID: 4,
|
||||
}
|
||||
err = db.Insert(db.DefaultContext, &pi2)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi2.ID)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
|
||||
OwnerID: user2.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, projects, 3)
|
||||
assert.EqualValues(t, 4, projects[0].ID)
|
||||
|
||||
t.Run("Authenticated user", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
Owner: user2,
|
||||
Doer: user2,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 1) // 4 has 2 issues, 6 will not contains here because 0 issues
|
||||
assert.Len(t, columnIssues[4], 2) // user2 can visit both issues, one from public repository one from private repository
|
||||
})
|
||||
|
||||
t.Run("Anonymous user", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
AllPublic: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 1)
|
||||
assert.Len(t, columnIssues[4], 1) // anonymous user can only visit public repo issues
|
||||
})
|
||||
|
||||
t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
Owner: user2,
|
||||
Doer: user4,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 1)
|
||||
assert.Len(t, columnIssues[4], 1) // user4 can only visit public repo issues
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Org projects", func(t *testing.T) {
|
||||
project1 := project_model.Project{
|
||||
Title: "project in an org",
|
||||
OwnerID: org3.ID,
|
||||
Type: project_model.TypeOrganization,
|
||||
TemplateType: project_model.TemplateTypeBasicKanban,
|
||||
}
|
||||
err := project_model.NewProject(db.DefaultContext, &project1)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
err := project_model.DeleteProjectByID(db.DefaultContext, project1.ID)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
column1 := project_model.Column{
|
||||
Title: "column 1",
|
||||
ProjectID: project1.ID,
|
||||
}
|
||||
err = project_model.NewColumn(db.DefaultContext, &column1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
column2 := project_model.Column{
|
||||
Title: "column 2",
|
||||
ProjectID: project1.ID,
|
||||
}
|
||||
err = project_model.NewColumn(db.DefaultContext, &column2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// issue 6 belongs to private repo 3 under org 3
|
||||
issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
|
||||
err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue6, user2, project1.ID, column1.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// issue 16 belongs to public repo 16 under org 3
|
||||
issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16})
|
||||
err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue16, user2, project1.ID, column1.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
|
||||
OwnerID: org3.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, projects, 1)
|
||||
assert.EqualValues(t, project1.ID, projects[0].ID)
|
||||
|
||||
t.Run("Authenticated user", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
Owner: org3.AsUser(),
|
||||
Doer: userAdmin,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 1) // column1 has 2 issues, 6 will not contains here because 0 issues
|
||||
assert.Len(t, columnIssues[column1.ID], 2) // user2 can visit both issues, one from public repository one from private repository
|
||||
})
|
||||
|
||||
t.Run("Anonymous user", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
AllPublic: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 1)
|
||||
assert.Len(t, columnIssues[column1.ID], 1) // anonymous user can only visit public repo issues
|
||||
})
|
||||
|
||||
t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
Owner: org3.AsUser(),
|
||||
Doer: user2,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 1)
|
||||
assert.Len(t, columnIssues[column1.ID], 1) // user4 can only visit public repo issues
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Repository projects", func(t *testing.T) {
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
|
||||
RepoID: repo1.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, projects, 1)
|
||||
assert.EqualValues(t, 1, projects[0].ID)
|
||||
|
||||
t.Run("Authenticated user", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{repo1.ID},
|
||||
Doer: userAdmin,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 3)
|
||||
assert.Len(t, columnIssues[1], 2)
|
||||
assert.Len(t, columnIssues[2], 1)
|
||||
assert.Len(t, columnIssues[3], 1)
|
||||
})
|
||||
|
||||
t.Run("Anonymous user", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
AllPublic: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 3)
|
||||
assert.Len(t, columnIssues[1], 2)
|
||||
assert.Len(t, columnIssues[2], 1)
|
||||
assert.Len(t, columnIssues[3], 1)
|
||||
})
|
||||
|
||||
t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
|
||||
columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{repo1.ID},
|
||||
Doer: user2,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnIssues, 3)
|
||||
assert.Len(t, columnIssues[1], 2)
|
||||
assert.Len(t, columnIssues[2], 1)
|
||||
assert.Len(t, columnIssues[3], 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
18
services/projects/main_test.go
Normal file
18
services/projects/main_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
_ "code.gitea.io/gitea/models/activities"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
@@ -52,8 +52,9 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
||||
IsEmpty: !opts.AutoInit,
|
||||
}
|
||||
|
||||
repoPath := repo_model.RepoPath(u.Name, repo.Name)
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
repoPath := repo_model.RepoPath(u.Name, repo.Name)
|
||||
isExist, err := util.IsExist(repoPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
|
||||
@@ -75,7 +76,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
||||
if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
|
||||
return fmt.Errorf("getRepositoryByID: %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("adoptRepository: %w", err)
|
||||
}
|
||||
@@ -84,13 +90,6 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
||||
return fmt.Errorf("checkDaemonExportOK: %w", err)
|
||||
}
|
||||
|
||||
// Initialize Issue Labels if selected
|
||||
if len(opts.IssueLabels) > 0 {
|
||||
if err := repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
|
||||
return fmt.Errorf("InitializeLabels: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if stdout, _, err := git.NewCommand(ctx, "update-server-info").
|
||||
SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
|
||||
RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
|
||||
@@ -98,10 +97,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
||||
return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
}(); err != nil {
|
||||
if errDel := DeleteRepository(ctx, doer, repo, false /* no notify */); errDel != nil {
|
||||
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notify_service.AdoptRepository(ctx, doer, u, repo)
|
||||
|
||||
return repo, nil
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -875,6 +876,11 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod
|
||||
return
|
||||
}
|
||||
|
||||
// as a webhook url, target should be an absolute url. But for internal actions target url
|
||||
// the target url is a url path with no host and port to make it easy to be visited
|
||||
// from multiple hosts. So we need to convert it to an absolute url here.
|
||||
target := httplib.MakeAbsoluteURL(ctx, status.TargetURL)
|
||||
|
||||
payload := api.CommitStatusPayload{
|
||||
Context: status.Context,
|
||||
CreatedAt: status.CreatedUnix.AsTime().UTC(),
|
||||
@@ -882,7 +888,7 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod
|
||||
ID: status.ID,
|
||||
SHA: commit.Sha1,
|
||||
State: status.State.String(),
|
||||
TargetURL: status.TargetURL,
|
||||
TargetURL: target,
|
||||
|
||||
Commit: apiCommit,
|
||||
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
<div class="group">
|
||||
<div class="flex-text-block">
|
||||
{{svg "octicon-issue-opened" 14}}
|
||||
{{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}} {{ctx.Locale.Tr "repo.issues.open_title"}}
|
||||
{{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
|
||||
</div>
|
||||
<div class="flex-text-block">
|
||||
{{svg "octicon-check" 14}}
|
||||
{{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
|
||||
{{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
|
||||
</div>
|
||||
</div>
|
||||
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<div class="project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
||||
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
|
||||
<div class="ui circular label project-column-issue-count">
|
||||
{{.NumIssues ctx}}
|
||||
{{.NumIssues}}
|
||||
</div>
|
||||
<div class="project-column-title-label gt-ellipsis">{{.Title}}</div>
|
||||
{{if $canWriteProject}}
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="status" type="checkbox" {{if .Webhook.HookEvents.Get "status"}}checked{{end}}>
|
||||
<input name="status" type="checkbox" {{if .Webhook.Status}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_statuses"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_statuses_desc"}}</span>
|
||||
</div>
|
||||
|
||||
151
tests/integration/actions_runner_modify_test.go
Normal file
151
tests/integration/actions_runner_modify_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsRunnerModify(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, db.DeleteAllRecords("action_runner"))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner", TokenHash: "a", UUID: "a"})
|
||||
user2Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner"})
|
||||
userWebURL := "/user/settings/actions/runners"
|
||||
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner", TokenHash: "b", UUID: "b"}))
|
||||
org3Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner"})
|
||||
orgWebURL := "/org/org3/settings/actions/runners"
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner", TokenHash: "c", UUID: "c"})
|
||||
repo1Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner"})
|
||||
repoWebURL := "/user2/repo1/settings/actions/runners"
|
||||
|
||||
_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "global-runner", TokenHash: "d", UUID: "d"})
|
||||
globalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "global-runner"})
|
||||
adminWebURL := "/-/admin/actions/runners"
|
||||
|
||||
sessionAdmin := loginUser(t, "user1")
|
||||
sessionUser2 := loginUser(t, user2.Name)
|
||||
|
||||
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, description string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
"description": description,
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
|
||||
doDelete(t, sess, baseURL, id, http.StatusNotFound)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.Empty(t, v.Description)
|
||||
}
|
||||
|
||||
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.Equal(t, "ChangedDescription", v.Description)
|
||||
doDelete(t, sess, baseURL, id, http.StatusOK)
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
|
||||
}
|
||||
|
||||
t.Run("UpdateUserRunner", func(t *testing.T) {
|
||||
theRunner := user2Runner
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
t.Skip("Admin can update any runner (not right but not too bad)")
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateOrgRunner", func(t *testing.T) {
|
||||
theRunner := org3Runner
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
t.Skip("Admin can update any runner (not right but not too bad)")
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateRepoRunner", func(t *testing.T) {
|
||||
theRunner := repo1Runner
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
t.Skip("Admin can update any runner (not right but not too bad)")
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateGlobalRunner", func(t *testing.T) {
|
||||
theRunner := globalRunner
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateSuccess", func(t *testing.T) {
|
||||
t.Run("User", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, userWebURL, user2Runner.ID)
|
||||
})
|
||||
t.Run("Org", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, orgWebURL, org3Runner.ID)
|
||||
})
|
||||
t.Run("Repo", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, repoWebURL, repo1Runner.ID)
|
||||
})
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, adminWebURL, globalRunner.ID)
|
||||
})
|
||||
})
|
||||
}
|
||||
149
tests/integration/actions_variables_test.go
Normal file
149
tests/integration/actions_variables_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsVariables(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, db.DeleteAllRecords("action_variable"))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
_, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var")
|
||||
user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"})
|
||||
userWebURL := "/user/settings/actions/variables"
|
||||
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
_, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var")
|
||||
org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"})
|
||||
orgWebURL := "/org/org3/settings/actions/variables"
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var")
|
||||
repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"})
|
||||
repoWebURL := "/user2/repo1/settings/actions/variables"
|
||||
|
||||
_, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var")
|
||||
globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"})
|
||||
adminWebURL := "/-/admin/actions/variables"
|
||||
|
||||
sessionAdmin := loginUser(t, "user1")
|
||||
sessionUser2 := loginUser(t, user2.Name)
|
||||
|
||||
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
"name": "VAR",
|
||||
"data": data,
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusNotFound)
|
||||
doDelete(t, sess, baseURL, id, http.StatusNotFound)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
|
||||
assert.Contains(t, v.Data, "-var")
|
||||
}
|
||||
|
||||
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusOK)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
|
||||
assert.Equal(t, "ChangedData", v.Data)
|
||||
doDelete(t, sess, baseURL, id, http.StatusOK)
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionVariable{ID: id})
|
||||
}
|
||||
|
||||
t.Run("UpdateUserVar", func(t *testing.T) {
|
||||
theVar := user2Var
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateOrgVar", func(t *testing.T) {
|
||||
theVar := org3Var
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateRepoVar", func(t *testing.T) {
|
||||
theVar := repo1Var
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateGlobalVar", func(t *testing.T) {
|
||||
theVar := globalVar
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateSuccess", func(t *testing.T) {
|
||||
t.Run("User", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, userWebURL, user2Var.ID)
|
||||
})
|
||||
t.Run("Org", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, orgWebURL, org3Var.ID)
|
||||
})
|
||||
t.Run("Repo", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, repoWebURL, repo1Var.ID)
|
||||
})
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, adminWebURL, globalVar.ID)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -79,6 +79,34 @@ license = MIT`)
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
readIndexContent := func(r io.Reader) (map[string]string, error) {
|
||||
gzr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := make(map[string]string)
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content[hd.Name] = string(buf)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
compressions := []string{"gz", "xz", "zst"}
|
||||
repositories := []string{"main", "testing", "with/slash", ""}
|
||||
@@ -171,35 +199,6 @@ license = MIT`)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
})
|
||||
|
||||
readIndexContent := func(r io.Reader) (map[string]string, error) {
|
||||
gzr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := make(map[string]string)
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content[hd.Name] = string(buf)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
t.Run("Index", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -299,4 +298,39 @@ license = MIT`)
|
||||
})
|
||||
}
|
||||
}
|
||||
t.Run("KeepLastVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
pkgVer1 := createPackage("gz", "gitea-test", "1.0.0", "aarch64")
|
||||
pkgVer2 := createPackage("gz", "gitea-test", "1.0.1", "aarch64")
|
||||
req := NewRequestWithBody(t, "PUT", rootURL, bytes.NewReader(pkgVer1)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithBody(t, "PUT", rootURL, bytes.NewReader(pkgVer2)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
content, err := readIndexContent(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, content, 2)
|
||||
|
||||
_, has := content["gitea-test-1.0.0/desc"]
|
||||
assert.False(t, has)
|
||||
_, has = content["gitea-test-1.0.1/desc"]
|
||||
assert.True(t, has)
|
||||
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/gitea-test/1.0.1/aarch64", rootURL)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
content, err = readIndexContent(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, content, 2)
|
||||
_, has = content["gitea-test-1.0.0/desc"]
|
||||
assert.True(t, has)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
||||
|
||||
columnsAfter, err := project1.GetColumns(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.Len(t, columnsAfter, 3)
|
||||
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
||||
assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID)
|
||||
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
|
||||
|
||||
Reference in New Issue
Block a user