mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-02 15:09:33 -05:00
Merge branch 'main' into lunny/add_last_commit_when
This commit is contained in:
commit
14821a5a01
@ -674,7 +674,7 @@ module.exports = {
|
|||||||
'no-this-before-super': [2],
|
'no-this-before-super': [2],
|
||||||
'no-throw-literal': [2],
|
'no-throw-literal': [2],
|
||||||
'no-undef-init': [2],
|
'no-undef-init': [2],
|
||||||
'no-undef': [0],
|
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
|
||||||
'no-undefined': [0],
|
'no-undefined': [0],
|
||||||
'no-underscore-dangle': [0],
|
'no-underscore-dangle': [0],
|
||||||
'no-unexpected-multiline': [2],
|
'no-unexpected-multiline': [2],
|
||||||
|
@ -1339,6 +1339,9 @@ LEVEL = Info
|
|||||||
;; Number of repos that are displayed on one page
|
;; Number of repos that are displayed on one page
|
||||||
;REPO_PAGING_NUM = 15
|
;REPO_PAGING_NUM = 15
|
||||||
|
|
||||||
|
;; Number of orgs that are displayed on profile page
|
||||||
|
;ORG_PAGING_NUM = 15
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;[ui.meta]
|
;[ui.meta]
|
||||||
|
@ -275,7 +275,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
run.Index = index
|
run.Index = index
|
||||||
run.Title, _ = util.SplitStringAtByteN(run.Title, 255)
|
run.Title = util.EllipsisDisplayString(run.Title, 255)
|
||||||
|
|
||||||
if err := db.Insert(ctx, run); err != nil {
|
if err := db.Insert(ctx, run); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -308,7 +308,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
|||||||
} else {
|
} else {
|
||||||
hasWaiting = true
|
hasWaiting = true
|
||||||
}
|
}
|
||||||
job.Name, _ = util.SplitStringAtByteN(job.Name, 255)
|
job.Name = util.EllipsisDisplayString(job.Name, 255)
|
||||||
runJobs = append(runJobs, &ActionRunJob{
|
runJobs = append(runJobs, &ActionRunJob{
|
||||||
RunID: run.ID,
|
RunID: run.ID,
|
||||||
RepoID: run.RepoID,
|
RepoID: run.RepoID,
|
||||||
@ -402,7 +402,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||||||
if len(cols) > 0 {
|
if len(cols) > 0 {
|
||||||
sess.Cols(cols...)
|
sess.Cols(cols...)
|
||||||
}
|
}
|
||||||
run.Title, _ = util.SplitStringAtByteN(run.Title, 255)
|
run.Title = util.EllipsisDisplayString(run.Title, 255)
|
||||||
affected, err := sess.Update(run)
|
affected, err := sess.Update(run)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -252,7 +252,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) {
|
|||||||
// UpdateRunner updates runner's information.
|
// UpdateRunner updates runner's information.
|
||||||
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
|
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
r.Name, _ = util.SplitStringAtByteN(r.Name, 255)
|
r.Name = util.EllipsisDisplayString(r.Name, 255)
|
||||||
var err error
|
var err error
|
||||||
if len(cols) == 0 {
|
if len(cols) == 0 {
|
||||||
_, err = e.ID(r.ID).AllCols().Update(r)
|
_, err = e.ID(r.ID).AllCols().Update(r)
|
||||||
@ -279,7 +279,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
|||||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||||
t.OwnerID = 0
|
t.OwnerID = 0
|
||||||
}
|
}
|
||||||
t.Name, _ = util.SplitStringAtByteN(t.Name, 255)
|
t.Name = util.EllipsisDisplayString(t.Name, 255)
|
||||||
return db.Insert(ctx, t)
|
return db.Insert(ctx, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
@ -52,7 +51,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
|
return nil, fmt.Errorf(`runner token "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist)
|
||||||
}
|
}
|
||||||
return &runnerToken, nil
|
return &runnerToken, nil
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
|
|||||||
|
|
||||||
// Loop through each schedule row
|
// Loop through each schedule row
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
row.Title, _ = util.SplitStringAtByteN(row.Title, 255)
|
row.Title = util.EllipsisDisplayString(row.Title, 255)
|
||||||
// Create new schedule row
|
// Create new schedule row
|
||||||
if err = db.Insert(ctx, row); err != nil {
|
if err = db.Insert(ctx, row); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -298,7 +298,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if len(workflowJob.Steps) > 0 {
|
if len(workflowJob.Steps) > 0 {
|
||||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||||
for i, v := range workflowJob.Steps {
|
for i, v := range workflowJob.Steps {
|
||||||
name, _ := util.SplitStringAtByteN(v.String(), 255)
|
name := util.EllipsisDisplayString(v.String(), 255)
|
||||||
steps[i] = &ActionTaskStep{
|
steps[i] = &ActionTaskStep{
|
||||||
Name: name,
|
Name: name,
|
||||||
TaskID: task.ID,
|
TaskID: task.ID,
|
||||||
|
@ -20,12 +20,12 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
@ -226,7 +226,7 @@ func (a *Action) GetActUserName(ctx context.Context) string {
|
|||||||
// ShortActUserName gets the action's user name trimmed to max 20
|
// ShortActUserName gets the action's user name trimmed to max 20
|
||||||
// chars.
|
// chars.
|
||||||
func (a *Action) ShortActUserName(ctx context.Context) string {
|
func (a *Action) ShortActUserName(ctx context.Context) string {
|
||||||
return base.EllipsisString(a.GetActUserName(ctx), 20)
|
return util.EllipsisDisplayString(a.GetActUserName(ctx), 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
||||||
@ -260,7 +260,7 @@ func (a *Action) GetRepoUserName(ctx context.Context) string {
|
|||||||
// ShortRepoUserName returns the name of the action repository owner
|
// ShortRepoUserName returns the name of the action repository owner
|
||||||
// trimmed to max 20 chars.
|
// trimmed to max 20 chars.
|
||||||
func (a *Action) ShortRepoUserName(ctx context.Context) string {
|
func (a *Action) ShortRepoUserName(ctx context.Context) string {
|
||||||
return base.EllipsisString(a.GetRepoUserName(ctx), 20)
|
return util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoName returns the name of the action repository.
|
// GetRepoName returns the name of the action repository.
|
||||||
@ -275,7 +275,7 @@ func (a *Action) GetRepoName(ctx context.Context) string {
|
|||||||
// ShortRepoName returns the name of the action repository
|
// ShortRepoName returns the name of the action repository
|
||||||
// trimmed to max 33 chars.
|
// trimmed to max 33 chars.
|
||||||
func (a *Action) ShortRepoName(ctx context.Context) string {
|
func (a *Action) ShortRepoName(ctx context.Context) string {
|
||||||
return base.EllipsisString(a.GetRepoName(ctx), 33)
|
return util.EllipsisDisplayString(a.GetRepoName(ctx), 33)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoPath returns the virtual path to the action repository.
|
// GetRepoPath returns the virtual path to the action repository.
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *
|
|||||||
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
||||||
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
||||||
And("issue.is_pull = ?", false).
|
And("issue.is_pull = ?", false).
|
||||||
And("issue.created_unix >= ?", fromTime.Unix()).
|
And(builder.Or(
|
||||||
Or("issue.closed_unix >= ?", fromTime.Unix())
|
builder.Gte{"issue.created_unix": fromTime.Unix()},
|
||||||
|
builder.Gte{"issue.closed_unix": fromTime.Unix()},
|
||||||
|
))
|
||||||
|
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
@ -96,3 +96,14 @@
|
|||||||
num_issues: 0
|
num_issues: 0
|
||||||
num_closed_issues: 0
|
num_closed_issues: 0
|
||||||
archived_unix: 0
|
archived_unix: 0
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 10
|
||||||
|
repo_id: 3
|
||||||
|
org_id: 0
|
||||||
|
name: repo3label1
|
||||||
|
color: '#112233'
|
||||||
|
exclusive: false
|
||||||
|
num_issues: 0
|
||||||
|
num_closed_issues: 0
|
||||||
|
archived_unix: 0
|
||||||
|
@ -49,9 +49,13 @@ func TestCreateIssueDependency(t *testing.T) {
|
|||||||
assert.False(t, left)
|
assert.False(t, left)
|
||||||
|
|
||||||
// Close #2 and check again
|
// Close #2 and check again
|
||||||
_, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true)
|
_, err = issues_model.CloseIssue(db.DefaultContext, issue2, user1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
issue2Closed, err := issues_model.GetIssueByID(db.DefaultContext, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, issue2Closed.IsClosed)
|
||||||
|
|
||||||
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
|
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, left)
|
assert.True(t, left)
|
||||||
@ -59,4 +63,11 @@ func TestCreateIssueDependency(t *testing.T) {
|
|||||||
// Test removing the dependency
|
// Test removing the dependency
|
||||||
err = issues_model.RemoveIssueDependency(db.DefaultContext, user1, issue1, issue2, issues_model.DependencyTypeBlockedBy)
|
err = issues_model.RemoveIssueDependency(db.DefaultContext, user1, issue1, issue2, issues_model.DependencyTypeBlockedBy)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = issues_model.ReopenIssue(db.DefaultContext, issue2, user1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
issue2Reopened, err := issues_model.GetIssueByID(db.DefaultContext, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, issue2Reopened.IsClosed)
|
||||||
}
|
}
|
||||||
|
@ -119,8 +119,8 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeIssueStatus changes issue status to open or closed.
|
// CloseIssue changes issue status to closed.
|
||||||
func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) {
|
func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) {
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -128,7 +128,45 @@ func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return changeIssueStatus(ctx, issue, doer, isClosed, false)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
comment, err := changeIssueStatus(ctx, issue, doer, true, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := committer.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReopenIssue changes issue status to open.
|
||||||
|
func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) {
|
||||||
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := issue.LoadPoster(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
comment, err := changeIssueStatus(ctx, issue, doer, false, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := committer.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeIssueTitle changes the title of this issue, as the given user.
|
// ChangeIssueTitle changes the title of this issue, as the given user.
|
||||||
@ -139,7 +177,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User,
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
|
if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
|
||||||
return fmt.Errorf("updateIssueCols: %w", err)
|
return fmt.Errorf("updateIssueCols: %w", err)
|
||||||
}
|
}
|
||||||
@ -402,7 +440,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
@ -98,7 +98,7 @@ func TestXRef_ResolveCrossReferences(t *testing.T) {
|
|||||||
i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||||
i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
|
i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
|
||||||
i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
|
i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
|
||||||
_, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true)
|
_, err := issues_model.CloseIssue(db.DefaultContext, i3, d)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
|
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
|
||||||
|
@ -349,6 +349,17 @@ func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []st
|
|||||||
Find(&labelIDs)
|
Find(&labelIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given org.
|
||||||
|
func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) {
|
||||||
|
labelIDs := make([]int64, 0, len(labelNames))
|
||||||
|
return labelIDs, db.GetEngine(ctx).Table("label").
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
In("name", labelNames).
|
||||||
|
Asc("name").
|
||||||
|
Cols("id").
|
||||||
|
Find(&labelIDs)
|
||||||
|
}
|
||||||
|
|
||||||
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
|
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
|
||||||
func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
|
func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
|
||||||
return builder.Select("issue_label.issue_id").
|
return builder.Select("issue_label.issue_id").
|
||||||
|
@ -572,7 +572,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
@ -256,7 +256,7 @@ func NewProject(ctx context.Context, p *Project) error {
|
|||||||
return util.NewInvalidArgumentErrorf("project type is not valid")
|
return util.NewInvalidArgumentErrorf("project type is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Title, _ = util.SplitStringAtByteN(p.Title, 255)
|
p.Title = util.EllipsisDisplayString(p.Title, 255)
|
||||||
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err := db.Insert(ctx, p); err != nil {
|
if err := db.Insert(ctx, p); err != nil {
|
||||||
@ -311,7 +311,7 @@ func UpdateProject(ctx context.Context, p *Project) error {
|
|||||||
p.CardType = CardTypeTextOnly
|
p.CardType = CardTypeTextOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Title, _ = util.SplitStringAtByteN(p.Title, 255)
|
p.Title = util.EllipsisDisplayString(p.Title, 255)
|
||||||
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||||
"title",
|
"title",
|
||||||
"description",
|
"description",
|
||||||
|
@ -156,7 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er
|
|||||||
|
|
||||||
// UpdateRelease updates all columns of a release
|
// UpdateRelease updates all columns of a release
|
||||||
func UpdateRelease(ctx context.Context, rel *Release) error {
|
func UpdateRelease(ctx context.Context, rel *Release) error {
|
||||||
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
rel.Title = util.EllipsisDisplayString(rel.Title, 255)
|
||||||
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
|
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
|
|||||||
if user := GetVerifyUser(ctx, code); user != nil {
|
if user := GetVerifyUser(ctx, code); user != nil {
|
||||||
// time limit code
|
// time limit code
|
||||||
prefix := code[:base.TimeLimitCodeLength]
|
prefix := code[:base.TimeLimitCodeLength]
|
||||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
|
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
|
||||||
|
data := makeTimeLimitCodeHashData(opts, user)
|
||||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||||
@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
|||||||
|
|
||||||
// Activate/deactivate a user's primary email address and account
|
// Activate/deactivate a user's primary email address and account
|
||||||
if addr.IsPrimary {
|
if addr.IsPrimary {
|
||||||
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
|
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !exist {
|
} else if !exist || !strings.EqualFold(user.Email, email) {
|
||||||
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() {
|
|||||||
u.MaxRepoCreation = -1
|
u.MaxRepoCreation = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Organization does not need email
|
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
|
||||||
|
// This trick could be removed in new releases to display the user inputed email as-is.
|
||||||
u.Email = strings.ToLower(u.Email)
|
u.Email = strings.ToLower(u.Email)
|
||||||
if !u.IsOrganization() {
|
if !u.IsOrganization() {
|
||||||
if len(u.AvatarEmail) == 0 {
|
if len(u.AvatarEmail) == 0 {
|
||||||
@ -190,9 +191,9 @@ func (u *User) BeforeUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.LowerName = strings.ToLower(u.Name)
|
u.LowerName = strings.ToLower(u.Name)
|
||||||
u.Location = base.TruncateString(u.Location, 255)
|
u.Location = util.TruncateRunes(u.Location, 255)
|
||||||
u.Website = base.TruncateString(u.Website, 255)
|
u.Website = util.TruncateRunes(u.Website, 255)
|
||||||
u.Description = base.TruncateString(u.Description, 255)
|
u.Description = util.TruncateRunes(u.Description, 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterLoad is invoked from XORM after filling all the fields of this object.
|
// AfterLoad is invoked from XORM after filling all the fields of this object.
|
||||||
@ -310,17 +311,6 @@ func (u *User) OrganisationLink() string {
|
|||||||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
|
|
||||||
func (u *User) GenerateEmailActivateCode(email string) string {
|
|
||||||
code := base.CreateTimeLimitCode(
|
|
||||||
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
|
|
||||||
setting.Service.ActiveCodeLives, time.Now(), nil)
|
|
||||||
|
|
||||||
// Add tail hex username
|
|
||||||
code += hex.EncodeToString([]byte(u.LowerName))
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserFollowers returns range of user's followers.
|
// GetUserFollowers returns range of user's followers.
|
||||||
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
||||||
sess := db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
@ -501,9 +491,9 @@ func (u *User) GitName() string {
|
|||||||
// ShortName ellipses username to length
|
// ShortName ellipses username to length
|
||||||
func (u *User) ShortName(length int) string {
|
func (u *User) ShortName(length int) string {
|
||||||
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
|
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
|
||||||
return base.EllipsisString(u.FullName, length)
|
return util.EllipsisDisplayString(u.FullName, length)
|
||||||
}
|
}
|
||||||
return base.EllipsisString(u.Name, length)
|
return util.EllipsisDisplayString(u.Name, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMailable checks if a user is eligible
|
// IsMailable checks if a user is eligible
|
||||||
@ -863,12 +853,38 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyUserActiveCode verifies active code when active account
|
type TimeLimitCodePurpose string
|
||||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
|
||||||
|
const (
|
||||||
|
TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account"
|
||||||
|
TimeLimitCodeActivateEmail TimeLimitCodePurpose = "activate_email"
|
||||||
|
TimeLimitCodeResetPassword TimeLimitCodePurpose = "reset_password"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeLimitCodeOptions struct {
|
||||||
|
Purpose TimeLimitCodePurpose
|
||||||
|
NewEmail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTimeLimitCodeHashData(opts *TimeLimitCodeOptions, u *User) string {
|
||||||
|
return fmt.Sprintf("%s|%d|%s|%s|%s|%s", opts.Purpose, u.ID, strings.ToLower(util.IfZero(opts.NewEmail, u.Email)), u.LowerName, u.Passwd, u.Rands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail.
|
||||||
|
// TODO: need to use cache or db to store it to make sure a code can only be consumed once
|
||||||
|
func GenerateUserTimeLimitCode(opts *TimeLimitCodeOptions, u *User) string {
|
||||||
|
data := makeTimeLimitCodeHashData(opts, u)
|
||||||
|
code := base.CreateTimeLimitCode(data, setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||||
|
code += hex.EncodeToString([]byte(u.LowerName)) // Add tail hex username
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyUserTimeLimitCode verifies the time-limit code
|
||||||
|
func VerifyUserTimeLimitCode(ctx context.Context, opts *TimeLimitCodeOptions, code string) (user *User) {
|
||||||
if user = GetVerifyUser(ctx, code); user != nil {
|
if user = GetVerifyUser(ctx, code); user != nil {
|
||||||
// time limit code
|
// time limit code
|
||||||
prefix := code[:base.TimeLimitCodeLength]
|
prefix := code[:base.TimeLimitCodeLength]
|
||||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
data := makeTimeLimitCodeHashData(opts, user)
|
||||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
@ -35,7 +35,7 @@ func EncodeSha256(str string) string {
|
|||||||
// ShortSha is basically just truncating.
|
// ShortSha is basically just truncating.
|
||||||
// It is DEPRECATED and will be removed in the future.
|
// It is DEPRECATED and will be removed in the future.
|
||||||
func ShortSha(sha1 string) string {
|
func ShortSha(sha1 string) string {
|
||||||
return TruncateString(sha1, 10)
|
return util.TruncateRunes(sha1, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicAuthDecode decode basic auth string
|
// BasicAuthDecode decode basic auth string
|
||||||
@ -116,27 +116,6 @@ func FileSize(s int64) string {
|
|||||||
return humanize.IBytes(uint64(s))
|
return humanize.IBytes(uint64(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// EllipsisString returns a truncated short string,
|
|
||||||
// it appends '...' in the end of the length of string is too large.
|
|
||||||
func EllipsisString(str string, length int) string {
|
|
||||||
if length <= 3 {
|
|
||||||
return "..."
|
|
||||||
}
|
|
||||||
if utf8.RuneCountInString(str) <= length {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return string([]rune(str)[:length-3]) + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateString returns a truncated string with given limit,
|
|
||||||
// it returns input string if length is not reached limit.
|
|
||||||
func TruncateString(str string, limit int) string {
|
|
||||||
if utf8.RuneCountInString(str) < limit {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return string([]rune(str)[:limit])
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringsToInt64s converts a slice of string to a slice of int64.
|
// StringsToInt64s converts a slice of string to a slice of int64.
|
||||||
func StringsToInt64s(strs []string) ([]int64, error) {
|
func StringsToInt64s(strs []string) ([]int64, error) {
|
||||||
if strs == nil {
|
if strs == nil {
|
||||||
|
@ -113,36 +113,6 @@ func TestFileSize(t *testing.T) {
|
|||||||
assert.Equal(t, "2.0 EiB", FileSize(size))
|
assert.Equal(t, "2.0 EiB", FileSize(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEllipsisString(t *testing.T) {
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 0))
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 1))
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 2))
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 3))
|
|
||||||
assert.Equal(t, "f...", EllipsisString("foobar", 4))
|
|
||||||
assert.Equal(t, "fo...", EllipsisString("foobar", 5))
|
|
||||||
assert.Equal(t, "foobar", EllipsisString("foobar", 6))
|
|
||||||
assert.Equal(t, "foobar", EllipsisString("foobar", 10))
|
|
||||||
assert.Equal(t, "测...", EllipsisString("测试文本一二三四", 4))
|
|
||||||
assert.Equal(t, "测试...", EllipsisString("测试文本一二三四", 5))
|
|
||||||
assert.Equal(t, "测试文...", EllipsisString("测试文本一二三四", 6))
|
|
||||||
assert.Equal(t, "测试文本一二三四", EllipsisString("测试文本一二三四", 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTruncateString(t *testing.T) {
|
|
||||||
assert.Equal(t, "", TruncateString("foobar", 0))
|
|
||||||
assert.Equal(t, "f", TruncateString("foobar", 1))
|
|
||||||
assert.Equal(t, "fo", TruncateString("foobar", 2))
|
|
||||||
assert.Equal(t, "foo", TruncateString("foobar", 3))
|
|
||||||
assert.Equal(t, "foob", TruncateString("foobar", 4))
|
|
||||||
assert.Equal(t, "fooba", TruncateString("foobar", 5))
|
|
||||||
assert.Equal(t, "foobar", TruncateString("foobar", 6))
|
|
||||||
assert.Equal(t, "foobar", TruncateString("foobar", 7))
|
|
||||||
assert.Equal(t, "测试文本", TruncateString("测试文本一二三四", 4))
|
|
||||||
assert.Equal(t, "测试文本一", TruncateString("测试文本一二三四", 5))
|
|
||||||
assert.Equal(t, "测试文本一二", TruncateString("测试文本一二三四", 6))
|
|
||||||
assert.Equal(t, "测试文本一二三", TruncateString("测试文本一二三四", 7))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringsToInt64s(t *testing.T) {
|
func TestStringsToInt64s(t *testing.T) {
|
||||||
testSuccess := func(input []string, expected []int64) {
|
testSuccess := func(input []string, expected []int64) {
|
||||||
result, err := StringsToInt64s(input)
|
result, err := StringsToInt64s(input)
|
||||||
|
@ -109,7 +109,7 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
|
|||||||
|
|
||||||
it.Content = string(content)
|
it.Content = string(content)
|
||||||
it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath!
|
it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath!
|
||||||
it.About, _ = util.SplitStringAtByteN(it.Content, 80)
|
it.About = util.EllipsisDisplayString(it.Content, 80)
|
||||||
} else {
|
} else {
|
||||||
it.Content = templateBody
|
it.Content = templateBody
|
||||||
if it.About == "" {
|
if it.About == "" {
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/markup/common"
|
"code.gitea.io/gitea/modules/markup/common"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
"golang.org/x/net/html/atom"
|
"golang.org/x/net/html/atom"
|
||||||
@ -171,6 +172,10 @@ func linkProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uri := node.Data[m[0]:m[1]]
|
uri := node.Data[m[0]:m[1]]
|
||||||
|
remaining := node.Data[m[1]:]
|
||||||
|
if util.IsLikelyEllipsisLeftPart(remaining) {
|
||||||
|
return
|
||||||
|
}
|
||||||
replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
|
replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
|
||||||
node = node.NextSibling.NextSibling
|
node = node.NextSibling.NextSibling
|
||||||
}
|
}
|
||||||
|
@ -206,6 +206,16 @@ func TestRender_links(t *testing.T) {
|
|||||||
test(
|
test(
|
||||||
"ftps://gitea.com",
|
"ftps://gitea.com",
|
||||||
`<p>ftps://gitea.com</p>`)
|
`<p>ftps://gitea.com</p>`)
|
||||||
|
|
||||||
|
t.Run("LinkEllipsis", func(t *testing.T) {
|
||||||
|
input := util.EllipsisDisplayString("http://10.1.2.3", 12)
|
||||||
|
assert.Equal(t, "http://10…", input)
|
||||||
|
test(input, "<p>http://10…</p>")
|
||||||
|
|
||||||
|
input = util.EllipsisDisplayString("http://10.1.2.3", 13)
|
||||||
|
assert.Equal(t, "http://10.…", input)
|
||||||
|
test(input, "<p>http://10.…</p>")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_email(t *testing.T) {
|
func TestRender_email(t *testing.T) {
|
||||||
|
@ -63,6 +63,7 @@ var UI = struct {
|
|||||||
} `ini:"ui.admin"`
|
} `ini:"ui.admin"`
|
||||||
User struct {
|
User struct {
|
||||||
RepoPagingNum int
|
RepoPagingNum int
|
||||||
|
OrgPagingNum int
|
||||||
} `ini:"ui.user"`
|
} `ini:"ui.user"`
|
||||||
Meta struct {
|
Meta struct {
|
||||||
Author string
|
Author string
|
||||||
@ -127,8 +128,10 @@ var UI = struct {
|
|||||||
},
|
},
|
||||||
User: struct {
|
User: struct {
|
||||||
RepoPagingNum int
|
RepoPagingNum int
|
||||||
|
OrgPagingNum int
|
||||||
}{
|
}{
|
||||||
RepoPagingNum: 15,
|
RepoPagingNum: 15,
|
||||||
|
OrgPagingNum: 15,
|
||||||
},
|
},
|
||||||
Meta: struct {
|
Meta: struct {
|
||||||
Author string
|
Author string
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
texttmpl "text/template"
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||||
@ -24,7 +24,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
|||||||
"dict": dict,
|
"dict": dict,
|
||||||
"Eval": evalTokens,
|
"Eval": evalTokens,
|
||||||
|
|
||||||
"EllipsisString": base.EllipsisString,
|
"EllipsisString": util.EllipsisDisplayString,
|
||||||
"AppName": func() string {
|
"AppName": func() string {
|
||||||
return setting.AppName
|
return setting.AppName
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StringUtils struct{}
|
type StringUtils struct{}
|
||||||
@ -54,7 +54,7 @@ func (su *StringUtils) Cut(s, sep string) []any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (su *StringUtils) EllipsisString(s string, maxLength int) string {
|
func (su *StringUtils) EllipsisString(s string, maxLength int) string {
|
||||||
return base.EllipsisString(s, maxLength)
|
return util.EllipsisDisplayString(s, maxLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (su *StringUtils) ToUpper(s string) string {
|
func (su *StringUtils) ToUpper(s string) string {
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import "unsafe"
|
import (
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
func isSnakeCaseUpper(c byte) bool {
|
func isSnakeCaseUpper(c byte) bool {
|
||||||
return 'A' <= c && c <= 'Z'
|
return 'A' <= c && c <= 'Z'
|
||||||
@ -95,3 +98,15 @@ func UnsafeBytesToString(b []byte) string {
|
|||||||
func UnsafeStringToBytes(s string) []byte {
|
func UnsafeStringToBytes(s string) []byte {
|
||||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SplitTrimSpace splits the string at given separator and trims leading and trailing space
|
||||||
|
func SplitTrimSpace(input, sep string) []string {
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
var stringList []string
|
||||||
|
for _, s := range strings.Split(input, sep) {
|
||||||
|
if s = strings.TrimSpace(s); s != "" {
|
||||||
|
stringList = append(stringList, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stringList
|
||||||
|
}
|
||||||
|
@ -45,3 +45,8 @@ func TestToSnakeCase(t *testing.T) {
|
|||||||
assert.Equal(t, expected, ToSnakeCase(input))
|
assert.Equal(t, expected, ToSnakeCase(input))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSplitTrimSpace(t *testing.T) {
|
||||||
|
assert.Equal(t, []string{"a", "b", "c"}, SplitTrimSpace("a\nb\nc", "\n"))
|
||||||
|
assert.Equal(t, []string{"a", "b"}, SplitTrimSpace("\r\na\n\r\nb\n\n", "\n"))
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,43 +15,110 @@ const (
|
|||||||
asciiEllipsis = "..."
|
asciiEllipsis = "..."
|
||||||
)
|
)
|
||||||
|
|
||||||
// SplitStringAtByteN splits a string at byte n accounting for rune boundaries. (Combining characters are not accounted for.)
|
func IsLikelyEllipsisLeftPart(s string) bool {
|
||||||
func SplitStringAtByteN(input string, n int) (left, right string) {
|
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
|
||||||
if len(input) <= n {
|
}
|
||||||
return input, ""
|
|
||||||
|
func ellipsisGuessDisplayWidth(r rune) int {
|
||||||
|
// To make the truncated string as long as possible,
|
||||||
|
// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
|
||||||
|
// Here we only make the best guess (better than counting them in bytes),
|
||||||
|
// it's impossible to 100% correctly determine the width of a rune without a real font and render.
|
||||||
|
//
|
||||||
|
// ATTENTION: the guessed width can't be zero, more details in ellipsisDisplayString's comment
|
||||||
|
if r <= 255 {
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utf8.ValidString(input) {
|
switch {
|
||||||
if n-3 < 0 {
|
case r == '\u3000': /* ideographic (CJK) characters, still use 2 */
|
||||||
return input, ""
|
return 2
|
||||||
|
case unicode.Is(unicode.M, r), /* (Mark) */
|
||||||
|
unicode.Is(unicode.Cf, r), /* (Other, format) */
|
||||||
|
unicode.Is(unicode.Cs, r), /* (Other, surrogate) */
|
||||||
|
unicode.Is(unicode.Z /* (Space) */, r):
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EllipsisDisplayString returns a truncated short string for display purpose.
|
||||||
|
// The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width)
|
||||||
|
// It appends "…" or "..." at the end of truncated string.
|
||||||
|
// It guarantees the length of the returned runes doesn't exceed the limit.
|
||||||
|
func EllipsisDisplayString(str string, limit int) string {
|
||||||
|
s, _, _, _ := ellipsisDisplayString(str, limit)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
|
||||||
|
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
|
||||||
|
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit)
|
||||||
|
if truncated {
|
||||||
|
right = str[offset:]
|
||||||
|
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
|
||||||
|
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
||||||
|
ellipsis := utf8Ellipsis
|
||||||
|
if encounterInvalid {
|
||||||
|
ellipsis = asciiEllipsis
|
||||||
}
|
}
|
||||||
return input[:n-3] + asciiEllipsis, asciiEllipsis + input[n-3:]
|
right = ellipsis + right
|
||||||
|
}
|
||||||
|
return left, right
|
||||||
|
}
|
||||||
|
|
||||||
|
func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) {
|
||||||
|
if len(str) <= limit {
|
||||||
|
return str, len(str), false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
end := 0
|
// To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit,
|
||||||
for end <= n-3 {
|
// because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters,
|
||||||
_, size := utf8.DecodeRuneInString(input[end:])
|
// So each rune must be countered as at least 1 width.
|
||||||
if end+size > n-3 {
|
// Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero.
|
||||||
|
pos, used := 0, 0
|
||||||
|
for i, r := range str {
|
||||||
|
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
||||||
|
pos = i
|
||||||
|
runeWidth := ellipsisGuessDisplayWidth(r)
|
||||||
|
if used+runeWidth+3 > limit {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
end += size
|
used += runeWidth
|
||||||
|
offset += utf8.RuneLen(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
|
// if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse
|
||||||
}
|
if len(str)-pos <= 12 {
|
||||||
|
var nextCnt, nextWidth int
|
||||||
// SplitTrimSpace splits the string at given separator and trims leading and trailing space
|
for _, r := range str[pos:] {
|
||||||
func SplitTrimSpace(input, sep string) []string {
|
if nextCnt >= 4 {
|
||||||
// Trim initial leading & trailing space
|
break
|
||||||
input = strings.TrimSpace(input)
|
}
|
||||||
// replace CRLF with LF
|
nextWidth += ellipsisGuessDisplayWidth(r)
|
||||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
nextCnt++
|
||||||
|
}
|
||||||
var stringList []string
|
if nextCnt <= 3 && used+nextWidth <= limit {
|
||||||
for _, s := range strings.Split(input, sep) {
|
return str, len(str), false, false
|
||||||
// trim leading and trailing space
|
}
|
||||||
stringList = append(stringList, strings.TrimSpace(s))
|
|
||||||
}
|
}
|
||||||
|
if limit < 3 {
|
||||||
return stringList
|
// if the limit is so small, do not add ellipsis
|
||||||
|
return str[:offset], offset, true, false
|
||||||
|
}
|
||||||
|
ellipsis := utf8Ellipsis
|
||||||
|
if encounterInvalid {
|
||||||
|
ellipsis = asciiEllipsis
|
||||||
|
}
|
||||||
|
return str[:offset] + ellipsis, offset, true, encounterInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateRunes returns a truncated string with given rune limit,
|
||||||
|
// it returns input string if its rune length doesn't exceed the limit.
|
||||||
|
func TruncateRunes(str string, limit int) string {
|
||||||
|
if utf8.RuneCountInString(str) < limit {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return string([]rune(str)[:limit])
|
||||||
}
|
}
|
||||||
|
@ -4,43 +4,127 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSplitString(t *testing.T) {
|
func TestEllipsisGuessDisplayWidth(t *testing.T) {
|
||||||
type testCase struct {
|
cases := []struct {
|
||||||
input string
|
r string
|
||||||
n int
|
want int
|
||||||
leftSub string
|
}{
|
||||||
ellipsis string
|
{r: "a", want: 1},
|
||||||
|
{r: "é", want: 1},
|
||||||
|
{r: "测", want: 2},
|
||||||
|
{r: "⚽", want: 2},
|
||||||
|
{r: "☁️", want: 3}, // 2 runes, it has a mark
|
||||||
|
{r: "\u200B", want: 1}, // ZWSP
|
||||||
|
{r: "\u3000", want: 2}, // ideographic space
|
||||||
}
|
}
|
||||||
|
for _, c := range cases {
|
||||||
test := func(tc []*testCase, f func(input string, n int) (left, right string)) {
|
t.Run(c.r, func(t *testing.T) {
|
||||||
for _, c := range tc {
|
w := 0
|
||||||
l, r := f(c.input, c.n)
|
for _, r := range c.r {
|
||||||
if c.ellipsis != "" {
|
w += ellipsisGuessDisplayWidth(r)
|
||||||
assert.Equal(t, c.leftSub+c.ellipsis, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub)
|
|
||||||
assert.Equal(t, c.ellipsis+c.input[len(c.leftSub):], r, "test split %s at %d, expected rightSub: %q", c.input, c.n, c.input[len(c.leftSub):])
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, c.leftSub, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub)
|
|
||||||
assert.Empty(t, r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "")
|
|
||||||
}
|
}
|
||||||
}
|
assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEllipsisString(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
limit int
|
||||||
|
|
||||||
|
input, left, right string
|
||||||
|
}{
|
||||||
|
{limit: 0, input: "abcde", left: "", right: "…abcde"},
|
||||||
|
{limit: 1, input: "abcde", left: "", right: "…abcde"},
|
||||||
|
{limit: 2, input: "abcde", left: "", right: "…abcde"},
|
||||||
|
{limit: 3, input: "abcde", left: "…", right: "…abcde"},
|
||||||
|
{limit: 4, input: "abcde", left: "a…", right: "…bcde"},
|
||||||
|
{limit: 5, input: "abcde", left: "abcde", right: ""},
|
||||||
|
{limit: 6, input: "abcde", left: "abcde", right: ""},
|
||||||
|
{limit: 7, input: "abcde", left: "abcde", right: ""},
|
||||||
|
|
||||||
|
// a CJK char or emoji is considered as 2-ASCII width, the ellipsis is 3-ASCII width
|
||||||
|
{limit: 0, input: "测试文本", left: "", right: "…测试文本"},
|
||||||
|
{limit: 1, input: "测试文本", left: "", right: "…测试文本"},
|
||||||
|
{limit: 2, input: "测试文本", left: "", right: "…测试文本"},
|
||||||
|
{limit: 3, input: "测试文本", left: "…", right: "…测试文本"},
|
||||||
|
{limit: 4, input: "测试文本", left: "…", right: "…测试文本"},
|
||||||
|
{limit: 5, input: "测试文本", left: "测…", right: "…试文本"},
|
||||||
|
{limit: 6, input: "测试文本", left: "测…", right: "…试文本"},
|
||||||
|
{limit: 7, input: "测试文本", left: "测试…", right: "…文本"},
|
||||||
|
{limit: 8, input: "测试文本", left: "测试文本", right: ""},
|
||||||
|
{limit: 9, input: "测试文本", left: "测试文本", right: ""},
|
||||||
|
|
||||||
|
{limit: 6, input: "测试abc", left: "测…", right: "…试abc"},
|
||||||
|
{limit: 7, input: "测试abc", left: "测试abc", right: ""}, // exactly 7-width
|
||||||
|
{limit: 8, input: "测试abc", left: "测试abc", right: ""},
|
||||||
|
|
||||||
|
{limit: 7, input: "测abc试啊", left: "测ab…", right: "…c试啊"},
|
||||||
|
{limit: 8, input: "测abc试啊", left: "测abc…", right: "…试啊"},
|
||||||
|
{limit: 9, input: "测abc试啊", left: "测abc试啊", right: ""}, // exactly 9-width
|
||||||
|
{limit: 10, input: "测abc试啊", left: "测abc试啊", right: ""},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(fmt.Sprintf("%s(%d)", c.input, c.limit), func(t *testing.T) {
|
||||||
|
left, right := EllipsisDisplayStringX(c.input, c.limit)
|
||||||
|
assert.Equal(t, c.left, left, "left")
|
||||||
|
assert.Equal(t, c.right, right, "right")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tc := []*testCase{
|
t.Run("LongInput", func(t *testing.T) {
|
||||||
{"abc123xyz", 0, "", utf8Ellipsis},
|
left, right := EllipsisDisplayStringX(strings.Repeat("abc", 240), 90)
|
||||||
{"abc123xyz", 1, "", utf8Ellipsis},
|
assert.Equal(t, strings.Repeat("abc", 29)+"…", left)
|
||||||
{"abc123xyz", 4, "a", utf8Ellipsis},
|
assert.Equal(t, "…"+strings.Repeat("abc", 211), right)
|
||||||
{"啊bc123xyz", 4, "", utf8Ellipsis},
|
})
|
||||||
{"啊bc123xyz", 6, "啊", utf8Ellipsis},
|
|
||||||
{"啊bc", 5, "啊bc", ""},
|
t.Run("InvalidUtf8", func(t *testing.T) {
|
||||||
{"啊bc", 6, "啊bc", ""},
|
invalidCases := []struct {
|
||||||
{"abc\xef\x03\xfe", 3, "", asciiEllipsis},
|
limit int
|
||||||
{"abc\xef\x03\xfe", 4, "a", asciiEllipsis},
|
left, right string
|
||||||
{"\xef\x03", 1, "\xef\x03", ""},
|
}{
|
||||||
}
|
{limit: 0, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
test(tc, SplitStringAtByteN)
|
{limit: 1, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 2, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 3, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 4, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 5, left: "\xef\x03\xfe...", right: "...\xef\x03\xfe"},
|
||||||
|
{limit: 6, left: "\xef\x03\xfe\xef\x03\xfe", right: ""},
|
||||||
|
{limit: 7, left: "\xef\x03\xfe\xef\x03\xfe", right: ""},
|
||||||
|
}
|
||||||
|
for _, c := range invalidCases {
|
||||||
|
t.Run(fmt.Sprintf("%d", c.limit), func(t *testing.T) {
|
||||||
|
left, right := EllipsisDisplayStringX("\xef\x03\xfe\xef\x03\xfe", c.limit)
|
||||||
|
assert.Equal(t, c.left, left, "left")
|
||||||
|
assert.Equal(t, c.right, right, "right")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("IsLikelyEllipsisLeftPart", func(t *testing.T) {
|
||||||
|
assert.True(t, IsLikelyEllipsisLeftPart("abcde…"))
|
||||||
|
assert.True(t, IsLikelyEllipsisLeftPart("abcde..."))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateRunes(t *testing.T) {
|
||||||
|
assert.Equal(t, "", TruncateRunes("", 0))
|
||||||
|
assert.Equal(t, "", TruncateRunes("", 1))
|
||||||
|
|
||||||
|
assert.Equal(t, "", TruncateRunes("ab", 0))
|
||||||
|
assert.Equal(t, "a", TruncateRunes("ab", 1))
|
||||||
|
assert.Equal(t, "ab", TruncateRunes("ab", 2))
|
||||||
|
assert.Equal(t, "ab", TruncateRunes("ab", 3))
|
||||||
|
|
||||||
|
assert.Equal(t, "", TruncateRunes("测试", 0))
|
||||||
|
assert.Equal(t, "测", TruncateRunes("测试", 1))
|
||||||
|
assert.Equal(t, "测试", TruncateRunes("测试", 2))
|
||||||
|
assert.Equal(t, "测试", TruncateRunes("测试", 3))
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,14 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
"code.gitea.io/gitea/modules/htmlutil"
|
"code.gitea.io/gitea/modules/htmlutil"
|
||||||
"code.gitea.io/gitea/modules/reqctx"
|
"code.gitea.io/gitea/modules/reqctx"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
|
||||||
"gitea.com/go-chi/binding"
|
"gitea.com/go-chi/binding"
|
||||||
@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any {
|
|||||||
|
|
||||||
// Router defines a route based on chi's router
|
// Router defines a route based on chi's router
|
||||||
type Router struct {
|
type Router struct {
|
||||||
chiRouter chi.Router
|
chiRouter *chi.Mux
|
||||||
curGroupPrefix string
|
curGroupPrefix string
|
||||||
curMiddlewares []any
|
curMiddlewares []any
|
||||||
}
|
}
|
||||||
@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool {
|
|||||||
return r.Kind() == reflect.Func && r.IsNil()
|
return r.Kind() == reflect.Func && r.IsNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
|
func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
|
||||||
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1)
|
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1)
|
||||||
for _, m := range r.curMiddlewares {
|
for _, m := range curMiddlewares {
|
||||||
if !isNilOrFuncNil(m) {
|
if !isNilOrFuncNil(m) {
|
||||||
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, m := range h {
|
if len(h) == 0 {
|
||||||
|
panic("no endpoint handler provided")
|
||||||
|
}
|
||||||
|
for i, m := range h {
|
||||||
if !isNilOrFuncNil(m) {
|
if !isNilOrFuncNil(m) {
|
||||||
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
||||||
|
} else if i == len(h)-1 {
|
||||||
|
panic("endpoint handler can't be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
middlewares := handlerProviders[:len(handlerProviders)-1]
|
middlewares := handlerProviders[:len(handlerProviders)-1]
|
||||||
@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha
|
|||||||
// Methods adds the same handlers for multiple http "methods" (separated by ",").
|
// Methods adds the same handlers for multiple http "methods" (separated by ",").
|
||||||
// If any method is invalid, the lower level router will panic.
|
// If any method is invalid, the lower level router will panic.
|
||||||
func (r *Router) Methods(methods, pattern string, h ...any) {
|
func (r *Router) Methods(methods, pattern string, h ...any) {
|
||||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
|
||||||
fullPattern := r.getPattern(pattern)
|
fullPattern := r.getPattern(pattern)
|
||||||
if strings.Contains(methods, ",") {
|
if strings.Contains(methods, ",") {
|
||||||
methods := strings.Split(methods, ",")
|
methods := strings.Split(methods, ",")
|
||||||
@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) {
|
|||||||
|
|
||||||
// Any delegate requests for all methods
|
// Any delegate requests for all methods
|
||||||
func (r *Router) Any(pattern string, h ...any) {
|
func (r *Router) Any(pattern string, h ...any) {
|
||||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
|
||||||
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
|
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) {
|
|||||||
r.chiRouter.NotFound(h)
|
r.chiRouter.NotFound(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathProcessorParam struct {
|
|
||||||
name string
|
|
||||||
captureGroup int
|
|
||||||
}
|
|
||||||
|
|
||||||
type PathProcessor struct {
|
|
||||||
methods container.Set[string]
|
|
||||||
re *regexp.Regexp
|
|
||||||
params []pathProcessorParam
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
|
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
|
||||||
normalized := false
|
normalized := false
|
||||||
normalizedPath := req.URL.EscapedPath()
|
normalizedPath := req.URL.EscapedPath()
|
||||||
@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
|
|||||||
next.ServeHTTP(resp, req)
|
next.ServeHTTP(resp, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool {
|
|
||||||
if !p.methods.Contains(chiCtx.RouteMethod) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
path = "/" + path
|
|
||||||
}
|
|
||||||
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
|
||||||
if pathMatches == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var paramMatches [][]int
|
|
||||||
for i := 2; i < len(pathMatches); {
|
|
||||||
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
|
||||||
pmIdx := len(paramMatches) - 1
|
|
||||||
end := pathMatches[i+1]
|
|
||||||
i += 2
|
|
||||||
for ; i < len(pathMatches); i += 2 {
|
|
||||||
if pathMatches[i] >= end {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, pm := range paramMatches {
|
|
||||||
groupIdx := p.params[i].captureGroup * 2
|
|
||||||
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPathProcessor(methods, pattern string) *PathProcessor {
|
|
||||||
p := &PathProcessor{methods: make(container.Set[string])}
|
|
||||||
for _, method := range strings.Split(methods, ",") {
|
|
||||||
p.methods.Add(strings.TrimSpace(method))
|
|
||||||
}
|
|
||||||
re := []byte{'^'}
|
|
||||||
lastEnd := 0
|
|
||||||
for lastEnd < len(pattern) {
|
|
||||||
start := strings.IndexByte(pattern[lastEnd:], '<')
|
|
||||||
if start == -1 {
|
|
||||||
re = append(re, pattern[lastEnd:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
|
||||||
if end == -1 {
|
|
||||||
panic(fmt.Sprintf("invalid pattern: %s", pattern))
|
|
||||||
}
|
|
||||||
re = append(re, pattern[lastEnd:lastEnd+start]...)
|
|
||||||
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
|
||||||
lastEnd += start + end + 1
|
|
||||||
|
|
||||||
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
|
||||||
// it is not used so no need to implement it now
|
|
||||||
param := pathProcessorParam{}
|
|
||||||
if partExp == "*" {
|
|
||||||
re = append(re, "(.*?)/?"...)
|
|
||||||
if lastEnd < len(pattern) {
|
|
||||||
if pattern[lastEnd] == '/' {
|
|
||||||
lastEnd++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
partExp = util.IfZero(partExp, "[^/]+")
|
|
||||||
re = append(re, '(')
|
|
||||||
re = append(re, partExp...)
|
|
||||||
re = append(re, ')')
|
|
||||||
}
|
|
||||||
param.name = partName
|
|
||||||
p.params = append(p.params, param)
|
|
||||||
}
|
|
||||||
re = append(re, '$')
|
|
||||||
reStr := string(re)
|
|
||||||
p.re = regexp.MustCompile(reStr)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combo delegates requests to Combo
|
// Combo delegates requests to Combo
|
||||||
func (r *Router) Combo(pattern string, h ...any) *Combo {
|
func (r *Router) Combo(pattern string, h ...any) *Combo {
|
||||||
return &Combo{r, pattern, h}
|
return &Combo{r, pattern, h}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combo represents a tiny group routes with same pattern
|
// PathGroup creates a group of paths which could be matched by regexp.
|
||||||
type Combo struct {
|
// It is only designed to resolve some special cases which chi router can't handle.
|
||||||
r *Router
|
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
||||||
pattern string
|
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
|
||||||
h []any
|
g := &RouterPathGroup{r: r, pathParam: "*"}
|
||||||
}
|
fn(g)
|
||||||
|
r.Any(pattern, append(h, g.ServeHTTP)...)
|
||||||
// Get delegates Get method
|
|
||||||
func (c *Combo) Get(h ...any) *Combo {
|
|
||||||
c.r.Get(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post delegates Post method
|
|
||||||
func (c *Combo) Post(h ...any) *Combo {
|
|
||||||
c.r.Post(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete delegates Delete method
|
|
||||||
func (c *Combo) Delete(h ...any) *Combo {
|
|
||||||
c.r.Delete(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put delegates Put method
|
|
||||||
func (c *Combo) Put(h ...any) *Combo {
|
|
||||||
c.r.Put(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch delegates Patch method
|
|
||||||
func (c *Combo) Patch(h ...any) *Combo {
|
|
||||||
c.r.Patch(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
}
|
41
modules/web/router_combo.go
Normal file
41
modules/web/router_combo.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
// Combo represents a tiny group routes with same pattern
|
||||||
|
type Combo struct {
|
||||||
|
r *Router
|
||||||
|
pattern string
|
||||||
|
h []any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get delegates Get method
|
||||||
|
func (c *Combo) Get(h ...any) *Combo {
|
||||||
|
c.r.Get(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post delegates Post method
|
||||||
|
func (c *Combo) Post(h ...any) *Combo {
|
||||||
|
c.r.Post(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete delegates Delete method
|
||||||
|
func (c *Combo) Delete(h ...any) *Combo {
|
||||||
|
c.r.Delete(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put delegates Put method
|
||||||
|
func (c *Combo) Put(h ...any) *Combo {
|
||||||
|
c.r.Put(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch delegates Patch method
|
||||||
|
func (c *Combo) Patch(h ...any) *Combo {
|
||||||
|
c.r.Patch(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
135
modules/web/router_path.go
Normal file
135
modules/web/router_path.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RouterPathGroup struct {
|
||||||
|
r *Router
|
||||||
|
pathParam string
|
||||||
|
matchers []*routerPathMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
chiCtx := chi.RouteContext(req.Context())
|
||||||
|
path := chiCtx.URLParam(g.pathParam)
|
||||||
|
for _, m := range g.matchers {
|
||||||
|
if m.matchPath(chiCtx, path) {
|
||||||
|
handler := m.handlerFunc
|
||||||
|
for i := len(m.middlewares) - 1; i >= 0; i-- {
|
||||||
|
handler = m.middlewares[i](handler).ServeHTTP
|
||||||
|
}
|
||||||
|
handler(resp, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchPath matches the request method, and uses regexp to match the path.
|
||||||
|
// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router)
|
||||||
|
// It is only designed to resolve some special cases which chi router can't handle.
|
||||||
|
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
||||||
|
func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
|
||||||
|
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
||||||
|
}
|
||||||
|
|
||||||
|
type routerPathParam struct {
|
||||||
|
name string
|
||||||
|
captureGroup int
|
||||||
|
}
|
||||||
|
|
||||||
|
type routerPathMatcher struct {
|
||||||
|
methods container.Set[string]
|
||||||
|
re *regexp.Regexp
|
||||||
|
params []routerPathParam
|
||||||
|
middlewares []func(http.Handler) http.Handler
|
||||||
|
handlerFunc http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
|
||||||
|
if !p.methods.Contains(chiCtx.RouteMethod) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
||||||
|
if pathMatches == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var paramMatches [][]int
|
||||||
|
for i := 2; i < len(pathMatches); {
|
||||||
|
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
||||||
|
pmIdx := len(paramMatches) - 1
|
||||||
|
end := pathMatches[i+1]
|
||||||
|
i += 2
|
||||||
|
for ; i < len(pathMatches); i += 2 {
|
||||||
|
if pathMatches[i] >= end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, pm := range paramMatches {
|
||||||
|
groupIdx := p.params[i].captureGroup * 2
|
||||||
|
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher {
|
||||||
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h)
|
||||||
|
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
|
||||||
|
for _, method := range strings.Split(methods, ",") {
|
||||||
|
p.methods.Add(strings.TrimSpace(method))
|
||||||
|
}
|
||||||
|
re := []byte{'^'}
|
||||||
|
lastEnd := 0
|
||||||
|
for lastEnd < len(pattern) {
|
||||||
|
start := strings.IndexByte(pattern[lastEnd:], '<')
|
||||||
|
if start == -1 {
|
||||||
|
re = append(re, pattern[lastEnd:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
||||||
|
if end == -1 {
|
||||||
|
panic(fmt.Sprintf("invalid pattern: %s", pattern))
|
||||||
|
}
|
||||||
|
re = append(re, pattern[lastEnd:lastEnd+start]...)
|
||||||
|
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
||||||
|
lastEnd += start + end + 1
|
||||||
|
|
||||||
|
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
||||||
|
// it is not used so no need to implement it now
|
||||||
|
param := routerPathParam{}
|
||||||
|
if partExp == "*" {
|
||||||
|
re = append(re, "(.*?)/?"...)
|
||||||
|
if lastEnd < len(pattern) && pattern[lastEnd] == '/' {
|
||||||
|
lastEnd++ // the "*" pattern is able to handle the last slash, so skip it
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partExp = util.IfZero(partExp, "[^/]+")
|
||||||
|
re = append(re, '(')
|
||||||
|
re = append(re, partExp...)
|
||||||
|
re = append(re, ')')
|
||||||
|
}
|
||||||
|
param.name = partName
|
||||||
|
p.params = append(p.params, param)
|
||||||
|
}
|
||||||
|
re = append(re, '$')
|
||||||
|
reStr := string(re)
|
||||||
|
p.re = regexp.MustCompile(reStr)
|
||||||
|
return p
|
||||||
|
}
|
@ -27,17 +27,21 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string {
|
|||||||
}
|
}
|
||||||
m[key] = pathParams.Values[i]
|
m[key] = pathParams.Values[i]
|
||||||
}
|
}
|
||||||
return m
|
return util.Iif(len(m) == 0, nil, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathProcessor(t *testing.T) {
|
func TestPathProcessor(t *testing.T) {
|
||||||
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.RouteMethod = "GET"
|
chiCtx.RouteMethod = "GET"
|
||||||
p := NewPathProcessor("GET", pattern)
|
p := newRouterPathMatcher("GET", pattern, http.NotFound)
|
||||||
assert.True(t, p.ProcessRequestPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
||||||
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the "<...>" is intentionally designed to distinguish from chi's path parameters, because:
|
||||||
|
// 1. their behaviors are totally different, we do not want to mislead developers
|
||||||
|
// 2. we can write regexp in "<name:\w{3,4}>" easily and parse it easily
|
||||||
testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"})
|
testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"})
|
||||||
testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path
|
testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path
|
||||||
testProcess("/<p1:*>", "/", map[string]string{"p1": ""})
|
testProcess("/<p1:*>", "/", map[string]string{"p1": ""})
|
||||||
@ -67,24 +71,31 @@ func TestRouter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
mark := util.OptionalArg(optMark, "")
|
||||||
|
return func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) {
|
||||||
|
h(stop)(resp, req)
|
||||||
|
resp.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r := NewRouter()
|
r := NewRouter()
|
||||||
|
r.NotFound(h("not-found:/"))
|
||||||
r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called
|
r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called
|
||||||
r.Group("/{username}/{reponame}", func() {
|
r.Group("/{username}/{reponame}", func() {
|
||||||
r.Get("/{type:issues|pulls}", h("list-issues-b"))
|
r.Get("/{type:issues|pulls}", h("list-issues-b"))
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
r.Get("/{type:issues|pulls}/{index}", h("view-issue"))
|
r.Get("/{type:issues|pulls}/{index}", h("view-issue"))
|
||||||
}, func(resp http.ResponseWriter, req *http.Request) {
|
}, stopMark())
|
||||||
if stop := req.FormValue("stop"); stop != "" {
|
|
||||||
h(stop)(resp, req)
|
|
||||||
resp.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
r.Group("/issues/{index}", func() {
|
r.Group("/issues/{index}", func() {
|
||||||
r.Post("/update", h("update-issue"))
|
r.Post("/update", h("update-issue"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
m := NewRouter()
|
m := NewRouter()
|
||||||
|
m.NotFound(h("not-found:/api/v1"))
|
||||||
r.Mount("/api/v1", m)
|
r.Mount("/api/v1", m)
|
||||||
m.Group("/repos", func() {
|
m.Group("/repos", func() {
|
||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
@ -96,11 +107,14 @@ func TestRouter(t *testing.T) {
|
|||||||
m.Patch("", h())
|
m.Patch("", h())
|
||||||
m.Delete("", h())
|
m.Delete("", h())
|
||||||
})
|
})
|
||||||
|
m.PathGroup("/*", func(g *RouterPathGroup) {
|
||||||
|
g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path"))
|
||||||
|
}, stopMark("s1"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute := func(methodPath string, expected resultStruct) {
|
testRoute := func(t *testing.T, methodPath string, expected resultStruct) {
|
||||||
t.Run(methodPath, func(t *testing.T) {
|
t.Run(methodPath, func(t *testing.T) {
|
||||||
res = resultStruct{}
|
res = resultStruct{}
|
||||||
methodPathFields := strings.Fields(methodPath)
|
methodPathFields := strings.Fields(methodPath)
|
||||||
@ -111,24 +125,24 @@ func TestRouter(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Root Router", func(t *testing.T) {
|
t.Run("RootRouter", func(t *testing.T) {
|
||||||
testRoute("GET /the-user/the-repo/other", resultStruct{})
|
testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"})
|
||||||
testRoute("GET /the-user/the-repo/pulls", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
||||||
handlerMark: "list-issues-b",
|
handlerMark: "list-issues-b",
|
||||||
})
|
})
|
||||||
testRoute("GET /the-user/the-repo/issues/123", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||||
handlerMark: "view-issue",
|
handlerMark: "view-issue",
|
||||||
})
|
})
|
||||||
testRoute("GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||||
handlerMark: "hijack",
|
handlerMark: "hijack",
|
||||||
})
|
})
|
||||||
testRoute("POST /the-user/the-repo/issues/123/update", resultStruct{
|
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
||||||
handlerMark: "update-issue",
|
handlerMark: "update-issue",
|
||||||
@ -136,31 +150,57 @@ func TestRouter(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Sub Router", func(t *testing.T) {
|
t.Run("Sub Router", func(t *testing.T) {
|
||||||
testRoute("GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"})
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("POST /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("MatchPath", func(t *testing.T) {
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||||
|
handlerMark: "match-path",
|
||||||
|
})
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
|
||||||
|
handlerMark: "not-found:/api/v1",
|
||||||
|
})
|
||||||
|
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
|
||||||
|
handlerMark: "s1",
|
||||||
|
})
|
||||||
|
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||||
|
handlerMark: "s2",
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouteNormalizePath(t *testing.T) {
|
func TestRouteNormalizePath(t *testing.T) {
|
@ -647,6 +647,7 @@ joined_on=Přidal/a se %s
|
|||||||
repositories=Repozitáře
|
repositories=Repozitáře
|
||||||
activity=Veřejná aktivita
|
activity=Veřejná aktivita
|
||||||
followers=Sledující
|
followers=Sledující
|
||||||
|
show_more=Zobrazit více
|
||||||
starred=Oblíbené repozitáře
|
starred=Oblíbené repozitáře
|
||||||
watched=Sledované repozitáře
|
watched=Sledované repozitáře
|
||||||
code=Kód
|
code=Kód
|
||||||
|
@ -618,6 +618,7 @@ joined_on=Beigetreten am %s
|
|||||||
repositories=Repositories
|
repositories=Repositories
|
||||||
activity=Öffentliche Aktivität
|
activity=Öffentliche Aktivität
|
||||||
followers=Follower
|
followers=Follower
|
||||||
|
show_more=Mehr anzeigen
|
||||||
starred=Favoriten
|
starred=Favoriten
|
||||||
watched=Beobachtete Repositories
|
watched=Beobachtete Repositories
|
||||||
code=Quelltext
|
code=Quelltext
|
||||||
|
@ -580,6 +580,7 @@ joined_on=Εγγράφηκε την %s
|
|||||||
repositories=Αποθετήρια
|
repositories=Αποθετήρια
|
||||||
activity=Δημόσια Δραστηριότητα
|
activity=Δημόσια Δραστηριότητα
|
||||||
followers=Ακόλουθοι
|
followers=Ακόλουθοι
|
||||||
|
show_more=Εμφάνιση Περισσότερων
|
||||||
starred=Αγαπημένα Αποθετήρια
|
starred=Αγαπημένα Αποθετήρια
|
||||||
watched=Ακολουθούμενα Αποθετήρια
|
watched=Ακολουθούμενα Αποθετήρια
|
||||||
code=Κώδικας
|
code=Κώδικας
|
||||||
|
@ -649,6 +649,7 @@ joined_on = Joined on %s
|
|||||||
repositories = Repositories
|
repositories = Repositories
|
||||||
activity = Public Activity
|
activity = Public Activity
|
||||||
followers = Followers
|
followers = Followers
|
||||||
|
show_more = Show More
|
||||||
starred = Starred Repositories
|
starred = Starred Repositories
|
||||||
watched = Watched Repositories
|
watched = Watched Repositories
|
||||||
code = Code
|
code = Code
|
||||||
|
@ -577,6 +577,7 @@ joined_on=Se unió el %s
|
|||||||
repositories=Repositorios
|
repositories=Repositorios
|
||||||
activity=Actividad pública
|
activity=Actividad pública
|
||||||
followers=Seguidores
|
followers=Seguidores
|
||||||
|
show_more=Ver más
|
||||||
starred=Repositorios Favoritos
|
starred=Repositorios Favoritos
|
||||||
watched=Repositorios seguidos
|
watched=Repositorios seguidos
|
||||||
code=Código
|
code=Código
|
||||||
|
@ -463,6 +463,7 @@ change_avatar=تغییر آواتار…
|
|||||||
repositories=مخازن
|
repositories=مخازن
|
||||||
activity=فعالیت های عمومی
|
activity=فعالیت های عمومی
|
||||||
followers=دنبال کنندگان
|
followers=دنبال کنندگان
|
||||||
|
show_more=نمایش بیشتر
|
||||||
starred=مخان ستاره دار
|
starred=مخان ستاره دار
|
||||||
watched=مخازنی که دنبال میشوند
|
watched=مخازنی که دنبال میشوند
|
||||||
projects=پروژهها
|
projects=پروژهها
|
||||||
|
@ -649,6 +649,7 @@ joined_on=Inscrit le %s
|
|||||||
repositories=Dépôts
|
repositories=Dépôts
|
||||||
activity=Activité publique
|
activity=Activité publique
|
||||||
followers=abonnés
|
followers=abonnés
|
||||||
|
show_more=Voir plus
|
||||||
starred=Dépôts favoris
|
starred=Dépôts favoris
|
||||||
watched=Dépôts surveillés
|
watched=Dépôts surveillés
|
||||||
code=Code
|
code=Code
|
||||||
|
@ -649,6 +649,7 @@ joined_on=Cláraigh ar %s
|
|||||||
repositories=Stórais
|
repositories=Stórais
|
||||||
activity=Gníomhaíocht Phoiblí
|
activity=Gníomhaíocht Phoiblí
|
||||||
followers=Leantóirí
|
followers=Leantóirí
|
||||||
|
show_more=Taispeáin Tuilleadh
|
||||||
starred=Stórais Réaltaithe
|
starred=Stórais Réaltaithe
|
||||||
watched=Stórais Breathnaithe
|
watched=Stórais Breathnaithe
|
||||||
code=Cód
|
code=Cód
|
||||||
@ -1945,6 +1946,8 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo?
|
|||||||
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
|
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
|
||||||
|
|
||||||
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
|
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %[1]d tiomantas taobh thiar de %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %[1]d geallta taobh thiar de %[2]s
|
||||||
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
|
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
|
||||||
pulls.upstream_diverging_merge=Forc sionc
|
pulls.upstream_diverging_merge=Forc sionc
|
||||||
|
|
||||||
@ -3719,6 +3722,7 @@ runners.status.active=Gníomhach
|
|||||||
runners.status.offline=As líne
|
runners.status.offline=As líne
|
||||||
runners.version=Leagan
|
runners.version=Leagan
|
||||||
runners.reset_registration_token=Athshocraigh comhartha clár
|
runners.reset_registration_token=Athshocraigh comhartha clár
|
||||||
|
runners.reset_registration_token_confirm=Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint?
|
||||||
runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit
|
runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit
|
||||||
|
|
||||||
runs.all_workflows=Gach Sreafaí Oibre
|
runs.all_workflows=Gach Sreafaí Oibre
|
||||||
@ -3770,6 +3774,8 @@ variables.creation.success=Tá an athróg "%s" curtha leis.
|
|||||||
variables.update.failed=Theip ar athróg a chur in eagar.
|
variables.update.failed=Theip ar athróg a chur in eagar.
|
||||||
variables.update.success=Tá an t-athróg curtha in eagar.
|
variables.update.success=Tá an t-athróg curtha in eagar.
|
||||||
|
|
||||||
|
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
|
||||||
|
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
deleted.display_name=Tionscadal scriosta
|
deleted.display_name=Tionscadal scriosta
|
||||||
|
@ -488,6 +488,7 @@ change_avatar=Modifica il tuo avatar…
|
|||||||
repositories=Repository
|
repositories=Repository
|
||||||
activity=Attività pubblica
|
activity=Attività pubblica
|
||||||
followers=Seguaci
|
followers=Seguaci
|
||||||
|
show_more=Mostra Altro
|
||||||
starred=Repositories votate
|
starred=Repositories votate
|
||||||
watched=Repository Osservate
|
watched=Repository Osservate
|
||||||
projects=Progetti
|
projects=Progetti
|
||||||
|
@ -649,6 +649,7 @@ joined_on=%sに登録
|
|||||||
repositories=リポジトリ
|
repositories=リポジトリ
|
||||||
activity=公開アクティビティ
|
activity=公開アクティビティ
|
||||||
followers=フォロワー
|
followers=フォロワー
|
||||||
|
show_more=さらに表示
|
||||||
starred=スター付きリポジトリ
|
starred=スター付きリポジトリ
|
||||||
watched=ウォッチ中リポジトリ
|
watched=ウォッチ中リポジトリ
|
||||||
code=コード
|
code=コード
|
||||||
|
@ -583,6 +583,7 @@ joined_on=Pievienojās %s
|
|||||||
repositories=Repozitoriji
|
repositories=Repozitoriji
|
||||||
activity=Publiskā aktivitāte
|
activity=Publiskā aktivitāte
|
||||||
followers=Sekotāji
|
followers=Sekotāji
|
||||||
|
show_more=Rādīt vairāk
|
||||||
starred=Atzīmēti repozitoriji
|
starred=Atzīmēti repozitoriji
|
||||||
watched=Vērotie repozitoriji
|
watched=Vērotie repozitoriji
|
||||||
code=Kods
|
code=Kods
|
||||||
|
@ -487,6 +487,7 @@ change_avatar=Wijzig je profielfoto…
|
|||||||
repositories=repositories
|
repositories=repositories
|
||||||
activity=Openbare activiteit
|
activity=Openbare activiteit
|
||||||
followers=Volgers
|
followers=Volgers
|
||||||
|
show_more=Meer weergeven
|
||||||
starred=Repositories met ster
|
starred=Repositories met ster
|
||||||
watched=Gevolgde repositories
|
watched=Gevolgde repositories
|
||||||
projects=Projecten
|
projects=Projecten
|
||||||
|
@ -582,6 +582,7 @@ joined_on=Inscreveu-se em %s
|
|||||||
repositories=Repositórios
|
repositories=Repositórios
|
||||||
activity=Atividade pública
|
activity=Atividade pública
|
||||||
followers=Seguidores
|
followers=Seguidores
|
||||||
|
show_more=Mostrar mais
|
||||||
starred=Repositórios favoritos
|
starred=Repositórios favoritos
|
||||||
watched=Repositórios observados
|
watched=Repositórios observados
|
||||||
code=Código
|
code=Código
|
||||||
|
@ -649,6 +649,7 @@ joined_on=Inscreveu-se em %s
|
|||||||
repositories=Repositórios
|
repositories=Repositórios
|
||||||
activity=Trabalho público
|
activity=Trabalho público
|
||||||
followers=Seguidores
|
followers=Seguidores
|
||||||
|
show_more=Mostrar mais
|
||||||
starred=Repositórios favoritos
|
starred=Repositórios favoritos
|
||||||
watched=Repositórios sob vigilância
|
watched=Repositórios sob vigilância
|
||||||
code=Código
|
code=Código
|
||||||
@ -1921,8 +1922,8 @@ pulls.close=Encerrar pedido de integração
|
|||||||
pulls.closed_at=`fechou este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
pulls.closed_at=`fechou este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
pulls.reopened_at=`reabriu este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
pulls.reopened_at=`reabriu este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
pulls.cmd_instruction_hint=`Ver <a class="show-instruction">instruções para a linha de comandos</a>.`
|
pulls.cmd_instruction_hint=`Ver <a class="show-instruction">instruções para a linha de comandos</a>.`
|
||||||
pulls.cmd_instruction_checkout_title=Conferir
|
pulls.cmd_instruction_checkout_title=Checkout
|
||||||
pulls.cmd_instruction_checkout_desc=No seu repositório, irá criar um novo ramo para que possa testar as modificações.
|
pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações.
|
||||||
pulls.cmd_instruction_merge_title=Integrar
|
pulls.cmd_instruction_merge_title=Integrar
|
||||||
pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea.
|
pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea.
|
||||||
pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado
|
pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado
|
||||||
@ -1945,6 +1946,8 @@ pulls.delete.title=Eliminar este pedido de integração?
|
|||||||
pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo.
|
pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo.
|
||||||
|
|
||||||
pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2]s
|
pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrás de %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s
|
||||||
pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações
|
pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações
|
||||||
pulls.upstream_diverging_merge=Sincronizar derivação
|
pulls.upstream_diverging_merge=Sincronizar derivação
|
||||||
|
|
||||||
|
@ -578,6 +578,7 @@ joined_on=Присоединил(ся/ась) %s
|
|||||||
repositories=Репозитории
|
repositories=Репозитории
|
||||||
activity=Активность
|
activity=Активность
|
||||||
followers=Подписчики
|
followers=Подписчики
|
||||||
|
show_more=Показать больше
|
||||||
starred=Избранные репозитории
|
starred=Избранные репозитории
|
||||||
watched=Отслеживаемые репозитории
|
watched=Отслеживаемые репозитории
|
||||||
code=Код
|
code=Код
|
||||||
|
@ -452,6 +452,7 @@ change_avatar=ඔබගේ අවතාරය වෙනස් කරන්න…
|
|||||||
repositories=කෝෂ්ඨ
|
repositories=කෝෂ්ඨ
|
||||||
activity=ප්රසිද්ධ ක්රියාකාරකම
|
activity=ප්රසිද්ධ ක්රියාකාරකම
|
||||||
followers=අනුගාමිකයන්
|
followers=අනුගාමිකයන්
|
||||||
|
show_more=තව පෙන්වන්න
|
||||||
starred=තරු ගබඩාව
|
starred=තරු ගබඩාව
|
||||||
watched=නරඹන ලද ගබඩාවලදී
|
watched=නරඹන ලද ගබඩාවලදී
|
||||||
projects=ව්යාපෘති
|
projects=ව්යාපෘති
|
||||||
|
@ -631,6 +631,7 @@ joined_on=%s tarihinde katıldı
|
|||||||
repositories=Depolar
|
repositories=Depolar
|
||||||
activity=Genel Aktivite
|
activity=Genel Aktivite
|
||||||
followers=Takipçiler
|
followers=Takipçiler
|
||||||
|
show_more=Daha Fazla Göster
|
||||||
starred=Yıldızlanmış depolar
|
starred=Yıldızlanmış depolar
|
||||||
watched=İzlenen Depolar
|
watched=İzlenen Depolar
|
||||||
code=Kod
|
code=Kod
|
||||||
|
@ -466,6 +466,7 @@ change_avatar=Змінити свій аватар…
|
|||||||
repositories=Репозиторії
|
repositories=Репозиторії
|
||||||
activity=Публічна активність
|
activity=Публічна активність
|
||||||
followers=Читачі
|
followers=Читачі
|
||||||
|
show_more=Показати більше
|
||||||
starred=Обрані Репозиторії
|
starred=Обрані Репозиторії
|
||||||
watched=Відстежувані репозиторії
|
watched=Відстежувані репозиторії
|
||||||
projects=Проєкт
|
projects=Проєкт
|
||||||
|
@ -649,6 +649,7 @@ joined_on=加入于 %s
|
|||||||
repositories=仓库列表
|
repositories=仓库列表
|
||||||
activity=公开活动
|
activity=公开活动
|
||||||
followers=关注者
|
followers=关注者
|
||||||
|
show_more=显示更多
|
||||||
starred=已点赞
|
starred=已点赞
|
||||||
watched=已关注仓库
|
watched=已关注仓库
|
||||||
code=代码
|
code=代码
|
||||||
|
@ -647,6 +647,7 @@ joined_on=加入於 %s
|
|||||||
repositories=儲存庫
|
repositories=儲存庫
|
||||||
activity=公開動態
|
activity=公開動態
|
||||||
followers=追蹤者
|
followers=追蹤者
|
||||||
|
show_more=顯示更多
|
||||||
starred=已加星號
|
starred=已加星號
|
||||||
watched=關注的儲存庫
|
watched=關注的儲存庫
|
||||||
code=程式碼
|
code=程式碼
|
||||||
|
170
package-lock.json
generated
170
package-lock.json
generated
@ -87,7 +87,7 @@
|
|||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-import-resolver-typescript": "3.7.0",
|
"eslint-import-resolver-typescript": "3.7.0",
|
||||||
"eslint-plugin-array-func": "4.0.0",
|
"eslint-plugin-array-func": "4.0.0",
|
||||||
"eslint-plugin-github": "5.1.4",
|
"eslint-plugin-github": "5.0.2",
|
||||||
"eslint-plugin-import-x": "4.6.1",
|
"eslint-plugin-import-x": "4.6.1",
|
||||||
"eslint-plugin-no-jquery": "3.1.0",
|
"eslint-plugin-no-jquery": "3.1.0",
|
||||||
"eslint-plugin-no-use-extend-native": "0.5.0",
|
"eslint-plugin-no-use-extend-native": "0.5.0",
|
||||||
@ -7953,166 +7953,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-github": {
|
"node_modules/eslint-plugin-github": {
|
||||||
"version": "5.1.4",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.0.2.tgz",
|
||||||
"integrity": "sha512-j5IgIxsDoch06zJzeqPvenfzRXDKI9Z8YwfUg1pm2ay1q44tMSFwvEu6l0uEIrTpA3v8QdPyLr98LqDl1TIhSA==",
|
"integrity": "sha512-nMdzWJQ5CimjQDY6SFeJ0KIXuNFf0dgDWEd4eP3UWfuTuP/dXcZJDg7MQRvAFt743T1zUi4+/HdOihfu8xJkLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/compat": "^1.2.3",
|
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
|
||||||
"@eslint/js": "^9.14.0",
|
|
||||||
"@github/browserslist-config": "^1.0.0",
|
"@github/browserslist-config": "^1.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"aria-query": "^5.3.0",
|
"aria-query": "^5.3.0",
|
||||||
"eslint-config-prettier": ">=8.0.0",
|
"eslint-config-prettier": ">=8.0.0",
|
||||||
"eslint-plugin-escompat": "^3.11.3",
|
"eslint-plugin-escompat": "^3.3.3",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-filenames": "^1.3.2",
|
"eslint-plugin-filenames": "^1.3.2",
|
||||||
"eslint-plugin-i18n-text": "^1.0.1",
|
"eslint-plugin-i18n-text": "^1.0.1",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-plugin-import": "^2.25.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-rule-documentation": ">=1.0.0",
|
"eslint-rule-documentation": ">=1.0.0",
|
||||||
"globals": "^15.12.0",
|
|
||||||
"jsx-ast-utils": "^3.3.2",
|
"jsx-ast-utils": "^3.3.2",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"svg-element-attributes": "^1.3.1",
|
"svg-element-attributes": "^1.3.1"
|
||||||
"typescript-eslint": "^8.14.0"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-ignore-errors": "bin/eslint-ignore-errors.js"
|
"eslint-ignore-errors": "bin/eslint-ignore-errors.js"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8 || ^9"
|
"eslint": "^8.0.1"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/@eslint/compat": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "^9.10.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"eslint": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ajv": "^6.12.4",
|
|
||||||
"debug": "^4.3.2",
|
|
||||||
"espree": "^10.0.1",
|
|
||||||
"globals": "^14.0.0",
|
|
||||||
"ignore": "^5.2.0",
|
|
||||||
"import-fresh": "^3.2.1",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"minimatch": "^3.1.2",
|
|
||||||
"strip-json-comments": "^3.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/eslint"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc/node_modules/globals": {
|
|
||||||
"version": "14.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
|
||||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/@eslint/js": {
|
|
||||||
"version": "9.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
|
||||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/ajv": {
|
|
||||||
"version": "6.12.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.1",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"json-schema-traverse": "^0.4.1",
|
|
||||||
"uri-js": "^4.2.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/globals": {
|
|
||||||
"version": "15.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
|
|
||||||
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/json-schema-traverse": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-github/node_modules/minimatch": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-i18n-text": {
|
"node_modules/eslint-plugin-i18n-text": {
|
||||||
@ -14063,29 +13932,6 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
|
||||||
"version": "8.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz",
|
|
||||||
"integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
|
||||||
"@typescript-eslint/parser": "8.18.1",
|
|
||||||
"@typescript-eslint/utils": "8.18.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
|
||||||
"typescript": ">=4.8.4 <5.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typo-js": {
|
"node_modules/typo-js": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.5.tgz",
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-import-resolver-typescript": "3.7.0",
|
"eslint-import-resolver-typescript": "3.7.0",
|
||||||
"eslint-plugin-array-func": "4.0.0",
|
"eslint-plugin-array-func": "4.0.0",
|
||||||
"eslint-plugin-github": "5.1.4",
|
"eslint-plugin-github": "5.0.2",
|
||||||
"eslint-plugin-import-x": "4.6.1",
|
"eslint-plugin-import-x": "4.6.1",
|
||||||
"eslint-plugin-no-jquery": "3.1.0",
|
"eslint-plugin-no-jquery": "3.1.0",
|
||||||
"eslint-plugin-no-use-extend-native": "0.5.0",
|
"eslint-plugin-no-use-extend-native": "0.5.0",
|
||||||
|
@ -69,7 +69,7 @@ func (s *Service) Register(
|
|||||||
labels := req.Msg.Labels
|
labels := req.Msg.Labels
|
||||||
|
|
||||||
// create new runner
|
// create new runner
|
||||||
name, _ := util.SplitStringAtByteN(req.Msg.Name, 255)
|
name := util.EllipsisDisplayString(req.Msg.Name, 255)
|
||||||
runner := &actions_model.ActionRunner{
|
runner := &actions_model.ActionRunner{
|
||||||
UUID: gouuid.New().String(),
|
UUID: gouuid.New().String(),
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -37,8 +37,6 @@ import (
|
|||||||
"code.gitea.io/gitea/routers/api/packages/vagrant"
|
"code.gitea.io/gitea/routers/api/packages/vagrant"
|
||||||
"code.gitea.io/gitea/services/auth"
|
"code.gitea.io/gitea/services/auth"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||||
@ -140,39 +138,10 @@ func CommonRoutes() *web.Router {
|
|||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/arch", func() {
|
r.Group("/arch", func() {
|
||||||
r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey)
|
r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey)
|
||||||
|
r.PathGroup("*", func(g *web.RouterPathGroup) {
|
||||||
reqPutRepository := web.NewPathProcessor("PUT", "/<repository:*>")
|
g.MatchPath("PUT", "/<repository:*>", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile)
|
||||||
reqGetRepoArchFile := web.NewPathProcessor("HEAD,GET", "/<repository:*>/<architecture>/<filename>")
|
g.MatchPath("HEAD,GET", "/<repository:*>/<architecture>/<filename>", arch.GetPackageOrRepositoryFile)
|
||||||
reqDeleteRepoNameVerArch := web.NewPathProcessor("DELETE", "/<repository:*>/<name>/<version>/<architecture>")
|
g.MatchPath("DELETE", "/<repository:*>/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion)
|
||||||
|
|
||||||
r.Any("*", func(ctx *context.Context) {
|
|
||||||
chiCtx := chi.RouteContext(ctx.Req.Context())
|
|
||||||
path := ctx.PathParam("*")
|
|
||||||
|
|
||||||
if reqPutRepository.ProcessRequestPath(chiCtx, path) {
|
|
||||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
arch.UploadPackageFile(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqGetRepoArchFile.ProcessRequestPath(chiCtx, path) {
|
|
||||||
arch.GetPackageOrRepositoryFile(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqDeleteRepoNameVerArch.ProcessRequestPath(chiCtx, path) {
|
|
||||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
arch.DeletePackageVersion(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Status(http.StatusNotFound)
|
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/cargo", func() {
|
r.Group("/cargo", func() {
|
||||||
|
@ -733,7 +733,7 @@ func CreateIssue(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if form.Closed {
|
if form.Closed {
|
||||||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil {
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
if issues_model.IsErrDependenciesLeft(err) {
|
if issues_model.IsErrDependenciesLeft(err) {
|
||||||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
||||||
return
|
return
|
||||||
@ -912,27 +912,11 @@ func EditIssue(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isClosed bool
|
state := api.StateType(*form.State)
|
||||||
switch state := api.StateType(*form.State); state {
|
closeOrReopenIssue(ctx, issue, state)
|
||||||
case api.StateOpen:
|
if ctx.Written() {
|
||||||
isClosed = false
|
|
||||||
case api.StateClosed:
|
|
||||||
isClosed = true
|
|
||||||
default:
|
|
||||||
ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.IsClosed != isClosed {
|
|
||||||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
|
|
||||||
if issues_model.IsErrDependenciesLeft(err) {
|
|
||||||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refetch from database to assign some automatic values
|
// Refetch from database to assign some automatic values
|
||||||
@ -1055,3 +1039,26 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
|
|||||||
|
|
||||||
ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()})
|
ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) {
|
||||||
|
if state != api.StateOpen && state != api.StateClosed {
|
||||||
|
ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state == api.StateClosed && !issue.IsClosed {
|
||||||
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
|
if issues_model.IsErrDependenciesLeft(err) {
|
||||||
|
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue or pull request because it still has open dependencies")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Error(http.StatusInternalServerError, "CloseIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if state == api.StateOpen && issue.IsClosed {
|
||||||
|
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "ReopenIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -335,6 +335,9 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
|||||||
labelIDs = append(labelIDs, int64(rv.Float()))
|
labelIDs = append(labelIDs, int64(rv.Float()))
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
labelNames = append(labelNames, rv.String())
|
labelNames = append(labelNames, rv.String())
|
||||||
|
default:
|
||||||
|
ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string")
|
||||||
|
return nil, nil, fmt.Errorf("invalid label")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(labelIDs) > 0 && len(labelNames) > 0 {
|
if len(labelIDs) > 0 && len(labelNames) > 0 {
|
||||||
@ -342,11 +345,20 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
|||||||
return nil, nil, fmt.Errorf("invalid labels")
|
return nil, nil, fmt.Errorf("invalid labels")
|
||||||
}
|
}
|
||||||
if len(labelNames) > 0 {
|
if len(labelNames) > 0 {
|
||||||
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
|
ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
labelIDs = append(labelIDs, repoLabelIDs...)
|
||||||
|
if ctx.Repo.Owner.IsOrganization() {
|
||||||
|
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetLabelIDsInOrgByNames", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
labelIDs = append(labelIDs, orgLabelIDs...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
|
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
|
||||||
|
@ -728,27 +728,11 @@ func EditPullRequest(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var isClosed bool
|
state := api.StateType(*form.State)
|
||||||
switch state := api.StateType(*form.State); state {
|
closeOrReopenIssue(ctx, issue, state)
|
||||||
case api.StateOpen:
|
if ctx.Written() {
|
||||||
isClosed = false
|
|
||||||
case api.StateClosed:
|
|
||||||
isClosed = true
|
|
||||||
default:
|
|
||||||
ctx.Error(http.StatusPreconditionFailed, "UnknownPRStateError", fmt.Sprintf("unknown state: %s", state))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.IsClosed != isClosed {
|
|
||||||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
|
|
||||||
if issues_model.IsErrDependenciesLeft(err) {
|
|
||||||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// change pull target branch
|
// change pull target branch
|
||||||
|
@ -689,7 +689,7 @@ func Activate(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||||
if user == nil { // if code is wrong
|
if user == nil { // if code is wrong
|
||||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||||
return
|
return
|
||||||
@ -734,7 +734,7 @@ func ActivatePost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||||
if user == nil { // if code is wrong
|
if user == nil { // if code is wrong
|
||||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||||
return
|
return
|
||||||
|
@ -113,7 +113,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fail early, don't frustrate the user
|
// Fail early, don't frustrate the user
|
||||||
u := user_model.VerifyUserActiveCode(ctx, code)
|
u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -9,6 +9,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/asymkey"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
@ -41,16 +45,85 @@ func FetchActionTest(ctx *context.Context) {
|
|||||||
ctx.JSONRedirect("")
|
ctx.JSONRedirect("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Tmpl(ctx *context.Context) {
|
func prepareMockData(ctx *context.Context) {
|
||||||
now := time.Now()
|
if ctx.Req.URL.Path == "/devtest/gitea-ui" {
|
||||||
ctx.Data["TimeNow"] = now
|
now := time.Now()
|
||||||
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
|
ctx.Data["TimeNow"] = now
|
||||||
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
|
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
|
||||||
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
|
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
|
||||||
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
|
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
|
||||||
ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
|
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
|
||||||
ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
|
ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
|
||||||
|
ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Req.URL.Path == "/devtest/commit-sign-badge" {
|
||||||
|
var commits []*asymkey.SignCommit
|
||||||
|
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
|
||||||
|
mockUser := mockUsers[0]
|
||||||
|
commits = append(commits, &asymkey.SignCommit{
|
||||||
|
Verification: &asymkey.CommitVerification{},
|
||||||
|
UserCommit: &user_model.UserCommit{
|
||||||
|
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
commits = append(commits, &asymkey.SignCommit{
|
||||||
|
Verification: &asymkey.CommitVerification{
|
||||||
|
Verified: true,
|
||||||
|
Reason: "name / key-id",
|
||||||
|
SigningUser: mockUser,
|
||||||
|
SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
|
||||||
|
TrustStatus: "trusted",
|
||||||
|
},
|
||||||
|
UserCommit: &user_model.UserCommit{
|
||||||
|
User: mockUser,
|
||||||
|
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
commits = append(commits, &asymkey.SignCommit{
|
||||||
|
Verification: &asymkey.CommitVerification{
|
||||||
|
Verified: true,
|
||||||
|
Reason: "name / key-id",
|
||||||
|
SigningUser: mockUser,
|
||||||
|
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||||
|
TrustStatus: "untrusted",
|
||||||
|
},
|
||||||
|
UserCommit: &user_model.UserCommit{
|
||||||
|
User: mockUser,
|
||||||
|
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
commits = append(commits, &asymkey.SignCommit{
|
||||||
|
Verification: &asymkey.CommitVerification{
|
||||||
|
Verified: true,
|
||||||
|
Reason: "name / key-id",
|
||||||
|
SigningUser: mockUser,
|
||||||
|
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||||
|
TrustStatus: "other(unmatch)",
|
||||||
|
},
|
||||||
|
UserCommit: &user_model.UserCommit{
|
||||||
|
User: mockUser,
|
||||||
|
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
commits = append(commits, &asymkey.SignCommit{
|
||||||
|
Verification: &asymkey.CommitVerification{
|
||||||
|
Warning: true,
|
||||||
|
Reason: "gpg.error",
|
||||||
|
SigningEmail: "test@example.com",
|
||||||
|
},
|
||||||
|
UserCommit: &user_model.UserCommit{
|
||||||
|
User: mockUser,
|
||||||
|
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.Data["MockCommits"] = commits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Tmpl(ctx *context.Context) {
|
||||||
|
prepareMockData(ctx)
|
||||||
if ctx.Req.Method == "POST" {
|
if ctx.Req.Method == "POST" {
|
||||||
_ = ctx.Req.ParseForm()
|
_ = ctx.Req.ParseForm()
|
||||||
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+
|
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+
|
||||||
@ -60,6 +133,5 @@ func Tmpl(ctx *context.Context) {
|
|||||||
)
|
)
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, templates.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub"))))
|
ctx.HTML(http.StatusOK, templates.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub"))))
|
||||||
}
|
}
|
||||||
|
@ -664,7 +664,7 @@ func PrepareCompareDiff(
|
|||||||
}
|
}
|
||||||
if len(title) > 255 {
|
if len(title) > 255 {
|
||||||
var trailer string
|
var trailer string
|
||||||
title, trailer = util.SplitStringAtByteN(title, 255)
|
title, trailer = util.EllipsisDisplayStringX(title, 255)
|
||||||
if len(trailer) > 0 {
|
if len(trailer) > 0 {
|
||||||
if ctx.Data["content"] != nil {
|
if ctx.Data["content"] != nil {
|
||||||
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
||||||
|
@ -154,25 +154,28 @@ func NewComment(ctx *context.Context) {
|
|||||||
if pr != nil {
|
if pr != nil {
|
||||||
ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
|
ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
|
||||||
} else {
|
} else {
|
||||||
isClosed := form.Status == "close"
|
if form.Status == "close" && !issue.IsClosed {
|
||||||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
log.Error("ChangeStatus: %v", err)
|
log.Error("CloseIssue: %v", err)
|
||||||
|
if issues_model.IsErrDependenciesLeft(err) {
|
||||||
if issues_model.IsErrDependenciesLeft(err) {
|
if issue.IsPull {
|
||||||
if issue.IsPull {
|
ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||||
ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
} else {
|
||||||
} else {
|
ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
|
||||||
ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
} else {
|
||||||
|
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
|
||||||
|
ctx.ServerError("stopTimerIfAvailable", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
|
||||||
}
|
}
|
||||||
} else {
|
} else if form.Status == "reopen" && issue.IsClosed {
|
||||||
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
|
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
ctx.ServerError("CreateOrStopIssueStopwatch", err)
|
log.Error("ReopenIssue: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,14 +418,11 @@ func UpdateIssueStatus(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var isClosed bool
|
action := ctx.FormString("action")
|
||||||
switch action := ctx.FormString("action"); action {
|
if action != "open" && action != "close" {
|
||||||
case "open":
|
|
||||||
isClosed = false
|
|
||||||
case "close":
|
|
||||||
isClosed = true
|
|
||||||
default:
|
|
||||||
log.Warn("Unrecognized action: %s", action)
|
log.Warn("Unrecognized action: %s", action)
|
||||||
|
ctx.JSONOK()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := issues.LoadRepositories(ctx); err != nil {
|
if _, err := issues.LoadRepositories(ctx); err != nil {
|
||||||
@ -441,15 +438,20 @@ func UpdateIssueStatus(ctx *context.Context) {
|
|||||||
if issue.IsPull && issue.PullRequest.HasMerged {
|
if issue.IsPull && issue.PullRequest.HasMerged {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if issue.IsClosed != isClosed {
|
if action == "close" && !issue.IsClosed {
|
||||||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
if issues_model.IsErrDependenciesLeft(err) {
|
if issues_model.IsErrDependenciesLeft(err) {
|
||||||
ctx.JSON(http.StatusPreconditionFailed, map[string]any{
|
ctx.JSON(http.StatusPreconditionFailed, map[string]any{
|
||||||
"error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
|
"error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.ServerError("ChangeStatus", err)
|
ctx.ServerError("CloseIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if action == "open" && issue.IsClosed {
|
||||||
|
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
|
ctx.ServerError("ReopenIssue", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,11 +61,20 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
|
|||||||
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
|
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
|
||||||
UserID: ctx.ContextUser.ID,
|
UserID: ctx.ContextUser.ID,
|
||||||
IncludePrivate: showPrivate,
|
IncludePrivate: showPrivate,
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: 1,
|
||||||
|
// query one more results (without a separate counting) to see whether we need to add the "show more orgs" link
|
||||||
|
PageSize: setting.UI.User.OrgPagingNum + 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("FindOrgs", err)
|
ctx.ServerError("FindOrgs", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(orgs) > setting.UI.User.OrgPagingNum {
|
||||||
|
orgs = orgs[:setting.UI.User.OrgPagingNum]
|
||||||
|
ctx.Data["ShowMoreOrgs"] = true
|
||||||
|
}
|
||||||
ctx.Data["Orgs"] = orgs
|
ctx.Data["Orgs"] = orgs
|
||||||
ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer)
|
ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer)
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/renderhelper"
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@ -256,6 +257,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
|||||||
ctx.Data["ProfileReadme"] = profileContent
|
ctx.Data["ProfileReadme"] = profileContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "organizations":
|
||||||
|
orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{
|
||||||
|
UserID: ctx.ContextUser.ID,
|
||||||
|
IncludePrivate: showPrivate,
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pagingNum,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserOrganizations", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Cards"] = orgs
|
||||||
|
total = int(count)
|
||||||
default: // default to "repositories"
|
default: // default to "repositories"
|
||||||
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
@ -294,31 +310,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
|||||||
}
|
}
|
||||||
|
|
||||||
pager := context.NewPagination(total, pagingNum, page, 5)
|
pager := context.NewPagination(total, pagingNum, page, 5)
|
||||||
pager.SetDefaultParams(ctx)
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
pager.AddParamString("tab", tab)
|
|
||||||
if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
|
|
||||||
pager.AddParamString("language", language)
|
|
||||||
}
|
|
||||||
if tab == "activity" {
|
|
||||||
if ctx.Data["Date"] != nil {
|
|
||||||
pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if archived.Has() {
|
|
||||||
pager.AddParamString("archived", fmt.Sprint(archived.Value()))
|
|
||||||
}
|
|
||||||
if fork.Has() {
|
|
||||||
pager.AddParamString("fork", fmt.Sprint(fork.Value()))
|
|
||||||
}
|
|
||||||
if mirror.Has() {
|
|
||||||
pager.AddParamString("mirror", fmt.Sprint(mirror.Value()))
|
|
||||||
}
|
|
||||||
if template.Has() {
|
|
||||||
pager.AddParamString("template", fmt.Sprint(template.Value()))
|
|
||||||
}
|
|
||||||
if private.Has() {
|
|
||||||
pager.AddParamString("private", fmt.Sprint(private.Value()))
|
|
||||||
}
|
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
|
|||||||
IsPrivate: issue.Repo.IsPrivate,
|
IsPrivate: issue.Repo.IsPrivate,
|
||||||
}
|
}
|
||||||
|
|
||||||
truncatedContent, truncatedRight := util.SplitStringAtByteN(comment.Content, 200)
|
truncatedContent, truncatedRight := util.EllipsisDisplayStringX(comment.Content, 200)
|
||||||
if truncatedRight != "" {
|
if truncatedRight != "" {
|
||||||
// in case the content is in a Latin family language, we remove the last broken word.
|
// in case the content is in a Latin family language, we remove the last broken word.
|
||||||
lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
|
lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
|
||||||
|
@ -188,15 +188,19 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isClosed := ref.Action == references.XRefActionCloses
|
|
||||||
if isClosed && len(ref.TimeLog) > 0 {
|
refIssue.Repo = refRepo
|
||||||
if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil {
|
if ref.Action == references.XRefActionCloses && !refIssue.IsClosed {
|
||||||
|
if len(ref.TimeLog) > 0 {
|
||||||
|
if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := CloseIssue(ctx, refIssue, doer, c.Sha1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
} else if ref.Action == references.XRefActionReopens && refIssue.IsClosed {
|
||||||
if isClosed != refIssue.IsClosed {
|
if err := ReopenIssue(ctx, refIssue, doer, c.Sha1); err != nil {
|
||||||
refIssue.Repo = refRepo
|
|
||||||
if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,10 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issue.MilestoneID == 0 {
|
||||||
|
issue.Milestone = nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ func TestChangeMilestoneAssign(t *testing.T) {
|
|||||||
|
|
||||||
oldMilestoneID := issue.MilestoneID
|
oldMilestoneID := issue.MilestoneID
|
||||||
issue.MilestoneID = 2
|
issue.MilestoneID = 2
|
||||||
|
assert.NoError(t, issue.LoadMilestone(db.DefaultContext))
|
||||||
assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID))
|
assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID))
|
||||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
@ -31,4 +32,11 @@ func TestChangeMilestoneAssign(t *testing.T) {
|
|||||||
OldMilestoneID: oldMilestoneID,
|
OldMilestoneID: oldMilestoneID,
|
||||||
})
|
})
|
||||||
unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{})
|
unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{})
|
||||||
|
assert.NotNil(t, issue.Milestone)
|
||||||
|
|
||||||
|
oldMilestoneID = issue.MilestoneID
|
||||||
|
issue.MilestoneID = 0
|
||||||
|
assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID))
|
||||||
|
assert.EqualValues(t, 0, issue.MilestoneID)
|
||||||
|
assert.Nil(t, issue.Milestone)
|
||||||
}
|
}
|
||||||
|
@ -6,34 +6,54 @@ package issue
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChangeStatus changes issue status to open or closed.
|
// CloseIssue close an issue.
|
||||||
// closed means the target status
|
func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error {
|
||||||
// Fix me: you should check whether the current issue status is same to the target status before call this function
|
dbCtx, committer, err := db.TxContext(ctx)
|
||||||
// as in function changeIssueStatus we will return WasClosedError, even the issue status and target status are both open
|
|
||||||
func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed bool) error {
|
|
||||||
comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if issues_model.IsErrDependenciesLeft(err) && closed {
|
return err
|
||||||
if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
comment, err := issues_model.CloseIssue(dbCtx, issue, doer)
|
||||||
|
if err != nil {
|
||||||
|
if issues_model.IsErrDependenciesLeft(err) {
|
||||||
|
if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil {
|
||||||
log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err)
|
log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if closed {
|
if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil {
|
||||||
if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, closed)
|
if err := committer.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
committer.Close()
|
||||||
|
|
||||||
|
notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, true)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReopenIssue reopen an issue.
|
||||||
|
// FIXME: If some issues dependent this one are closed, should we also reopen them?
|
||||||
|
func ReopenIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error {
|
||||||
|
comment, err := issues_model.ReopenIssue(ctx, issue, doer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, false)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,8 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
|
|||||||
// No mail service configured
|
// No mail service configured
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
|
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}
|
||||||
|
sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendResetPasswordMail sends a password reset mail to the user
|
// SendResetPasswordMail sends a password reset mail to the user
|
||||||
@ -103,7 +104,8 @@ func SendResetPasswordMail(u *user_model.User) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
locale := translation.NewLocale(u.Language)
|
locale := translation.NewLocale(u.Language)
|
||||||
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
|
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}
|
||||||
|
sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendActivateEmailMail sends confirmation email to confirm new email address
|
// SendActivateEmailMail sends confirmation email to confirm new email address
|
||||||
@ -113,11 +115,12 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
locale := translation.NewLocale(u.Language)
|
locale := translation.NewLocale(u.Language)
|
||||||
|
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email}
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"DisplayName": u.DisplayName(),
|
"DisplayName": u.DisplayName(),
|
||||||
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
||||||
"Code": u.GenerateEmailActivateCode(email),
|
"Code": user_model.GenerateUserTimeLimitCode(opts, u),
|
||||||
"Email": email,
|
"Email": email,
|
||||||
"Language": locale.Language(),
|
"Language": locale.Language(),
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/jaytaylor/html2text"
|
"github.com/jaytaylor/html2text"
|
||||||
gomail "github.com/wneessen/go-mail"
|
gomail "github.com/wneessen/go-mail"
|
||||||
@ -54,7 +54,7 @@ func (m *Message) ToMessage() *gomail.Msg {
|
|||||||
|
|
||||||
plainBody, err := html2text.FromString(m.Body)
|
plainBody, err := html2text.FromString(m.Body)
|
||||||
if err != nil || setting.MailService.SendAsPlainText {
|
if err != nil || setting.MailService.SendAsPlainText {
|
||||||
if strings.Contains(base.TruncateString(m.Body, 100), "<html>") {
|
if strings.Contains(util.TruncateRunes(m.Body, 100), "<html>") {
|
||||||
log.Warn("Mail contains HTML but configured to send as plain text.")
|
log.Warn("Mail contains HTML but configured to send as plain text.")
|
||||||
}
|
}
|
||||||
msg.SetBodyString("text/plain", plainBody)
|
msg.SetBodyString("text/plain", plainBody)
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
base_module "code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/label"
|
"code.gitea.io/gitea/modules/label"
|
||||||
@ -409,7 +408,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
|
|||||||
RepoID: g.repo.ID,
|
RepoID: g.repo.ID,
|
||||||
Repo: g.repo,
|
Repo: g.repo,
|
||||||
Index: issue.Number,
|
Index: issue.Number,
|
||||||
Title: base_module.TruncateString(issue.Title, 255),
|
Title: util.TruncateRunes(issue.Title, 255),
|
||||||
Content: issue.Content,
|
Content: issue.Content,
|
||||||
Ref: issue.Ref,
|
Ref: issue.Ref,
|
||||||
IsClosed: issue.State == "closed",
|
IsClosed: issue.State == "closed",
|
||||||
|
@ -263,14 +263,17 @@ func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullReques
|
|||||||
if err = ref.Issue.LoadRepo(ctx); err != nil {
|
if err = ref.Issue.LoadRepo(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
isClosed := ref.RefAction == references.XRefActionCloses
|
if ref.RefAction == references.XRefActionCloses && !ref.Issue.IsClosed {
|
||||||
if isClosed != ref.Issue.IsClosed {
|
if err = issue_service.CloseIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil {
|
||||||
if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil {
|
|
||||||
// Allow ErrDependenciesLeft
|
// Allow ErrDependenciesLeft
|
||||||
if !issues_model.IsErrDependenciesLeft(err) {
|
if !issues_model.IsErrDependenciesLeft(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if ref.RefAction == references.XRefActionReopens && ref.Issue.IsClosed {
|
||||||
|
if err = issue_service.ReopenIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -64,7 +64,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// user should be a collaborator or a member of the organization for base repo
|
// user should be a collaborator or a member of the organization for base repo
|
||||||
if !issue.Poster.IsAdmin {
|
canCreate := issue.Poster.IsAdmin || pr.Flow == issues_model.PullRequestFlowAGit
|
||||||
|
if !canCreate {
|
||||||
canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID)
|
canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -706,7 +707,7 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64,
|
|||||||
|
|
||||||
var errs errlist
|
var errs errlist
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
|
if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -740,7 +741,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
|
|||||||
if pr.BaseRepoID == repo.ID {
|
if pr.BaseRepoID == repo.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) {
|
if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
rel.Title = util.EllipsisDisplayString(rel.Title, 255)
|
||||||
rel.LowerTagName = strings.ToLower(rel.TagName)
|
rel.LowerTagName = strings.ToLower(rel.TagName)
|
||||||
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
||||||
return err
|
return err
|
||||||
|
13
templates/devtest/commit-sign-badge.tmpl
Normal file
13
templates/devtest/commit-sign-badge.tmpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{{template "devtest/devtest-header"}}
|
||||||
|
<div class="page-content devtest ui container">
|
||||||
|
<div>
|
||||||
|
<h1>Commit Sign Badges</h1>
|
||||||
|
{{range $commit := .MockCommits}}
|
||||||
|
<div class="flex-text-block tw-my-2">
|
||||||
|
{{template "repo/commit_sign_badge" dict "Commit" $commit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}}
|
||||||
|
{{template "repo/commit_sign_badge" dict "CommitSignVerification" $commit.Verification}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "devtest/devtest-footer"}}
|
@ -1,23 +1,9 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
|
{{$commitLinkBase := print $.RepoLink (Iif $.PageIsWiki "/wiki" "") "/commit"}}
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository diff">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository diff">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container fluid padded">
|
<div class="ui container fluid padded">
|
||||||
{{$class := ""}}
|
<div class="ui top attached header clearing segment tw-relative commit-header">
|
||||||
{{if .Commit.Signature}}
|
|
||||||
{{$class = (print $class " isSigned")}}
|
|
||||||
{{if .Verification.Verified}}
|
|
||||||
{{if eq .Verification.TrustStatus "trusted"}}
|
|
||||||
{{$class = (print $class " isVerified")}}
|
|
||||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
|
||||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
|
||||||
{{else}}
|
|
||||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
|
||||||
{{end}}
|
|
||||||
{{else if .Verification.Warning}}
|
|
||||||
{{$class = (print $class " isWarning")}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
|
|
||||||
<div class="tw-flex tw-mb-4 tw-gap-1">
|
<div class="tw-flex tw-mb-4 tw-gap-1">
|
||||||
<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
|
<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
|
||||||
{{if not $.PageIsWiki}}
|
{{if not $.PageIsWiki}}
|
||||||
@ -142,125 +128,59 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/commit_load_branches_and_tags" .}}
|
{{template "repo/commit_load_branches_and_tags" .}}
|
||||||
</div>
|
</div>
|
||||||
<div class="ui{{if not .Commit.Signature}} bottom{{end}} attached segment tw-flex tw-items-center tw-justify-between tw-py-1 commit-header-row tw-flex-wrap {{$class}}">
|
|
||||||
<div class="tw-flex tw-items-center author">
|
<div class="ui bottom attached segment flex-text-block tw-flex-wrap">
|
||||||
{{if .Author}}
|
<div class="flex-text-inline">
|
||||||
{{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}}
|
{{if .Author}}
|
||||||
{{if .Author.FullName}}
|
{{ctx.AvatarUtils.Avatar .Author 20}}
|
||||||
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a>
|
{{if .Author.FullName}}
|
||||||
{{else}}
|
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a>
|
||||||
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}}
|
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a>
|
||||||
<strong>{{.Commit.Author.Name}}</strong>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="text grey tw-ml-2" id="authored-time">{{DateUtils.TimeSince .Commit.Author.When}}</span>
|
{{else}}
|
||||||
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
|
{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 20}}
|
||||||
<span class="text grey tw-mx-2">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
|
<strong>{{.Commit.Author.Name}}</strong>
|
||||||
{{if ne .Verification.CommittingUser.ID 0}}
|
{{end}}
|
||||||
{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}}
|
</div>
|
||||||
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
|
|
||||||
{{else}}
|
<span class="text grey">{{DateUtils.TimeSince .Commit.Author.When}}</span>
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}}
|
|
||||||
<strong>{{.Commit.Committer.Name}}</strong>
|
<div class="flex-text-inline">
|
||||||
|
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
|
||||||
|
<span class="text grey">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
|
||||||
|
{{if ne .Verification.CommittingUser.ID 0}}
|
||||||
|
{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}}
|
||||||
|
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
|
||||||
|
{{else}}
|
||||||
|
{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 20}}
|
||||||
|
<strong>{{.Commit.Committer.Name}}</strong>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Verification}}
|
||||||
|
{{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="tw-flex-1"></div>
|
||||||
|
|
||||||
|
<div class="flex-text-inline tw-gap-5">
|
||||||
|
{{if .Parents}}
|
||||||
|
<div class="flex-text-inline">
|
||||||
|
<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
|
||||||
|
{{range .Parents}}
|
||||||
|
<a class="ui label commit-id-short" href="{{$commitLinkBase}}/{{PathEscape .}}">{{ShortSha .}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
{{if .Parents}}
|
|
||||||
<div>
|
|
||||||
<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
|
|
||||||
{{range .Parents}}
|
|
||||||
{{if $.PageIsWiki}}
|
|
||||||
<a class="ui primary sha label" href="{{$.RepoLink}}/wiki/commit/{{PathEscape .}}">{{ShortSha .}}</a>
|
|
||||||
{{else}}
|
|
||||||
<a class="ui primary sha label" href="{{$.RepoLink}}/commit/{{PathEscape .}}">{{ShortSha .}}</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="item">
|
|
||||||
<span>{{ctx.Locale.Tr "repo.diff.commit"}}</span>
|
|
||||||
<span class="ui primary sha label">{{ShortSha .CommitID}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{end}}
|
||||||
</div>
|
<div class="flex-text-inline">
|
||||||
{{if .Commit.Signature}}
|
<span>{{ctx.Locale.Tr "repo.diff.commit"}}</span>
|
||||||
<div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap tw-mb-0 {{$class}}">
|
<a class="ui label commit-id-short" href="{{$commitLinkBase}}/{{PathEscape .CommitID}}">{{ShortSha .CommitID}}</a>
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
{{if .Verification.Verified}}
|
|
||||||
{{if ne .Verification.SigningUser.ID 0}}
|
|
||||||
{{svg "gitea-lock" 16 "tw-mr-2"}}
|
|
||||||
{{if eq .Verification.TrustStatus "trusted"}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
|
|
||||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
|
|
||||||
{{end}}
|
|
||||||
{{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}}
|
|
||||||
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.GetDisplayName}}</strong></a>
|
|
||||||
{{else}}
|
|
||||||
<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span>
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
|
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}}
|
|
||||||
<strong>{{.Verification.SigningUser.GetDisplayName}}</strong>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
{{svg "gitea-unlock" 16 "tw-mr-2"}}
|
|
||||||
<span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
{{if .Verification.Verified}}
|
|
||||||
{{if ne .Verification.SigningUser.ID 0}}
|
|
||||||
{{svg "octicon-verified" 16 "tw-mr-2"}}
|
|
||||||
{{if .Verification.SigningSSHKey}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
|
||||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
|
||||||
{{else}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
|
||||||
{{.Verification.SigningKey.PaddedKeyID}}
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
{{svg "octicon-unverified" 16 "tw-mr-2"}}
|
|
||||||
{{if .Verification.SigningSSHKey}}
|
|
||||||
<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
|
||||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
|
||||||
{{else}}
|
|
||||||
<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
|
||||||
{{.Verification.SigningKey.PaddedKeyID}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{else if .Verification.Warning}}
|
|
||||||
{{svg "octicon-unverified" 16 "tw-mr-2"}}
|
|
||||||
{{if .Verification.SigningSSHKey}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
|
||||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
|
||||||
{{else}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
|
||||||
{{.Verification.SigningKey.PaddedKeyID}}
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
{{if .Verification.SigningKey}}
|
|
||||||
{{if ne .Verification.SigningKey.KeyID ""}}
|
|
||||||
{{svg "octicon-verified" 16 "tw-mr-2"}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
|
||||||
{{.Verification.SigningKey.PaddedKeyID}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{if .Verification.SigningSSHKey}}
|
|
||||||
{{if ne .Verification.SigningSSHKey.Fingerprint ""}}
|
|
||||||
{{svg "octicon-verified" 16 "tw-mr-2"}}
|
|
||||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
|
||||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
</div>
|
||||||
|
|
||||||
{{if .NoteRendered}}
|
{{if .NoteRendered}}
|
||||||
<div class="ui top attached header segment git-notes">
|
<div class="ui top attached header segment git-notes">
|
||||||
{{svg "octicon-note" 16 "tw-mr-2"}}
|
{{svg "octicon-note" 16 "tw-mr-2"}}
|
||||||
@ -276,12 +196,13 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<strong>{{.NoteCommit.Author.Name}}</strong>
|
<strong>{{.NoteCommit.Author.Name}}</strong>
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
|
<span class="text grey">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached info segment git-notes">
|
<div class="ui bottom attached info segment git-notes">
|
||||||
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
|
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "repo/diff/box" .}}
|
{{template "repo/diff/box" .}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
78
templates/repo/commit_sign_badge.tmpl
Normal file
78
templates/repo/commit_sign_badge.tmpl
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{{/* Template attributes:
|
||||||
|
* Commit
|
||||||
|
* CommitBaseLink
|
||||||
|
* CommitSignVerification
|
||||||
|
If you'd like to modify this template, you could test it on the devtest page.
|
||||||
|
ATTENTION: this template could be re-rendered many times (on the graph and commit list page),
|
||||||
|
so this template should be kept as small as possbile, DO NOT put large components like modal/dialog into it.
|
||||||
|
*/}}
|
||||||
|
{{- $commit := $.Commit -}}
|
||||||
|
{{- $commitBaseLink := $.CommitBaseLink -}}
|
||||||
|
{{- $verification := $.CommitSignVerification -}}
|
||||||
|
|
||||||
|
{{- $extraClass := "" -}}
|
||||||
|
{{- $verified := false -}}
|
||||||
|
{{- $signingUser := NIL -}}
|
||||||
|
{{- $signingEmail := "" -}}
|
||||||
|
{{- $msgReasonPrefix := "" -}}
|
||||||
|
{{- $msgReason := "" -}}
|
||||||
|
{{- $msgSigningKey := "" -}}
|
||||||
|
|
||||||
|
{{- if $verification -}}
|
||||||
|
{{- $signingUser = $verification.SigningUser -}}
|
||||||
|
{{- $signingEmail = $verification.SigningEmail -}}
|
||||||
|
{{- $extraClass = print $extraClass " commit-is-signed" -}}
|
||||||
|
{{- if $verification.Verified -}}
|
||||||
|
{{- /* reason is "{name} / {key-id}" */ -}}
|
||||||
|
{{- $msgReason = $verification.Reason -}}
|
||||||
|
{{- $verified = true -}}
|
||||||
|
{{- if eq $verification.TrustStatus "trusted" -}}
|
||||||
|
{{- $extraClass = print $extraClass " sign-trusted" -}}
|
||||||
|
{{- else if eq $verification.TrustStatus "untrusted" -}}
|
||||||
|
{{- $extraClass = print $extraClass " sign-untrusted" -}}
|
||||||
|
{{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $extraClass = print $extraClass " sign-unmatched" -}}
|
||||||
|
{{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- if $verification.Warning -}}
|
||||||
|
{{- $extraClass = print $extraClass " sign-warning" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $msgReason = ctx.Locale.Tr $verification.Reason -}}{{- /* dirty part: it is the translation key ..... */ -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if $msgReasonPrefix -}}
|
||||||
|
{{- $msgReason = print $msgReasonPrefix ": " $msgReason -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if $verification.SigningSSHKey -}}
|
||||||
|
{{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.ssh_key_fingerprint") ": " $verification.SigningSSHKey.Fingerprint -}}
|
||||||
|
{{- else if $verification.SigningKey -}}
|
||||||
|
{{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.gpg_key_id") ": " $verification.SigningKey.PaddedKeyID -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if $commit -}}
|
||||||
|
<a {{if $commitBaseLink}}href="{{$commitBaseLink}}/{{$commit.ID}}"{{end}} class="ui label commit-id-short {{$extraClass}}" rel="nofollow">
|
||||||
|
{{- ShortSha $commit.ID.String -}}
|
||||||
|
{{- end -}}
|
||||||
|
<span class="ui label commit-sign-badge {{$extraClass}}">
|
||||||
|
{{- if $verified -}}
|
||||||
|
{{- if and $signingUser $signingUser.ID -}}
|
||||||
|
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock"}}</span>
|
||||||
|
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.Avatar $signingUser 16}}</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock-cog"}}</span>
|
||||||
|
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}}</span>
|
||||||
|
{{- end -}}
|
||||||
|
{{- else -}}
|
||||||
|
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-unlock"}}</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{{- if $commit -}}
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- /* This template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. */ -}}
|
@ -28,33 +28,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="sha">
|
<td class="sha">
|
||||||
{{$class := "ui sha label"}}
|
{{$commitBaseLink := ""}}
|
||||||
{{if .Signature}}
|
|
||||||
{{$class = (print $class " isSigned")}}
|
|
||||||
{{if .Verification.Verified}}
|
|
||||||
{{if eq .Verification.TrustStatus "trusted"}}
|
|
||||||
{{$class = (print $class " isVerified")}}
|
|
||||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
|
||||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
|
||||||
{{else}}
|
|
||||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
|
||||||
{{end}}
|
|
||||||
{{else if .Verification.Warning}}
|
|
||||||
{{$class = (print $class " isWarning")}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{$commitShaLink := ""}}
|
|
||||||
{{if $.PageIsWiki}}
|
{{if $.PageIsWiki}}
|
||||||
{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
|
{{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}}
|
||||||
{{else if $.PageIsPullCommits}}
|
{{else if $.PageIsPullCommits}}
|
||||||
{{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
|
{{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}}
|
||||||
{{else if $.Reponame}}
|
{{else if $.Reponame}}
|
||||||
{{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
|
{{$commitBaseLink = printf "%s/commit" $commitRepoLink}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}">
|
{{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
|
||||||
<span class="shortsha">{{ShortSha .ID.String}}</span>
|
|
||||||
{{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="message">
|
<td class="message">
|
||||||
<span class="message-wrapper">
|
<span class="message-wrapper">
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{{range .comment.Commits}}
|
{{range .comment.Commits}}
|
||||||
{{$tag := printf "%s-%d" $.comment.HashTag $index}}
|
{{$tag := printf "%s-%d" $.comment.HashTag $index}}
|
||||||
{{$index = Eval $index "+" 1}}
|
{{$index = Eval $index "+" 1}}
|
||||||
<div class="singular-commit" id="{{$tag}}">
|
<div class="flex-text-block" id="{{$tag}}">{{/*singular-commit*/}}
|
||||||
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
|
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a>
|
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a>
|
||||||
@ -11,7 +11,8 @@
|
|||||||
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}}
|
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
|
{{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}}
|
||||||
|
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}
|
||||||
|
|
||||||
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}">
|
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}">
|
||||||
{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
||||||
@ -21,29 +22,9 @@
|
|||||||
<button class="ui button ellipsis-button show-panel toggle" data-panel="[data-singular-commit-body-for='{{$tag}}']">...</button>
|
<button class="ui button ellipsis-button show-panel toggle" data-panel="[data-singular-commit-body-for='{{$tag}}']">...</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<span class="shabox tw-flex tw-items-center">
|
<span class="tw-flex tw-items-center tw-gap-2">
|
||||||
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
|
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
|
||||||
{{$class := "ui sha label"}}
|
{{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
|
||||||
{{if .Signature}}
|
|
||||||
{{$class = (print $class " isSigned")}}
|
|
||||||
{{if .Verification.Verified}}
|
|
||||||
{{if eq .Verification.TrustStatus "trusted"}}
|
|
||||||
{{$class = (print $class " isVerified")}}
|
|
||||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
|
||||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
|
||||||
{{else}}
|
|
||||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
|
||||||
{{end}}
|
|
||||||
{{else if .Verification.Warning}}
|
|
||||||
{{$class = (print $class " isWarning")}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
<a href="{{$commitLink}}" rel="nofollow" class="tw-ml-2 {{$class}}">
|
|
||||||
<span class="shortsha">{{ShortSha .ID.String}}</span>
|
|
||||||
{{if .Signature}}
|
|
||||||
{{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}}
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{{$file := .file}}
|
{{$file := .file}}
|
||||||
{{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}}
|
{{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}}
|
||||||
|
{{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}}
|
||||||
|
{{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}}
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col width="50">
|
<col width="50">
|
||||||
<col width="50">
|
<col width="50">
|
||||||
|
@ -5,33 +5,13 @@
|
|||||||
{{if $commit.OnlyRelation}}
|
{{if $commit.OnlyRelation}}
|
||||||
<span></span>
|
<span></span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="sha" id="{{$commit.ShortRev}}">
|
{{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}}
|
||||||
{{$class := "ui sha label"}}
|
|
||||||
{{if $commit.Commit.Signature}}
|
<span class="message tw-inline-block gt-ellipsis">
|
||||||
{{$class = (print $class " isSigned")}}
|
|
||||||
{{if $commit.Verification.Verified}}
|
|
||||||
{{if eq $commit.Verification.TrustStatus "trusted"}}
|
|
||||||
{{$class = (print $class " isVerified")}}
|
|
||||||
{{else if eq $commit.Verification.TrustStatus "untrusted"}}
|
|
||||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
|
||||||
{{else}}
|
|
||||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
|
||||||
{{end}}
|
|
||||||
{{else if $commit.Verification.Warning}}
|
|
||||||
{{$class = (print $class " isWarning")}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
<a href="{{$.RepoLink}}/commit/{{$commit.Rev|PathEscape}}" rel="nofollow" class="{{$class}}">
|
|
||||||
<span class="shortsha">{{ShortSha $commit.Commit.ID.String}}</span>
|
|
||||||
{{- if $commit.Commit.Signature -}}
|
|
||||||
{{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}}
|
|
||||||
{{- end -}}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span class="message tw-inline-block gt-ellipsis tw-mr-2">
|
|
||||||
<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
|
<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="commit-refs tw-flex tw-items-center tw-mr-1">
|
|
||||||
|
<span class="commit-refs flex-text-inline">
|
||||||
{{range $commit.Refs}}
|
{{range $commit.Refs}}
|
||||||
{{$refGroup := .RefGroup}}
|
{{$refGroup := .RefGroup}}
|
||||||
{{if eq $refGroup "pull"}}
|
{{if eq $refGroup "pull"}}
|
||||||
@ -56,7 +36,8 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
<span class="author tw-flex tw-items-center tw-mr-2 tw-gap-1">
|
|
||||||
|
<span class="author flex-text-inline">
|
||||||
{{$userName := $commit.Commit.Author.Name}}
|
{{$userName := $commit.Commit.Author.Name}}
|
||||||
{{if $commit.User}}
|
{{if $commit.User}}
|
||||||
{{if and $commit.User.FullName DefaultShowFullName}}
|
{{if and $commit.User.FullName DefaultShowFullName}}
|
||||||
@ -69,7 +50,8 @@
|
|||||||
{{$userName}}
|
{{$userName}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
<span class="time tw-flex tw-items-center">{{DateUtils.FullTime $commit.Date}}</span>
|
|
||||||
|
<span class="time flex-text-inline">{{DateUtils.FullTime $commit.Date}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<div class="latest-commit">
|
||||||
{{if not .LatestCommit}}
|
{{if not .LatestCommit}}
|
||||||
…
|
…
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -14,13 +15,11 @@
|
|||||||
<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
|
<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified}} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{PathEscape .LatestCommit.ID.String}}">
|
|
||||||
<span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span>
|
{{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}}
|
||||||
{{if .LatestCommit.Signature}}
|
|
||||||
{{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}}
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}}
|
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}}
|
||||||
|
|
||||||
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
|
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
|
||||||
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
|
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
|
||||||
{{if IsMultilineCommitMessage .LatestCommit.Message}}
|
{{if IsMultilineCommitMessage .LatestCommit.Message}}
|
||||||
@ -29,3 +28,4 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<div class="ui detail icon button">
|
|
||||||
{{if .verification.Verified}}
|
|
||||||
<div title="{{if eq .verification.TrustStatus "trusted"}}{{else if eq .verification.TrustStatus "untrusted"}}{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.verification.Reason}}">
|
|
||||||
{{if ne .verification.SigningUser.ID 0}}
|
|
||||||
{{svg "gitea-lock"}}
|
|
||||||
{{ctx.AvatarUtils.Avatar .verification.SigningUser 16 "signature"}}
|
|
||||||
{{else}}
|
|
||||||
<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog"}}</span>
|
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .verification.SigningEmail "" 16 "signature"}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<span title="{{ctx.Locale.Tr .verification.Reason}}">{{svg "gitea-unlock"}}</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
@ -12,9 +12,7 @@
|
|||||||
|
|
||||||
{{if not .ReadmeInList}}
|
{{if not .ReadmeInList}}
|
||||||
<div id="repo-file-commit-box" class="ui segment list-header tw-mb-4 tw-flex tw-justify-between">
|
<div id="repo-file-commit-box" class="ui segment list-header tw-mb-4 tw-flex tw-justify-between">
|
||||||
<div class="latest-commit">
|
{{template "repo/latest_commit" .}}
|
||||||
{{template "repo/latest_commit" .}}
|
|
||||||
</div>
|
|
||||||
{{if .LatestCommit}}
|
{{if .LatestCommit}}
|
||||||
{{if .LatestCommit.Committer}}
|
{{if .LatestCommit.Committer}}
|
||||||
<div class="text grey age">
|
<div class="text grey age">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
|
{{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
|
||||||
<div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
<div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
||||||
<div class="repo-file-line repo-file-last-commit">
|
<div class="repo-file-line repo-file-last-commit">
|
||||||
<div class="latest-commit">{{template "repo/latest_commit" .}}</div>
|
{{template "repo/latest_commit" .}}
|
||||||
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .HasParentPath}}
|
{{if .HasParentPath}}
|
||||||
|
@ -92,6 +92,9 @@
|
|||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .ShowMoreOrgs}}
|
||||||
|
<li><a class="tw-align-center" href="{{.ContextUser.HomeLink}}?tab=organizations" data-tooltip-content="{{ctx.Locale.Tr "user.show_more"}}">{{svg "octicon-kebab-horizontal" 28 "icon tw-p-1"}}</a></li>
|
||||||
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
{{template "repo/user_cards" .}}
|
{{template "repo/user_cards" .}}
|
||||||
{{else if eq .TabName "overview"}}
|
{{else if eq .TabName "overview"}}
|
||||||
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
|
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
|
||||||
|
{{else if eq .TabName "organizations"}}
|
||||||
|
{{template "repo/user_cards" .}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{template "shared/repo_search" .}}
|
{{template "shared/repo_search" .}}
|
||||||
{{template "explore/repo_list" .}}
|
{{template "explore/repo_list" .}}
|
||||||
|
410
tests/integration/actions_job_test.go
Normal file
410
tests/integration/actions_job_test.go
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJobWithNeeds(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
treePath string
|
||||||
|
fileContent string
|
||||||
|
outcomes map[string]*mockTaskOutcome
|
||||||
|
expectedStatuses map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/job-with-needs.yml",
|
||||||
|
fileContent: `name: job-with-needs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/job-with-needs.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo job2
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
},
|
||||||
|
"job2": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatuses: map[string]string{
|
||||||
|
"job1": actions_model.StatusSuccess.String(),
|
||||||
|
"job2": actions_model.StatusSuccess.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/job-with-needs-fail.yml",
|
||||||
|
fileContent: `name: job-with-needs-fail
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/job-with-needs-fail.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo job2
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1": {
|
||||||
|
result: runnerv1.Result_RESULT_FAILURE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatuses: map[string]string{
|
||||||
|
"job1": actions_model.StatusFailure.String(),
|
||||||
|
"job2": actions_model.StatusSkipped.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/job-with-needs-fail-if.yml",
|
||||||
|
fileContent: `name: job-with-needs-fail-if
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/job-with-needs-fail-if.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo job2
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1": {
|
||||||
|
result: runnerv1.Result_RESULT_FAILURE,
|
||||||
|
},
|
||||||
|
"job2": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatuses: map[string]string{
|
||||||
|
"job1": actions_model.StatusFailure.String(),
|
||||||
|
"job2": actions_model.StatusSuccess.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
session := loginUser(t, user2.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
|
||||||
|
apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false)
|
||||||
|
runner := newMockRunner()
|
||||||
|
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||||
|
// create the workflow file
|
||||||
|
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||||
|
fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||||
|
|
||||||
|
// fetch and execute task
|
||||||
|
for i := 0; i < len(tc.outcomes); i++ {
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||||
|
outcome := tc.outcomes[jobName]
|
||||||
|
assert.NotNil(t, outcome)
|
||||||
|
runner.execTask(t, task, outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check result
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var actionTaskRespAfter api.ActionTaskResponse
|
||||||
|
DecodeJSON(t, resp, &actionTaskRespAfter)
|
||||||
|
for _, apiTask := range actionTaskRespAfter.Entries {
|
||||||
|
if apiTask.HeadSHA != fileResp.Commit.SHA {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status := apiTask.Status
|
||||||
|
assert.Equal(t, status, tc.expectedStatuses[apiTask.Name])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
doAPIDeleteRepository(httpContext)(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobNeedsMatrix(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
treePath string
|
||||||
|
fileContent string
|
||||||
|
outcomes map[string]*mockTaskOutcome
|
||||||
|
expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
|
||||||
|
fileContent: `name: jobs-outputs-with-matrix
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||||
|
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||||
|
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: [1, 2, 3]
|
||||||
|
steps:
|
||||||
|
- name: Generate output
|
||||||
|
id: gen_output
|
||||||
|
run: |
|
||||||
|
version="${{ matrix.version }}"
|
||||||
|
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1 (1)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (2)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "2",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (3)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||||
|
"job1": {
|
||||||
|
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "2",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
|
||||||
|
fileContent: `name: jobs-outputs-with-matrix-failure
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||||
|
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||||
|
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: [1, 2, 3]
|
||||||
|
steps:
|
||||||
|
- name: Generate output
|
||||||
|
id: gen_output
|
||||||
|
run: |
|
||||||
|
version="${{ matrix.version }}"
|
||||||
|
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [job1]
|
||||||
|
steps:
|
||||||
|
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||||
|
`,
|
||||||
|
outcomes: map[string]*mockTaskOutcome{
|
||||||
|
"job1 (1)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (2)": {
|
||||||
|
result: runnerv1.Result_RESULT_FAILURE,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job1 (3)": {
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
outputs: map[string]string{
|
||||||
|
"output_1": "",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||||
|
"job1": {
|
||||||
|
Result: runnerv1.Result_RESULT_FAILURE,
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"output_1": "1",
|
||||||
|
"output_2": "",
|
||||||
|
"output_3": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
session := loginUser(t, user2.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
|
||||||
|
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
|
||||||
|
runner := newMockRunner()
|
||||||
|
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||||
|
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||||
|
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||||
|
|
||||||
|
for i := 0; i < len(tc.outcomes); i++ {
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||||
|
outcome := tc.outcomes[jobName]
|
||||||
|
assert.NotNil(t, outcome)
|
||||||
|
runner.execTask(t, task, outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
actualTaskNeeds := task.Needs
|
||||||
|
assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
|
||||||
|
for jobID, tn := range tc.expectedTaskNeeds {
|
||||||
|
actualNeed := actualTaskNeeds[jobID]
|
||||||
|
assert.Equal(t, tn.Result, actualNeed.Result)
|
||||||
|
assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
|
||||||
|
for outputKey, outputValue := range tn.Outputs {
|
||||||
|
assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
doAPIDeleteRepository(httpContext)(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository {
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||||
|
Name: repoName,
|
||||||
|
Private: isPrivate,
|
||||||
|
Readme: "Default",
|
||||||
|
AutoInit: true,
|
||||||
|
DefaultBranch: "main",
|
||||||
|
}).AddTokenAuth(authToken)
|
||||||
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
|
var apiRepo api.Repository
|
||||||
|
DecodeJSON(t, resp, &apiRepo)
|
||||||
|
return &apiRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions {
|
||||||
|
return &api.CreateFileOptions{
|
||||||
|
FileOptions: api.FileOptions{
|
||||||
|
BranchName: branch,
|
||||||
|
Message: msg,
|
||||||
|
Author: api.Identity{
|
||||||
|
Name: u.Name,
|
||||||
|
Email: u.Email,
|
||||||
|
},
|
||||||
|
Committer: api.Identity{
|
||||||
|
Name: u.Name,
|
||||||
|
Email: u.Email,
|
||||||
|
},
|
||||||
|
Dates: api.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse {
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts).
|
||||||
|
AddTokenAuth(authToken)
|
||||||
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
|
var fileResponse api.FileResponse
|
||||||
|
DecodeJSON(t, resp, &fileResponse)
|
||||||
|
return &fileResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTaskJobNameByTaskID get the job name of the task by task ID
|
||||||
|
// there is currently not an API for querying a task by ID so we have to list all the tasks
|
||||||
|
func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string {
|
||||||
|
// FIXME: we may need to query several pages
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)).
|
||||||
|
AddTokenAuth(authToken)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var taskRespBefore api.ActionTaskResponse
|
||||||
|
DecodeJSON(t, resp, &taskRespBefore)
|
||||||
|
for _, apiTask := range taskRespBefore.Entries {
|
||||||
|
if apiTask.ID == taskID {
|
||||||
|
return apiTask.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
159
tests/integration/actions_log_test.go
Normal file
159
tests/integration/actions_log_test.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
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/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDownloadTaskLogs(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
testCases := []struct {
|
||||||
|
treePath string
|
||||||
|
fileContent string
|
||||||
|
outcome *mockTaskOutcome
|
||||||
|
zstdEnabled bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
|
||||||
|
fileContent: `name: download-task-logs-zstd
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/download-task-logs-zstd.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1 with zstd enabled
|
||||||
|
`,
|
||||||
|
outcome: &mockTaskOutcome{
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
logRows: []*runnerv1.LogRow{
|
||||||
|
{
|
||||||
|
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||||
|
Content: " \U0001F433 docker create image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Time: timestamppb.New(now.Add(2 * time.Second)),
|
||||||
|
Content: "job1 zstd enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Time: timestamppb.New(now.Add(3 * time.Second)),
|
||||||
|
Content: "\U0001F3C1 Job succeeded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
zstdEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
|
||||||
|
fileContent: `name: download-task-logs-no-zstd
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/download-task-logs-no-zstd.yml'
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1 with zstd disabled
|
||||||
|
`,
|
||||||
|
outcome: &mockTaskOutcome{
|
||||||
|
result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
|
logRows: []*runnerv1.LogRow{
|
||||||
|
{
|
||||||
|
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||||
|
Content: " \U0001F433 docker create image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||||
|
Content: "job1 zstd disabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||||
|
Content: "\U0001F3C1 Job succeeded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
zstdEnabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
session := loginUser(t, user2.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
|
||||||
|
apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", false)
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||||
|
runner := newMockRunner()
|
||||||
|
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||||
|
var resetFunc func()
|
||||||
|
if tc.zstdEnabled {
|
||||||
|
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd")
|
||||||
|
assert.True(t, setting.Actions.LogCompression.IsZstd())
|
||||||
|
} else {
|
||||||
|
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none")
|
||||||
|
assert.False(t, setting.Actions.LogCompression.IsZstd())
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the workflow file
|
||||||
|
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||||
|
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
||||||
|
|
||||||
|
// fetch and execute task
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
runner.execTask(t, task, tc.outcome)
|
||||||
|
|
||||||
|
// check whether the log file exists
|
||||||
|
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id)
|
||||||
|
if setting.Actions.LogCompression.IsZstd() {
|
||||||
|
logFileName += ".zst"
|
||||||
|
}
|
||||||
|
_, err := storage.Actions.Stat(logFileName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// download task logs and check content
|
||||||
|
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||||
|
assert.Len(t, logTextLines, len(tc.outcome.logRows))
|
||||||
|
for idx, lr := range tc.outcome.logRows {
|
||||||
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
||||||
|
logTextLines[idx],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFunc()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
doAPIDeleteRepository(httpContext)(t)
|
||||||
|
})
|
||||||
|
}
|
157
tests/integration/actions_runner_test.go
Normal file
157
tests/integration/actions_runner_test.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||||
|
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockRunner struct {
|
||||||
|
client *mockRunnerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockRunnerClient struct {
|
||||||
|
pingServiceClient pingv1connect.PingServiceClient
|
||||||
|
runnerServiceClient runnerv1connect.RunnerServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRunner() *mockRunner {
|
||||||
|
client := newMockRunnerClient("", "")
|
||||||
|
return &mockRunner{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRunnerClient(uuid, token string) *mockRunnerClient {
|
||||||
|
baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL)
|
||||||
|
|
||||||
|
opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
|
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||||
|
if uuid != "" {
|
||||||
|
req.Header().Set("x-runner-uuid", uuid)
|
||||||
|
}
|
||||||
|
if token != "" {
|
||||||
|
req.Header().Set("x-runner-token", token)
|
||||||
|
}
|
||||||
|
return next(ctx, req)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
client := &mockRunnerClient{
|
||||||
|
pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt),
|
||||||
|
runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt),
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRunner) doPing(t *testing.T) {
|
||||||
|
resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{
|
||||||
|
Data: "mock-runner",
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) {
|
||||||
|
r.doPing(t)
|
||||||
|
resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{
|
||||||
|
Name: name,
|
||||||
|
Token: token,
|
||||||
|
Version: "mock-runner-version",
|
||||||
|
Labels: labels,
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) {
|
||||||
|
session := loginUser(t, ownerName)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var registrationToken struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
DecodeJSON(t, resp, ®istrationToken)
|
||||||
|
r.doRegister(t, runnerName, registrationToken.Token, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
|
||||||
|
fetchTimeout := 10 * time.Second
|
||||||
|
if len(timeout) > 0 {
|
||||||
|
fetchTimeout = timeout[0]
|
||||||
|
}
|
||||||
|
ddl := time.Now().Add(fetchTimeout)
|
||||||
|
var task *runnerv1.Task
|
||||||
|
for time.Now().Before(ddl) {
|
||||||
|
resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||||
|
TasksVersion: 0,
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if resp.Msg.Task != nil {
|
||||||
|
task = resp.Msg.Task
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
assert.NotNil(t, task, "failed to fetch a task")
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockTaskOutcome struct {
|
||||||
|
result runnerv1.Result
|
||||||
|
outputs map[string]string
|
||||||
|
logRows []*runnerv1.LogRow
|
||||||
|
execTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) {
|
||||||
|
for idx, lr := range outcome.logRows {
|
||||||
|
resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||||
|
TaskId: task.Id,
|
||||||
|
Index: int64(idx),
|
||||||
|
Rows: []*runnerv1.LogRow{lr},
|
||||||
|
NoMore: idx == len(outcome.logRows)-1,
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, idx+1, resp.Msg.AckIndex)
|
||||||
|
}
|
||||||
|
sentOutputKeys := make([]string, 0, len(outcome.outputs))
|
||||||
|
for outputKey, outputValue := range outcome.outputs {
|
||||||
|
resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||||
|
State: &runnerv1.TaskState{
|
||||||
|
Id: task.Id,
|
||||||
|
Result: runnerv1.Result_RESULT_UNSPECIFIED,
|
||||||
|
},
|
||||||
|
Outputs: map[string]string{outputKey: outputValue},
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
sentOutputKeys = append(sentOutputKeys, outputKey)
|
||||||
|
assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs)
|
||||||
|
}
|
||||||
|
time.Sleep(outcome.execTime)
|
||||||
|
resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||||
|
State: &runnerv1.TaskState{
|
||||||
|
Id: task.Id,
|
||||||
|
Result: outcome.result,
|
||||||
|
StoppedAt: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, outcome.result, resp.Msg.State.Result)
|
||||||
|
}
|
@ -117,27 +117,33 @@ func TestAPIAddIssueLabels(t *testing.T) {
|
|||||||
func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) {
|
func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) {
|
||||||
assert.NoError(t, unittest.LoadFixtures())
|
assert.NoError(t, unittest.LoadFixtures())
|
||||||
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID})
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID})
|
||||||
|
orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID})
|
||||||
|
|
||||||
session := loginUser(t, owner.Name)
|
user1Session := loginUser(t, "user1")
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue)
|
||||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels",
|
|
||||||
repo.OwnerName, repo.Name, issue.Index)
|
// add the org label and the repo label to the issue
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index)
|
||||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
||||||
Labels: []any{"label1", "label2"},
|
Labels: []any{repoLabel.Name, orgLabel.Name},
|
||||||
}).AddTokenAuth(token)
|
}).AddTokenAuth(token)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var apiLabels []*api.Label
|
var apiLabels []*api.Label
|
||||||
DecodeJSON(t, resp, &apiLabels)
|
DecodeJSON(t, resp, &apiLabels)
|
||||||
assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
|
assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
|
||||||
|
|
||||||
var apiLabelNames []string
|
var apiLabelNames []string
|
||||||
for _, label := range apiLabels {
|
for _, label := range apiLabels {
|
||||||
apiLabelNames = append(apiLabelNames, label.Name)
|
apiLabelNames = append(apiLabelNames, label.Name)
|
||||||
}
|
}
|
||||||
assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"})
|
assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name})
|
||||||
|
|
||||||
|
// delete labels
|
||||||
|
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIReplaceIssueLabels(t *testing.T) {
|
func TestAPIReplaceIssueLabels(t *testing.T) {
|
||||||
|
@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
|
|||||||
user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
|
activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
|
||||||
|
activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode)
|
||||||
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
||||||
"password": "examplePassword!1",
|
"password": "examplePassword!1",
|
||||||
})
|
})
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
@ -226,3 +227,21 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) {
|
|||||||
assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url)
|
assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateAgitPullWithReadPermission(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
dstPath := t.TempDir()
|
||||||
|
|
||||||
|
u.Path = "user2/repo1.git"
|
||||||
|
u.User = url.UserPassword("user4", userPassword)
|
||||||
|
|
||||||
|
t.Run("Clone", doGitClone(dstPath, u))
|
||||||
|
|
||||||
|
t.Run("add commit", doGitAddSomeCommits(dstPath, "master"))
|
||||||
|
|
||||||
|
t.Run("do agit pull create", func(t *testing.T) {
|
||||||
|
err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + "test-topic").Run(&git.RunOpts{Dir: dstPath})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user