mirror of https://github.com/go-gitea/gitea.git
Compare commits
11 Commits
7d14dfcab7
...
51e4888d2e
Author | SHA1 | Date |
---|---|---|
Lunny Xiao | 51e4888d2e | |
yp05327 | e94723f2de | |
wxiaoguang | ed0fc2729e | |
yp05327 | f7d2f695a4 | |
wxiaoguang | 3fdb2d4ad8 | |
Zettat123 | f09e68ec33 | |
Lunny Xiao | a303c973e0 | |
Lunny Xiao | f5f921c095 | |
silverwind | d9b37d085a | |
GiteaBot | f1b0729078 | |
Kemal Zebari | 880e0b7c82 |
|
@ -61,3 +61,4 @@ kerwin612 <kerwin612@qq.com> (@kerwin612)
|
|||
Gary Wang <git@blumia.net> (@BLumia)
|
||||
Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
|
||||
Yu Liu <1240335630@qq.com> (@HEREYUA)
|
||||
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
|
||||
|
|
|
@ -57,6 +57,7 @@ type Engine interface {
|
|||
SumInt(bean any, columnName string) (res int64, err error)
|
||||
Sync(...any) error
|
||||
Select(string) *xorm.Session
|
||||
SetExpr(string, any) *xorm.Session
|
||||
NotIn(string, ...any) *xorm.Session
|
||||
OrderBy(any, ...any) *xorm.Session
|
||||
Exist(...any) (bool, error)
|
||||
|
|
|
@ -5,11 +5,11 @@ package issues
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// LoadProject load the project the issue was assigned to
|
||||
|
@ -90,58 +90,73 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m
|
|||
return issuesMap, nil
|
||||
}
|
||||
|
||||
// ChangeProjectAssign changes the project associated with an issue
|
||||
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
// IssueAssignOrRemoveProject changes the project associated with an issue
|
||||
// If newProjectID is 0, the issue is removed from the project
|
||||
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
oldProjectID := issue.projectID(ctx)
|
||||
|
||||
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
oldProjectID := issue.projectID(ctx)
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only check if we add a new project and not remove it.
|
||||
if newProjectID > 0 {
|
||||
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
|
||||
if err != nil {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID {
|
||||
return fmt.Errorf("issue's repository is not the same as project's repository")
|
||||
|
||||
// Only check if we add a new project and not remove it.
|
||||
if newProjectID > 0 {
|
||||
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) {
|
||||
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
||||
}
|
||||
if newColumnID == 0 {
|
||||
newDefaultColumn, err := newProject.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newColumnID = newDefaultColumn.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldProjectID > 0 || newProjectID > 0 {
|
||||
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeProject,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldProjectID: oldProjectID,
|
||||
ProjectID: newProjectID,
|
||||
}); err != nil {
|
||||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
if oldProjectID > 0 || newProjectID > 0 {
|
||||
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeProject,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldProjectID: oldProjectID,
|
||||
ProjectID: newProjectID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if newProjectID == 0 {
|
||||
return nil
|
||||
}
|
||||
if newColumnID == 0 {
|
||||
panic("newColumnID must not be zero") // shouldn't happen
|
||||
}
|
||||
|
||||
res := struct {
|
||||
MaxSorting int64
|
||||
IssueCount int64
|
||||
}{}
|
||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
|
||||
Where("project_id=?", newProjectID).
|
||||
And("project_board_id=?", newColumnID).
|
||||
Get(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
ProjectBoardID: newColumnID,
|
||||
Sorting: newSorting,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ package project
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -82,6 +84,17 @@ func (b *Board) NumIssues(ctx context.Context) int {
|
|||
return int(c)
|
||||
}
|
||||
|
||||
func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
|
||||
issues := make([]*ProjectIssue, 0, 5)
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID).
|
||||
And("project_board_id=?", b.ID).
|
||||
OrderBy("sorting, id").
|
||||
Find(&issues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Board))
|
||||
}
|
||||
|
@ -150,12 +163,27 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
|
|||
return db.Insert(ctx, boards)
|
||||
}
|
||||
|
||||
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
|
||||
// because sorting is int8 in database
|
||||
const maxProjectColumns = 20
|
||||
|
||||
// NewBoard adds a new project board to a given project
|
||||
func NewBoard(ctx context.Context, board *Board) error {
|
||||
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
|
||||
return fmt.Errorf("bad color code: %s", board.Color)
|
||||
}
|
||||
|
||||
res := struct {
|
||||
MaxSorting int64
|
||||
ColumnCount int64
|
||||
}{}
|
||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
|
||||
Where("project_id=?", board.ProjectID).Get(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
if res.ColumnCount >= maxProjectColumns {
|
||||
return fmt.Errorf("NewBoard: maximum number of columns reached")
|
||||
}
|
||||
board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
|
||||
_, err := db.GetEngine(ctx).Insert(board)
|
||||
return err
|
||||
}
|
||||
|
@ -189,7 +217,17 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
|
|||
return fmt.Errorf("deleteBoardByID: cannot delete default board")
|
||||
}
|
||||
|
||||
if err = board.removeIssues(ctx); err != nil {
|
||||
// move all issues to the default column
|
||||
project, err := GetProjectByID(ctx, board.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defaultColumn, err := project.GetDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -242,21 +280,15 @@ func UpdateBoard(ctx context.Context, board *Board) error {
|
|||
// GetBoards fetches all boards related to a project
|
||||
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
||||
boards := make([]*Board, 0, 5)
|
||||
|
||||
if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultB, err := p.getDefaultBoard(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append([]*Board{defaultB}, boards...), nil
|
||||
return boards, nil
|
||||
}
|
||||
|
||||
// getDefaultBoard return default board and ensure only one exists
|
||||
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
|
||||
// GetDefaultBoard return default board and ensure only one exists
|
||||
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
|
||||
var board Board
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("project_id=? AND `default` = ?", p.ID, true).
|
||||
|
@ -316,3 +348,42 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) {
|
||||
columns := make([]*Board, 0, 5)
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("project_id =?", projectID).
|
||||
In("id", columnsIDs).
|
||||
OrderBy("sorting").Find(&columns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// MoveColumnsOnProject sorts columns in a project
|
||||
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
columnIDs := util.ValuesOfMap(sortedColumnIDs)
|
||||
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(movedColumns) != len(sortedColumnIDs) {
|
||||
return errors.New("some columns do not exist")
|
||||
}
|
||||
|
||||
for _, column := range movedColumns {
|
||||
if column.ProjectID != project.ID {
|
||||
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
|
||||
}
|
||||
}
|
||||
|
||||
for sorting, columnID := range sortedColumnIDs {
|
||||
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -19,7 +21,7 @@ func TestGetDefaultBoard(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// check if default board was added
|
||||
board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
|
||||
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(5), board.ProjectID)
|
||||
assert.Equal(t, "Uncategorized", board.Title)
|
||||
|
@ -28,7 +30,7 @@ func TestGetDefaultBoard(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// check if multiple defaults were removed
|
||||
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
|
||||
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.Equal(t, int64(9), board.ID)
|
||||
|
@ -42,3 +44,84 @@ func TestGetDefaultBoard(t *testing.T) {
|
|||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.False(t, board.Default)
|
||||
}
|
||||
|
||||
func Test_moveIssuesToAnotherColumn(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1})
|
||||
|
||||
issues, err := column1.GetIssues(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issues, 1)
|
||||
assert.EqualValues(t, 1, issues[0].ID)
|
||||
|
||||
column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1})
|
||||
issues, err = column2.GetIssues(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issues, 1)
|
||||
assert.EqualValues(t, 3, issues[0].ID)
|
||||
|
||||
err = column1.moveIssuesToAnotherColumn(db.DefaultContext, column2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issues, err = column1.GetIssues(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issues, 0)
|
||||
|
||||
issues, err = column2.GetIssues(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issues, 2)
|
||||
assert.EqualValues(t, 3, issues[0].ID)
|
||||
assert.EqualValues(t, 0, issues[0].Sorting)
|
||||
assert.EqualValues(t, 1, issues[1].ID)
|
||||
assert.EqualValues(t, 1, issues[1].Sorting)
|
||||
}
|
||||
|
||||
func Test_MoveColumnsOnProject(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||
columns, err := project1.GetBoards(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
|
||||
assert.EqualValues(t, 0, columns[1].Sorting)
|
||||
assert.EqualValues(t, 0, columns[2].Sorting)
|
||||
|
||||
err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{
|
||||
0: columns[1].ID,
|
||||
1: columns[2].ID,
|
||||
2: columns[0].ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
columnsAfter, err := project1.GetBoards(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnsAfter, 3)
|
||||
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
||||
assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID)
|
||||
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
|
||||
}
|
||||
|
||||
func Test_NewBoard(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||
columns, err := project1.GetBoards(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
|
||||
for i := 0; i < maxProjectColumns-3; i++ {
|
||||
err := NewBoard(db.DefaultContext, &Board{
|
||||
Title: fmt.Sprintf("board-%d", i+4),
|
||||
ProjectID: project1.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
err = NewBoard(db.DefaultContext, &Board{
|
||||
Title: "board-21",
|
||||
ProjectID: project1.ID,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached"))
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ProjectIssue saves relation from issue to a project
|
||||
|
@ -17,7 +18,7 @@ type ProjectIssue struct { //revive:disable-line:exported
|
|||
IssueID int64 `xorm:"INDEX"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
|
||||
// If 0, then it has not been added to a specific board in the project
|
||||
// ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
|
||||
ProjectBoardID int64 `xorm:"INDEX"`
|
||||
|
||||
// the sorting order on the board
|
||||
|
@ -79,11 +80,8 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
|
|||
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
issueIDs := util.ValuesOfMap(sortedIssueIDs)
|
||||
|
||||
issueIDs := make([]int64, 0, len(sortedIssueIDs))
|
||||
for _, issueID := range sortedIssueIDs {
|
||||
issueIDs = append(issueIDs, issueID)
|
||||
}
|
||||
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -102,7 +100,44 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
|
|||
})
|
||||
}
|
||||
|
||||
func (b *Board) removeIssues(ctx context.Context) error {
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID)
|
||||
return err
|
||||
func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error {
|
||||
if b.ProjectID != newColumn.ProjectID {
|
||||
return fmt.Errorf("columns have to be in the same project")
|
||||
}
|
||||
|
||||
if b.ID == newColumn.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := struct {
|
||||
MaxSorting int64
|
||||
IssueCount int64
|
||||
}{}
|
||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").
|
||||
Table("project_issue").
|
||||
Where("project_id=?", newColumn.ProjectID).
|
||||
And("project_board_id=?", newColumn.ID).
|
||||
Get(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issues, err := b.GetIssues(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for i, issue := range issues {
|
||||
issue.ProjectBoardID = newColumn.ID
|
||||
issue.Sorting = nextSorting + int64(i)
|
||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -161,6 +161,13 @@ func (p *Project) IsRepositoryProject() bool {
|
|||
return p.Type == TypeRepository
|
||||
}
|
||||
|
||||
func (p *Project) CanBeAccessedByOwnerRepo(ownerID int64, repo *repo_model.Repository) bool {
|
||||
if p.Type == TypeRepository {
|
||||
return repo != nil && p.RepoID == repo.ID // if a project belongs to a repository, then its OwnerID is 0 and can be ignored
|
||||
}
|
||||
return p.OwnerID == ownerID && p.RepoID == 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Project))
|
||||
}
|
||||
|
|
|
@ -436,6 +436,7 @@ oauth_signin_submit=绑定账号
|
|||
oauth.signin.error=处理授权请求时出错。 如果此错误仍然存在,请联系站点管理员。
|
||||
oauth.signin.error.access_denied=授权请求被拒绝。
|
||||
oauth.signin.error.temporarily_unavailable=授权失败,因为认证服务器暂时不可用。请稍后再试。
|
||||
oauth_callback_unable_auto_reg=自动注册已启用,但OAuth2 提供商 %[1]s 返回缺失的字段:%[2]s,无法自动创建帐户,请创建或链接到一个帐户,或联系站点管理员。
|
||||
openid_connect_submit=连接
|
||||
openid_connect_title=连接到现有的帐户
|
||||
openid_connect_desc=所选的 OpenID URI 未知。在这里关联一个新帐户。
|
||||
|
@ -763,6 +764,8 @@ manage_themes=选择默认主题
|
|||
manage_openid=管理 OpenID 地址
|
||||
email_desc=您的主要电子邮件地址将用于通知、密码恢复,基于网页界面的Git操作(只要它不是设置为隐藏的)。
|
||||
theme_desc=这将是您在整个网站上的默认主题。
|
||||
theme_colorblindness_help=颜色障碍主题支持
|
||||
theme_colorblindness_prompt=Gitea 只能获得一些基本的颜色障碍支持,这些主题只定义了少数颜色。 这项工作仍在进行中,可以通过在主题的 CSS 文件中定义更多颜色来做更多的改进。
|
||||
primary=主要
|
||||
activated=已激活
|
||||
requires_activation=需要激活
|
||||
|
@ -1810,7 +1813,7 @@ pulls.is_empty=此分支上的更改已经在目标分支上。这将是一个
|
|||
pulls.required_status_check_failed=一些必要的检查没有成功
|
||||
pulls.required_status_check_missing=缺少一些必要的检查。
|
||||
pulls.required_status_check_administrator=作为管理员,您仍可合并此合并请求
|
||||
pulls.blocked_by_approvals=此合并请求没有通过审批。已获取审批数%d个,共需要审批数%d个。
|
||||
pulls.blocked_by_approvals=此合并请求还没有足够的批准。已获批准数 %d 个,需获批准数 %d 个。
|
||||
pulls.blocked_by_rejection=此合并请求有官方审核员请求的更改。
|
||||
pulls.blocked_by_official_review_requests=此合并请求需要官方评审。
|
||||
pulls.blocked_by_outdated_branch=此合并请求因过期而被阻止。
|
||||
|
@ -1884,14 +1887,14 @@ pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容
|
|||
pulls.auto_merge_button_when_succeed=(当检查成功时)
|
||||
pulls.auto_merge_when_succeed=在所有检查成功后自动合并
|
||||
pulls.auto_merge_newly_scheduled=合并请求计划在所有检查成功后合并。
|
||||
pulls.auto_merge_has_pending_schedule=%[1]s 安排此拉取请求在所有检查成功时自动合并 %[2]s。
|
||||
pulls.auto_merge_has_pending_schedule=%[1]s 于 %[2]s 设置此合并请求在所有检查成功时自动合并 。
|
||||
|
||||
pulls.auto_merge_cancel_schedule=取消自动合并
|
||||
pulls.auto_merge_not_scheduled=此拉取请求没有计划自动合并。
|
||||
pulls.auto_merge_canceled_schedule=此拉取请求的自动合并已取消。
|
||||
pulls.auto_merge_not_scheduled=此合并请求没有计划自动合并。
|
||||
pulls.auto_merge_canceled_schedule=此合并请求的自动合并已取消。
|
||||
|
||||
pulls.auto_merge_newly_scheduled_comment=`已安排此拉取请求在所有检查成功后自动合并 %[1]s`
|
||||
pulls.auto_merge_canceled_schedule_comment=`已取消当所有检查成功后自动合并此拉取请求 %[1]s`
|
||||
pulls.auto_merge_newly_scheduled_comment=`已于 %[1]s 设置此拉取请求在所有检查成功后自动合并`
|
||||
pulls.auto_merge_canceled_schedule_comment=`已于 %[1]s 取消了自动合并设置 `
|
||||
|
||||
pulls.delete.title=删除此拉取请求?
|
||||
pulls.delete.text=你真的要删除这个拉取请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它)
|
||||
|
@ -3331,7 +3334,7 @@ reopen_pull_request=`重新开启了合并请求 <a href="%[1]s">%[3]s#%[2]s</a>
|
|||
comment_issue=`评论了工单 <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
comment_pull=`评论了合并请求 <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
merge_pull_request=`合并了合并请求 <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
auto_merge_pull_request=`自动合并了拉取请求 <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
auto_merge_pull_request=`自动合并了合并请求 <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
transfer_repo=将仓库 <code>%s</code> 转移至 <a href="%s">%s</a>
|
||||
push_tag=推送了标签 <a href="%[2]s">%[3]s</a> 至仓库 <a href="%[1]s">%[4]s</a>
|
||||
delete_tag=从<a href="%[1]s">%[3]s</a> 删除了标签 %[2]s
|
||||
|
@ -3492,6 +3495,7 @@ npm.install=要使用 npm 安装软件包,请运行以下命令:
|
|||
npm.install2=或将其添加到 package.json 文件:
|
||||
npm.dependencies=依赖项
|
||||
npm.dependencies.development=开发依赖
|
||||
npm.dependencies.bundle=已绑定的依赖关系
|
||||
npm.dependencies.peer=Peer 依赖
|
||||
npm.dependencies.optional=可选依赖
|
||||
npm.details.tag=标签
|
||||
|
|
|
@ -881,7 +881,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
// start with merging by checking
|
||||
if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
|
||||
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
|
||||
if errors.Is(err, pull_service.ErrIsClosed) {
|
||||
ctx.NotFound()
|
||||
} else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
|
||||
|
@ -890,7 +890,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
|
||||
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged")
|
||||
} else if errors.Is(err, pull_service.ErrNotMergableState) {
|
||||
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
|
||||
} else if models.IsErrDisallowedToMerge(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -390,74 +389,6 @@ func ViewProject(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplProjectsView)
|
||||
}
|
||||
|
||||
func getActionIssues(ctx *context.Context) issues_model.IssueList {
|
||||
commaSeparatedIssueIDs := ctx.FormString("issue_ids")
|
||||
if len(commaSeparatedIssueIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
issueIDs := make([]int64, 0, 10)
|
||||
for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
|
||||
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
|
||||
if err != nil {
|
||||
ctx.ServerError("ParseInt", err)
|
||||
return nil
|
||||
}
|
||||
issueIDs = append(issueIDs, issueID)
|
||||
}
|
||||
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssuesByIDs", err)
|
||||
return nil
|
||||
}
|
||||
// Check access rights for all issues
|
||||
issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
for _, issue := range issues {
|
||||
if issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
|
||||
return nil
|
||||
}
|
||||
if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
|
||||
ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
|
||||
return nil
|
||||
}
|
||||
if err = issue.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
// UpdateIssueProject change an issue's project
|
||||
func UpdateIssueProject(ctx *context.Context) {
|
||||
issues := getActionIssues(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issues.LoadProjects(ctx); err != nil {
|
||||
ctx.ServerError("LoadProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
projectID := ctx.FormInt64("id")
|
||||
for _, issue := range issues {
|
||||
if issue.Project != nil {
|
||||
if issue.Project.ID == projectID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// DeleteProjectBoard allows for the deletion of a project board
|
||||
func DeleteProjectBoard(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
@ -383,17 +384,21 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
ctx.ServerError("LoadProjects", err)
|
||||
return
|
||||
}
|
||||
if _, err := issues.LoadRepositories(ctx); err != nil {
|
||||
ctx.ServerError("LoadProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
projectID := ctx.FormInt64("id")
|
||||
for _, issue := range issues {
|
||||
if issue.Project != nil {
|
||||
if issue.Project.ID == projectID {
|
||||
if issue.Project != nil && issue.Project.ID == projectID {
|
||||
continue
|
||||
}
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1007,7 +1007,7 @@ func MergePullRequest(ctx *context.Context) {
|
|||
}
|
||||
|
||||
// start with merging by checking
|
||||
if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
|
||||
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, pull_service.ErrIsClosed):
|
||||
if issue.IsPull {
|
||||
|
@ -1021,7 +1021,7 @@ func MergePullRequest(ctx *context.Context) {
|
|||
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
|
||||
case errors.Is(err, pull_service.ErrIsWorkInProgress):
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
|
||||
case errors.Is(err, pull_service.ErrNotMergableState):
|
||||
case errors.Is(err, pull_service.ErrNotMergeableState):
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
||||
case models.IsErrDisallowedToMerge(err):
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
||||
|
@ -1329,14 +1329,12 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if projectID > 0 {
|
||||
if !ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||
ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects")
|
||||
return
|
||||
}
|
||||
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
return
|
||||
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
|
||||
if !errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -789,6 +789,7 @@ func SettingsPost(ctx *context.Context) {
|
|||
ctx.Repo.GitRepo = nil
|
||||
}
|
||||
|
||||
oldFullname := repo.FullName()
|
||||
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
|
||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
|
||||
|
@ -803,8 +804,13 @@ func SettingsPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
|
||||
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
|
||||
log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
|
||||
} else {
|
||||
log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
|
||||
}
|
||||
ctx.Redirect(repo.Link() + "/settings")
|
||||
|
||||
case "cancel_transfer":
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// MoveColumns moves or keeps columns in a project and sorts them inside that project
|
||||
func MoveColumns(ctx *context.Context) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) {
|
||||
ctx.NotFound("CanBeAccessedByOwnerRepo", nil)
|
||||
return
|
||||
}
|
||||
|
||||
type movedColumnsForm struct {
|
||||
Columns []struct {
|
||||
ColumnID int64 `json:"columnID"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
} `json:"columns"`
|
||||
}
|
||||
|
||||
form := &movedColumnsForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedColumnsForm", err)
|
||||
return
|
||||
}
|
||||
|
||||
sortedColumnIDs := make(map[int64]int64)
|
||||
for _, column := range form.Columns {
|
||||
sortedColumnIDs[column.Sorting] = column.ColumnID
|
||||
}
|
||||
|
||||
if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
|
||||
ctx.ServerError("MoveColumnsOnProject", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
|
@ -37,6 +37,7 @@ import (
|
|||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
|
||||
"code.gitea.io/gitea/routers/web/shared/project"
|
||||
"code.gitea.io/gitea/routers/web/user"
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/routers/web/user/setting/security"
|
||||
|
@ -999,6 +1000,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
||||
m.Group("/{id}", func() {
|
||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
|
||||
m.Post("/move", project.MoveColumns)
|
||||
m.Post("/delete", org.DeleteProject)
|
||||
|
||||
m.Get("/edit", org.RenderEditProject)
|
||||
|
@ -1354,6 +1356,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
||||
m.Group("/{id}", func() {
|
||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
|
||||
m.Post("/move", project.MoveColumns)
|
||||
m.Post("/delete", repo.DeleteProject)
|
||||
|
||||
m.Get("/edit", repo.RenderEditProject)
|
||||
|
|
|
@ -276,12 +276,12 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
|
||||
if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
|
||||
if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) {
|
||||
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
|
||||
return
|
||||
}
|
||||
log.Error("%-v CheckPullMergable: %v", pr, err)
|
||||
log.Error("%-v CheckPullMergeable: %v", pr, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -152,3 +152,19 @@ func (r *indexerNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
|
|||
func (r *indexerNotifier) IssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
|
||||
issue_indexer.UpdateIssueIndexer(ctx, issue.ID)
|
||||
}
|
||||
|
||||
func (r *indexerNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
log.Error("LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
issue_indexer.UpdateIssueIndexer(ctx, pr.Issue.ID)
|
||||
}
|
||||
|
||||
func (r *indexerNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
log.Error("LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
issue_indexer.UpdateIssueIndexer(ctx, pr.Issue.ID)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
|
|||
}
|
||||
}
|
||||
if projectID > 0 {
|
||||
if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectID, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ var (
|
|||
ErrHasMerged = errors.New("has already been merged")
|
||||
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
||||
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
|
||||
ErrNotMergableState = errors.New("not in mergeable state")
|
||||
ErrNotMergeableState = errors.New("not in mergeable state")
|
||||
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
|
||||
)
|
||||
|
||||
|
@ -66,8 +66,8 @@ const (
|
|||
MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed
|
||||
)
|
||||
|
||||
// CheckPullMergable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
|
||||
func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error {
|
||||
// CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
|
||||
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error {
|
||||
return db.WithTx(stdCtx, func(ctx context.Context) error {
|
||||
if pr.HasMerged {
|
||||
return ErrHasMerged
|
||||
|
@ -97,7 +97,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
|
|||
}
|
||||
|
||||
if !pr.CanAutoMerge() && !pr.IsEmpty() {
|
||||
return ErrNotMergableState
|
||||
return ErrNotMergeableState
|
||||
}
|
||||
|
||||
if pr.IsChecking() {
|
||||
|
|
|
@ -36,10 +36,6 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
|||
}
|
||||
}
|
||||
|
||||
if len(opts.DefaultBranch) == 0 {
|
||||
opts.DefaultBranch = setting.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
repo := &repo_model.Repository{
|
||||
OwnerID: u.ID,
|
||||
Owner: u,
|
||||
|
@ -81,7 +77,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
|||
}
|
||||
|
||||
if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("createDelegateHooks: %w", err)
|
||||
return fmt.Errorf("adoptRepository: %w", err)
|
||||
}
|
||||
|
||||
if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
|
@ -143,6 +139,21 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo
|
|||
}
|
||||
}
|
||||
|
||||
// Don't bother looking this repo in the context it won't be there
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("openRepository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil {
|
||||
return fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
|
||||
}
|
||||
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
||||
}
|
||||
|
||||
branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
|
||||
RepoID: repo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
|
@ -183,26 +194,10 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo
|
|||
return fmt.Errorf("setDefaultBranch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = repo_module.UpdateRepository(ctx, repo, false); err != nil {
|
||||
return fmt.Errorf("updateRepository: %w", err)
|
||||
}
|
||||
|
||||
// Don't bother looking this repo in the context it won't be there
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("openRepository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil {
|
||||
return fmt.Errorf("SyncRepoBranches: %w", err)
|
||||
}
|
||||
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
|
||||
<div id="project-board">
|
||||
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
|
||||
<div class="board {{if .CanWriteProjects}}sortable{{end}}"{{if .CanWriteProjects}} data-url="{{$.Link}}/move"{{end}}>
|
||||
{{range .Columns}}
|
||||
<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
||||
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
{{if and (not .Issue.IsPull) (not .PageIsComparePull)}}
|
||||
<input id="ref_selector" name="ref" type="hidden" value="{{.Reference}}">
|
||||
<input id="editing_mode" name="edit_mode" type="hidden" value="{{(or .IsIssueWriter .HasIssuesOrPullsWritePermission)}}">
|
||||
<form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form">
|
||||
{{$.CsrfTokenHtml}}
|
||||
</form>
|
||||
<div class="ui dropdown select-branch branch-selector-dropdown ellipsis-items-nowrap {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}"
|
||||
data-no-results="{{ctx.Locale.Tr "no_results_found"}}"
|
||||
{{if not .Issue}}data-for-new-issue="true"{{end}}
|
||||
{{if and .Issue (or .IsIssueWriter .HasIssuesOrPullsWritePermission)}}data-url-update-issueref="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref"{{end}}
|
||||
>
|
||||
<div class="ui button branch-dropdown-button">
|
||||
<span class="text-branch-name gt-ellipsis">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
|
||||
|
|
|
@ -5,17 +5,17 @@ package integration
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
)
|
||||
|
||||
func TestOrgProjectAccess(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// disable repo project unit
|
||||
unit_model.DisabledRepoUnits = []unit_model.Type{unit_model.TypeProjects}
|
||||
defer test.MockVariableValue(&unit_model.DisabledRepoUnits, append(slices.Clone(unit_model.DisabledRepoUnits), unit_model.TypeProjects))()
|
||||
|
||||
// repo project, 404
|
||||
req := NewRequest(t, "GET", "/user2/repo1/projects")
|
||||
|
|
|
@ -4,10 +4,18 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrivateRepoProject(t *testing.T) {
|
||||
|
@ -21,3 +29,59 @@ func TestPrivateRepoProject(t *testing.T) {
|
|||
req = NewRequest(t, "GET", "/user31/-/projects")
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestMoveRepoProjectColumns(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
|
||||
projectsUnit := repo2.MustGetUnit(db.DefaultContext, unit.TypeProjects)
|
||||
assert.True(t, projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo))
|
||||
|
||||
project1 := project_model.Project{
|
||||
Title: "new created project",
|
||||
RepoID: repo2.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
BoardType: project_model.BoardTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(db.DefaultContext, &project1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err = project_model.NewBoard(db.DefaultContext, &project_model.Board{
|
||||
Title: fmt.Sprintf("column %d", i+1),
|
||||
ProjectID: project1.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
columns, err := project1.GetBoards(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.EqualValues(t, 0, columns[0].Sorting)
|
||||
assert.EqualValues(t, 1, columns[1].Sorting)
|
||||
assert.EqualValues(t, 2, columns[2].Sorting)
|
||||
|
||||
sess := loginUser(t, "user1")
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/projects/%d", repo2.FullName(), project1.ID))
|
||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/projects/%d/move?_csrf="+htmlDoc.GetCSRF(), repo2.FullName(), project1.ID), map[string]any{
|
||||
"columns": []map[string]any{
|
||||
{"columnID": columns[1].ID, "sorting": 0},
|
||||
{"columnID": columns[2].ID, "sorting": 1},
|
||||
{"columnID": columns[0].ID, "sorting": 2},
|
||||
},
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
columnsAfter, err := project1.GetBoards(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
||||
assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID)
|
||||
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
|
||||
|
||||
assert.NoError(t, project_model.DeleteProjectByID(db.DefaultContext, project1.ID))
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
@ -587,3 +588,63 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
|
|||
assert.EqualValues(t, "Closed", prStatus)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullMergeIndexerNotifier(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
// create a pull request
|
||||
session := loginUser(t, "user1")
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
||||
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
||||
createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull")
|
||||
|
||||
assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0))
|
||||
time.Sleep(time.Second)
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
||||
OwnerName: "user2",
|
||||
Name: "repo1",
|
||||
})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
RepoID: repo1.ID,
|
||||
Title: "Indexer notifier test pull",
|
||||
IsPull: true,
|
||||
IsClosed: false,
|
||||
})
|
||||
|
||||
// build the request for searching issues
|
||||
link, _ := url.Parse("/api/v1/repos/issues/search")
|
||||
query := url.Values{}
|
||||
query.Add("state", "closed")
|
||||
query.Add("type", "pulls")
|
||||
query.Add("q", "notifier")
|
||||
link.RawQuery = query.Encode()
|
||||
|
||||
// search issues
|
||||
searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
var apiIssuesBefore []*api.Issue
|
||||
DecodeJSON(t, searchIssuesResp, &apiIssuesBefore)
|
||||
assert.Len(t, apiIssuesBefore, 0)
|
||||
|
||||
// merge the pull request
|
||||
elem := strings.Split(test.RedirectURL(createPullResp), "/")
|
||||
assert.EqualValues(t, "pulls", elem[3])
|
||||
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
|
||||
|
||||
// check if the issue is closed
|
||||
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
ID: issue.ID,
|
||||
})
|
||||
assert.True(t, issue.IsClosed)
|
||||
|
||||
assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0))
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// search issues again
|
||||
searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
var apiIssuesAfter []*api.Issue
|
||||
DecodeJSON(t, searchIssuesResp, &apiIssuesAfter)
|
||||
if assert.Len(t, apiIssuesAfter, 1) {
|
||||
assert.Equal(t, issue.ID, apiIssuesAfter[0].ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -358,6 +358,7 @@
|
|||
}
|
||||
|
||||
.ui.vertical.menu .active.item {
|
||||
background: var(--color-active);
|
||||
border-radius: 0;
|
||||
}
|
||||
.ui.vertical.menu > .active.item:first-child {
|
||||
|
|
|
@ -102,10 +102,6 @@ export async function createMonaco(textarea, filename, editorOpts) {
|
|||
},
|
||||
});
|
||||
|
||||
// Quick fix: https://github.com/microsoft/monaco-editor/issues/2962
|
||||
monaco.languages.register({id: 'vs.editor.nullLanguage'});
|
||||
monaco.languages.setLanguageConfiguration('vs.editor.nullLanguage', {});
|
||||
|
||||
const editor = monaco.editor.create(container, {
|
||||
value: textarea.value,
|
||||
theme: 'gitea',
|
||||
|
|
|
@ -58,32 +58,27 @@ export function initRepoCommentForm() {
|
|||
function initBranchSelector() {
|
||||
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
|
||||
if (!elSelectBranch) return;
|
||||
const isForNewIssue = elSelectBranch.getAttribute('data-for-new-issue') === 'true';
|
||||
|
||||
const urlUpdateIssueRef = elSelectBranch.getAttribute('data-url-update-issueref');
|
||||
const $selectBranch = $(elSelectBranch);
|
||||
const $branchMenu = $selectBranch.find('.reference-list-menu');
|
||||
$branchMenu.find('.item:not(.no-select)').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
const selectedValue = $(this).data('id'); // eg: "refs/heads/my-branch"
|
||||
const editMode = $('#editing_mode').val();
|
||||
$($(this).data('id-selector')).val(selectedValue);
|
||||
if (isForNewIssue) {
|
||||
elSelectBranch.querySelector('.text-branch-name').textContent = this.getAttribute('data-name');
|
||||
return; // only update UI&form, do not send request/reload
|
||||
}
|
||||
|
||||
if (editMode === 'true') {
|
||||
const form = document.getElementById('update_issueref_form');
|
||||
const params = new URLSearchParams();
|
||||
params.append('ref', selectedValue);
|
||||
const selectedValue = this.getAttribute('data-id'); // eg: "refs/heads/my-branch"
|
||||
const selectedText = this.getAttribute('data-name'); // eg: "my-branch"
|
||||
if (urlUpdateIssueRef) {
|
||||
// for existing issue, send request to update issue ref, and reload page
|
||||
try {
|
||||
await POST(form.getAttribute('action'), {data: params});
|
||||
await POST(urlUpdateIssueRef, {data: new URLSearchParams({ref: selectedValue})});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else if (editMode === '') {
|
||||
$selectBranch.find('.ui .branch-name').text(selectedValue);
|
||||
} else {
|
||||
// for new issue, only update UI&form, do not send request/reload
|
||||
const selectedHiddenSelector = this.getAttribute('data-id-selector');
|
||||
document.querySelector(selectedHiddenSelector).value = selectedValue;
|
||||
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
||||
}
|
||||
});
|
||||
$selectBranch.find('.reference.column').on('click', function () {
|
||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'jquery';
|
|||
import {contrastColor} from '../utils/color.js';
|
||||
import {createSortable} from '../modules/sortable.js';
|
||||
import {POST, DELETE, PUT} from '../modules/fetch.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
function updateIssueCount(cards) {
|
||||
const parent = cards.parentElement;
|
||||
|
@ -63,17 +62,20 @@ async function initRepoProjectSortable() {
|
|||
delay: 500,
|
||||
onSort: async () => {
|
||||
boardColumns = mainBoard.getElementsByClassName('project-column');
|
||||
for (let i = 0; i < boardColumns.length; i++) {
|
||||
const column = boardColumns[i];
|
||||
if (parseInt(column.getAttribute('data-sorting')) !== i) {
|
||||
try {
|
||||
const bgColor = column.style.backgroundColor; // will be rgb() string
|
||||
const color = bgColor ? tinycolor(bgColor).toHexString() : '';
|
||||
await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const columnSorting = {
|
||||
columns: Array.from(boardColumns, (column, i) => ({
|
||||
columnID: parseInt(column.getAttribute('data-id')),
|
||||
sorting: i,
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
await POST(mainBoard.getAttribute('data-url'), {
|
||||
data: columnSorting,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue