Compare commits

..

18 Commits

Author SHA1 Message Date
Lunny Xiao
de7026528b Release of Gitea 1.23.4 (#33621)
---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
2025-02-19 01:16:26 +00:00
Lunny Xiao
ee3f5e8fac fix: add missing locale (#33641) (#33642) 2025-02-19 00:57:14 +00:00
Giteabot
b2707bcd18 Make actions URL in commit status webhooks absolute (#33620) (#33632)
Backport #33620 by lunny

Fix #33603

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-18 02:46:08 +00:00
Giteabot
0512b02b01 Fix project issues list and counting (#33594) (#33619)
Backport #33594 by lunny

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-18 00:59:32 +08:00
Giteabot
99545ae2fd Fix mirror bug (#33597) (#33607)
Backport #33597 by @ericLemanissier

follows-up be4e961240

This is the same modification as
be4e961240
but for force-pushes.
It is needed, because `git fetch` reveals force pushes for github
mirrors:
```
$ git fetch --tags origin
remote: Enumerating objects: 22, done.
remote: Counting objects: 100% (22/22), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 9 (delta 5), reused 8 (delta 5), pack-reused 0 (from 0)
Unpacking objects: 100% (9/9), 1.70 KiB | 12.00 KiB/s, done.
From https://github.com/conan-io/conan-center-index
   729f0f1b8f..48184eddeb  refs/pull/26595/head  -> refs/pull/26595/head
 + 0c31ab60a3...1283cca9e7 refs/pull/26595/merge -> refs/pull/26595/merge  (forced update)
```

Fix https://github.com/go-gitea/gitea/issues/33200

PS: I did not test the modification, but it is the exact same change as
the last hunk in
https://github.com/go-gitea/gitea/pull/33224/files#diff-bb5cdb90db0f0e7f6716c0e6d0b9cbb67f08d82052b03ab3a7b5e23a1d76aed7
, just moved to the previous case of the switch

Co-authored-by: ericLemanissier <ericLemanissier@users.noreply.github.com>
2025-02-15 21:17:34 -08:00
Giteabot
7697df9f93 Use default Git timeout when checking repo health (#33593) (#33598)
Backport #33593 by @Zettat123

Use `git.timeout.DEFAULT` configuration instead of 60 seconds.

Co-authored-by: Zettat123 <zettat123@gmail.com>
2025-02-14 15:38:55 +00:00
wxiaoguang
d17f8ffcc1 Fix PR's target branch dropdown (#33589) (#33591)
Backport #33589
2025-02-14 08:38:33 +00:00
Giteabot
5e9cc919cf Performance optimization for pull request files loading comments attachments (#33585) (#33592)
Backport #33585 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-02-14 00:06:46 -08:00
Giteabot
cc6ec56738 Only show the latest version in the Arch index (#33262) (#33580)
Backport #33262 by ExplodingDragon

Only show the latest version of the package in the arch repo.

closes #33534

Co-authored-by: Exploding Dragon <explodingfkl@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-13 20:02:28 +08:00
Giteabot
76bd60fc1d Fix various problems (artifact order, api empty slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569) (#33577)
Backport #33569 by @wxiaoguang

* Make artifact list output has stable order
* Fix #33506
* Fix #33521
* Fix #33288
* Fix #33196
* Fix #33561

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-13 07:33:11 +08:00
wxiaoguang
744f7c8200 Skip deletion error for action artifacts (#33476) (#33568)
Fix #33567
2025-02-13 03:27:37 +08:00
Lunny Xiao
da33b708af Add a transaction to pickTask (#33543) (#33563)
Backport #33543 

In the old `pickTask`, when getting secrets or variables failed, the
task could get stuck in the `running` status (task status is `running`
but the runner did not fetch the task). To fix this issue, these steps
should be in one transaction.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-12 11:53:56 +08:00
wxiaoguang
8fa3925874 Fix context usage (#33554) (#33557)
Backport #33554
2025-02-11 19:46:27 +08:00
Lunny Xiao
7794ff0874 Enhance routers for the Actions runner operations (#33549) (#33555)
Backport #33549 

- Find the runner before deleting
- Move the main logic from `routers/web/repo/setting/runners.go` to
`routers/web/shared/actions/runners.go`.

Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-11 15:25:56 +08:00
Lunny Xiao
7c17d0a73e Enhance routers for the Actions variable operations (#33547) (#33553)
Backport #33547

Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2025-02-11 03:52:09 +00:00
Giteabot
a014d071e4 Rework suggestion backend (#33538) (#33546)
Backport #33538 by @lunny

Fix #33522 

The suggestion backend logic now is

- If the keyword is empty, returned the latest 5 issues/prs with index
desc order
- If the keyword is digital, find all issues/prs which `index` has a
prefix with that, with index asc order
- If the keyword is non-digital or if the queried records less than 5,
searching issues/prs title with a `like`, with index desc order

## Empty keyword
<img width="310" alt="image"
src="https://github.com/user-attachments/assets/1912c634-0d98-4eeb-8542-d54240901f77"
/>

## Digital
<img width="479" alt="image"
src="https://github.com/user-attachments/assets/0356a936-7110-4a24-b21e-7400201bf9b8"
/>

## Digital and title contains the digital
<img width="363" alt="image"
src="https://github.com/user-attachments/assets/6e12f908-28fe-48de-8ccc-09cbeab024d4"
/>

## non-Digital
<img width="435" alt="image"
src="https://github.com/user-attachments/assets/2722bb53-baa2-4d67-a224-522a65f73856"
/>
<img width="477" alt="image"
src="https://github.com/user-attachments/assets/06708dd9-80d1-4a88-b32b-d29072dd1ba6"
/>

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-02-11 01:22:39 +08:00
Lunny Xiao
312565e3c2 Add changelog for 1.23.3 (#33515)
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2025-02-06 12:32:13 +08:00
Lunny Xiao
58daaf66e8 Fix a bug caused by status webhook template (#33512)
Fix #33511
2025-02-06 08:48:22 +08:00
67 changed files with 1835 additions and 687 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 != "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -80,7 +80,7 @@ type DiffLine struct {
Match int
Type DiffLineType
Content string
Comments []*issues_model.Comment
Comments issues_model.CommentList
SectionInfo *DiffLineSectionInfo
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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"))
}

View File

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

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

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

View File

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

View File

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

View File

@@ -52,11 +52,11 @@
<div class="group">
<div class="flex-text-block">
{{svg "octicon-issue-opened" 14}}
{{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
{{ctx.Locale.PrettyNumber .NumOpenIssues}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
</div>
<div class="flex-text-block">
{{svg "octicon-check" 14}}
{{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
{{ctx.Locale.PrettyNumber .NumClosedIssues}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
</div>
</div>
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}

View File

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

View File

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

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

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

View File

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

View File

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