mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-08 18:04:14 +01:00
Merge pull request '[gitea] week 2024-20 cherry pick (gitea-github/main -> forgejo)' (#3729) from earl-warren/wcp/2024-20 into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3729 Reviewed-by: twenty-panda <twenty-panda@noreply.codeberg.org>
This commit is contained in:
commit
1b3e6a4831
55 changed files with 806 additions and 328 deletions
|
@ -186,7 +186,6 @@ package "code.gitea.io/gitea/modules/git"
|
||||||
func (ErrExecTimeout).Error
|
func (ErrExecTimeout).Error
|
||||||
func (ErrUnsupportedVersion).Error
|
func (ErrUnsupportedVersion).Error
|
||||||
func SetUpdateHook
|
func SetUpdateHook
|
||||||
func GetObjectFormatOfRepo
|
|
||||||
func openRepositoryWithDefaultContext
|
func openRepositoryWithDefaultContext
|
||||||
func IsTagExist
|
func IsTagExist
|
||||||
func ToEntryMode
|
func ToEntryMode
|
||||||
|
@ -325,8 +324,6 @@ package "code.gitea.io/gitea/routers/web"
|
||||||
|
|
||||||
package "code.gitea.io/gitea/routers/web/org"
|
package "code.gitea.io/gitea/routers/web/org"
|
||||||
func MustEnableProjects
|
func MustEnableProjects
|
||||||
func getActionIssues
|
|
||||||
func UpdateIssueProject
|
|
||||||
|
|
||||||
package "code.gitea.io/gitea/services/context"
|
package "code.gitea.io/gitea/services/context"
|
||||||
func GetPrivateContext
|
func GetPrivateContext
|
||||||
|
|
|
@ -366,6 +366,7 @@ Forgejo or set your environment appropriately.`, "")
|
||||||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||||||
repoName := os.Getenv(repo_module.EnvRepoName)
|
repoName := os.Getenv(repo_module.EnvRepoName)
|
||||||
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||||||
|
prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
|
||||||
pusherName := os.Getenv(repo_module.EnvPusherName)
|
pusherName := os.Getenv(repo_module.EnvPusherName)
|
||||||
|
|
||||||
hookOptions := private.HookOptions{
|
hookOptions := private.HookOptions{
|
||||||
|
@ -375,6 +376,8 @@ Forgejo or set your environment appropriately.`, "")
|
||||||
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
|
||||||
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
|
||||||
GitPushOptions: pushOptions(),
|
GitPushOptions: pushOptions(),
|
||||||
|
PullRequestID: prID,
|
||||||
|
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
|
||||||
}
|
}
|
||||||
oldCommitIDs := make([]string, hookBatchSize)
|
oldCommitIDs := make([]string, hookBatchSize)
|
||||||
newCommitIDs := make([]string, hookBatchSize)
|
newCommitIDs := make([]string, hookBatchSize)
|
||||||
|
|
|
@ -34,7 +34,7 @@ var CmdMigrateStorage = &cli.Command{
|
||||||
Name: "type",
|
Name: "type",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Value: "",
|
Value: "",
|
||||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log'",
|
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "storage",
|
Name: "storage",
|
||||||
|
@ -160,6 +160,13 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||||
|
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
|
||||||
|
_, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func runMigrateStorage(ctx *cli.Context) error {
|
func runMigrateStorage(ctx *cli.Context) error {
|
||||||
stdCtx, cancel := installSignals()
|
stdCtx, cancel := installSignals()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -223,13 +230,14 @@ func runMigrateStorage(ctx *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{
|
migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{
|
||||||
"attachments": migrateAttachments,
|
"attachments": migrateAttachments,
|
||||||
"lfs": migrateLFS,
|
"lfs": migrateLFS,
|
||||||
"avatars": migrateAvatars,
|
"avatars": migrateAvatars,
|
||||||
"repo-avatars": migrateRepoAvatars,
|
"repo-avatars": migrateRepoAvatars,
|
||||||
"repo-archivers": migrateRepoArchivers,
|
"repo-archivers": migrateRepoArchivers,
|
||||||
"packages": migratePackages,
|
"packages": migratePackages,
|
||||||
"actions-log": migrateActionsLog,
|
"actions-log": migrateActionsLog,
|
||||||
|
"actions-artifacts": migrateActionsArtifacts,
|
||||||
}
|
}
|
||||||
|
|
||||||
tp := strings.ToLower(ctx.String("type"))
|
tp := strings.ToLower(ctx.String("type"))
|
||||||
|
|
|
@ -58,6 +58,7 @@ type Engine interface {
|
||||||
SumInt(bean any, columnName string) (res int64, err error)
|
SumInt(bean any, columnName string) (res int64, err error)
|
||||||
Sync(...any) error
|
Sync(...any) error
|
||||||
Select(string) *xorm.Session
|
Select(string) *xorm.Session
|
||||||
|
SetExpr(string, any) *xorm.Session
|
||||||
NotIn(string, ...any) *xorm.Session
|
NotIn(string, ...any) *xorm.Session
|
||||||
OrderBy(any, ...any) *xorm.Session
|
OrderBy(any, ...any) *xorm.Session
|
||||||
Exist(...any) (bool, error)
|
Exist(...any) (bool, error)
|
||||||
|
|
|
@ -5,11 +5,11 @@ package issues
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
project_model "code.gitea.io/gitea/models/project"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadProject load the project the issue was assigned to
|
// 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
|
return issuesMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeProjectAssign changes the project associated with an issue
|
// IssueAssignOrRemoveProject changes the project associated with an issue
|
||||||
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
// If newProjectID is 0, the issue is removed from the project
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
|
||||||
if err != nil {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
return err
|
oldProjectID := issue.projectID(ctx)
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
|
if err := issue.LoadRepo(ctx); 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 {
|
|
||||||
return err
|
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 {
|
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
if oldProjectID > 0 || newProjectID > 0 {
|
||||||
IssueID: issue.ID,
|
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
||||||
ProjectID: newProjectID,
|
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,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
-
|
-
|
||||||
id: 1
|
id: 1
|
||||||
|
user_id: 1
|
||||||
|
pull_id: 1
|
||||||
commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
|
commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
|
||||||
|
|
|
@ -19,21 +19,21 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) {
|
||||||
|
|
||||||
type CommitStatus struct {
|
type CommitStatus struct {
|
||||||
ID int64
|
ID int64
|
||||||
ContextHash string
|
ContextHash string `xorm:"char(40) index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoArchiver struct {
|
type RepoArchiver struct {
|
||||||
ID int64
|
ID int64
|
||||||
RepoID int64
|
RepoID int64 `xorm:"index unique(s)"`
|
||||||
Type int
|
Type int `xorm:"unique(s)"`
|
||||||
CommitID string
|
CommitID string `xorm:"VARCHAR(40) unique(s)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReviewState struct {
|
type ReviewState struct {
|
||||||
ID int64
|
ID int64
|
||||||
CommitSHA string
|
UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"`
|
||||||
UserID int64
|
PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"`
|
||||||
PullID int64
|
CommitSHA string `xorm:"NOT NULL VARCHAR(40) UNIQUE(pull_commit_user)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
|
|
|
@ -5,12 +5,14 @@ package project
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
@ -82,6 +84,17 @@ func (b *Board) NumIssues(ctx context.Context) int {
|
||||||
return int(c)
|
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() {
|
func init() {
|
||||||
db.RegisterModel(new(Board))
|
db.RegisterModel(new(Board))
|
||||||
}
|
}
|
||||||
|
@ -150,12 +163,27 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
|
||||||
return db.Insert(ctx, boards)
|
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
|
// NewBoard adds a new project board to a given project
|
||||||
func NewBoard(ctx context.Context, board *Board) error {
|
func NewBoard(ctx context.Context, board *Board) error {
|
||||||
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
|
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
|
||||||
return fmt.Errorf("bad color code: %s", 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)
|
_, err := db.GetEngine(ctx).Insert(board)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -189,7 +217,17 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
|
||||||
return fmt.Errorf("deleteBoardByID: cannot delete default board")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,21 +280,15 @@ func UpdateBoard(ctx context.Context, board *Board) error {
|
||||||
// GetBoards fetches all boards related to a project
|
// GetBoards fetches all boards related to a project
|
||||||
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
||||||
boards := make([]*Board, 0, 5)
|
boards := make([]*Board, 0, 5)
|
||||||
|
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
|
||||||
if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultB, err := p.getDefaultBoard(ctx)
|
return boards, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append([]*Board{defaultB}, boards...), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDefaultBoard return default board and ensure only one exists
|
// GetDefaultBoard return default board and ensure only one exists
|
||||||
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
|
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
|
||||||
var board Board
|
var board Board
|
||||||
has, err := db.GetEngine(ctx).
|
has, err := db.GetEngine(ctx).
|
||||||
Where("project_id=? AND `default` = ?", p.ID, true).
|
Where("project_id=? AND `default` = ?", p.ID, true).
|
||||||
|
@ -316,3 +348,42 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
|
||||||
return nil
|
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
|
package project
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -19,7 +21,7 @@ func TestGetDefaultBoard(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// check if default board was added
|
// check if default board was added
|
||||||
board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
|
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(5), board.ProjectID)
|
assert.Equal(t, int64(5), board.ProjectID)
|
||||||
assert.Equal(t, "Uncategorized", board.Title)
|
assert.Equal(t, "Uncategorized", board.Title)
|
||||||
|
@ -28,7 +30,7 @@ func TestGetDefaultBoard(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// check if multiple defaults were removed
|
// check if multiple defaults were removed
|
||||||
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
|
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(6), board.ProjectID)
|
assert.Equal(t, int64(6), board.ProjectID)
|
||||||
assert.Equal(t, int64(9), board.ID)
|
assert.Equal(t, int64(9), board.ID)
|
||||||
|
@ -42,3 +44,84 @@ func TestGetDefaultBoard(t *testing.T) {
|
||||||
assert.Equal(t, int64(6), board.ProjectID)
|
assert.Equal(t, int64(6), board.ProjectID)
|
||||||
assert.False(t, board.Default)
|
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/models/db"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProjectIssue saves relation from issue to a project
|
// ProjectIssue saves relation from issue to a project
|
||||||
|
@ -17,7 +18,7 @@ type ProjectIssue struct { //revive:disable-line:exported
|
||||||
IssueID int64 `xorm:"INDEX"`
|
IssueID int64 `xorm:"INDEX"`
|
||||||
ProjectID 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"`
|
ProjectBoardID int64 `xorm:"INDEX"`
|
||||||
|
|
||||||
// the sorting order on the board
|
// 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 {
|
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
sess := db.GetEngine(ctx)
|
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()
|
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -102,7 +100,44 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) removeIssues(ctx context.Context) error {
|
func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error {
|
||||||
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID)
|
if b.ProjectID != newColumn.ProjectID {
|
||||||
return err
|
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
|
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() {
|
func init() {
|
||||||
db.RegisterModel(new(Project))
|
db.RegisterModel(new(Project))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import "code.gitea.io/gitea/models/db"
|
||||||
// SearchOrderByMap represents all possible search order
|
// SearchOrderByMap represents all possible search order
|
||||||
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
|
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||||
"asc": {
|
"asc": {
|
||||||
"alpha": db.SearchOrderByAlphabetically,
|
"alpha": "owner_name ASC, name ASC",
|
||||||
"created": db.SearchOrderByOldest,
|
"created": db.SearchOrderByOldest,
|
||||||
"updated": db.SearchOrderByLeastUpdated,
|
"updated": db.SearchOrderByLeastUpdated,
|
||||||
"size": db.SearchOrderBySize,
|
"size": db.SearchOrderBySize,
|
||||||
"id": db.SearchOrderByID,
|
"id": db.SearchOrderByID,
|
||||||
},
|
},
|
||||||
"desc": {
|
"desc": {
|
||||||
"alpha": db.SearchOrderByAlphabeticallyReverse,
|
"alpha": "owner_name DESC, name DESC",
|
||||||
"created": db.SearchOrderByNewest,
|
"created": db.SearchOrderByNewest,
|
||||||
"updated": db.SearchOrderByRecentUpdated,
|
"updated": db.SearchOrderByRecentUpdated,
|
||||||
"size": db.SearchOrderBySizeReverse,
|
"size": db.SearchOrderBySizeReverse,
|
||||||
|
|
|
@ -7,7 +7,6 @@ package git
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -63,32 +62,6 @@ func IsRepoURLAccessible(ctx context.Context, url string) bool {
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectFormatOfRepo returns the hash type of repository at a given path
|
|
||||||
func GetObjectFormatOfRepo(ctx context.Context, repoPath string) (ObjectFormat, error) {
|
|
||||||
var stdout, stderr strings.Builder
|
|
||||||
|
|
||||||
err := NewCommand(ctx, "hash-object", "--stdin").Run(&RunOpts{
|
|
||||||
Dir: repoPath,
|
|
||||||
Stdout: &stdout,
|
|
||||||
Stderr: &stderr,
|
|
||||||
Stdin: &strings.Reader{},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if stderr.Len() > 0 {
|
|
||||||
return nil, errors.New(stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err := NewIDFromString(strings.TrimRight(stdout.String(), "\n"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Type(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitRepository initializes a new Git repository.
|
// InitRepository initializes a new Git repository.
|
||||||
func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error {
|
func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error {
|
||||||
err := os.MkdirAll(repoPath, os.ModePerm)
|
err := os.MkdirAll(repoPath, os.ModePerm)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ type HookOptions struct {
|
||||||
GitQuarantinePath string
|
GitQuarantinePath string
|
||||||
GitPushOptions GitPushOptions
|
GitPushOptions GitPushOptions
|
||||||
PullRequestID int64
|
PullRequestID int64
|
||||||
|
PushTrigger repository.PushTrigger
|
||||||
DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user.
|
DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user.
|
||||||
IsWiki bool
|
IsWiki bool
|
||||||
ActionPerm int
|
ActionPerm int
|
||||||
|
|
|
@ -5,6 +5,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
@ -36,6 +37,15 @@ func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) {
|
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) {
|
||||||
|
objFmt, err := gitRepo.GetObjectFormat()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("GetObjectFormat: %w", err)
|
||||||
|
}
|
||||||
|
_, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
allBranches := container.Set[string]{}
|
allBranches := container.Set[string]{}
|
||||||
{
|
{
|
||||||
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
||||||
|
|
31
modules/repository/branch_test.go
Normal file
31
modules/repository/branch_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSyncRepoBranches(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
_, err := db.GetEngine(db.DefaultContext).ID(1).Update(&repo_model.Repository{ObjectFormatName: "bad-fmt"})
|
||||||
|
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &git_model.Branch{}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
assert.Equal(t, "bad-fmt", repo.ObjectFormatName)
|
||||||
|
_, err = SyncRepoBranches(db.DefaultContext, 1, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
assert.Equal(t, "sha1", repo.ObjectFormatName)
|
||||||
|
branch, err := git_model.GetBranch(db.DefaultContext, 1, "master")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "master", branch.Name)
|
||||||
|
}
|
|
@ -25,11 +25,19 @@ const (
|
||||||
EnvKeyID = "GITEA_KEY_ID" // public key ID
|
EnvKeyID = "GITEA_KEY_ID" // public key ID
|
||||||
EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID"
|
EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID"
|
||||||
EnvPRID = "GITEA_PR_ID"
|
EnvPRID = "GITEA_PR_ID"
|
||||||
|
EnvPushTrigger = "GITEA_PUSH_TRIGGER"
|
||||||
EnvIsInternal = "GITEA_INTERNAL_PUSH"
|
EnvIsInternal = "GITEA_INTERNAL_PUSH"
|
||||||
EnvAppURL = "GITEA_ROOT_URL"
|
EnvAppURL = "GITEA_ROOT_URL"
|
||||||
EnvActionPerm = "GITEA_ACTION_PERM"
|
EnvActionPerm = "GITEA_ACTION_PERM"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PushTrigger string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PushTriggerPRMergeToBase PushTrigger = "pr-merge-to-base"
|
||||||
|
PushTriggerPRUpdateWithBase PushTrigger = "pr-update-with-base"
|
||||||
|
)
|
||||||
|
|
||||||
// InternalPushingEnvironment returns an os environment to switch off hooks on push
|
// InternalPushingEnvironment returns an os environment to switch off hooks on push
|
||||||
// It is recommended to avoid using this unless you are pushing within a transaction
|
// It is recommended to avoid using this unless you are pushing within a transaction
|
||||||
// or if you absolutely are sure that post-receive and pre-receive will do nothing
|
// or if you absolutely are sure that post-receive and pre-receive will do nothing
|
||||||
|
|
1
release-notes/8.0.0/feat/3729.md
Normal file
1
release-notes/8.0.0/feat/3729.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
- [PR](https://github.com/go-gitea/gitea/pull/30874): add actions-artifacts to the [storage migrate CLI](https://forgejo.org/docs/v8.0/admin/command-line/#migrate).
|
2
release-notes/8.0.0/fix/3729.md
Normal file
2
release-notes/8.0.0/fix/3729.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
- [PR](https://github.com/go-gitea/gitea/pull/30912): when adopting a repository, the default branch is not taken into account
|
||||||
|
- [PR](https://github.com/go-gitea/gitea/pull/30715): pull request search shows closed pull requests in the open tab
|
|
@ -23,8 +23,6 @@ import (
|
||||||
const (
|
const (
|
||||||
uuidHeaderKey = "x-runner-uuid"
|
uuidHeaderKey = "x-runner-uuid"
|
||||||
tokenHeaderKey = "x-runner-token"
|
tokenHeaderKey = "x-runner-token"
|
||||||
// Deprecated: will be removed after Gitea 1.20 released.
|
|
||||||
versionHeaderKey = "x-runner-version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc {
|
var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc {
|
||||||
|
@ -35,9 +33,6 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
|
||||||
}
|
}
|
||||||
uuid := request.Header().Get(uuidHeaderKey)
|
uuid := request.Header().Get(uuidHeaderKey)
|
||||||
token := request.Header().Get(tokenHeaderKey)
|
token := request.Header().Get(tokenHeaderKey)
|
||||||
// TODO: version will be removed from request header after Gitea 1.20 released.
|
|
||||||
// And Gitea will not try to read version from request header
|
|
||||||
version := request.Header().Get(versionHeaderKey)
|
|
||||||
|
|
||||||
runner, err := actions_model.GetRunnerByUUID(ctx, uuid)
|
runner, err := actions_model.GetRunnerByUUID(ctx, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,14 +46,6 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
|
||||||
}
|
}
|
||||||
|
|
||||||
cols := []string{"last_online"}
|
cols := []string{"last_online"}
|
||||||
|
|
||||||
// TODO: version will be removed from request header after Gitea 1.20 released.
|
|
||||||
// And Gitea will not try to read version from request header
|
|
||||||
version, _ = util.SplitStringAtByteN(version, 64)
|
|
||||||
if !util.IsEmptyString(version) && runner.Version != version {
|
|
||||||
runner.Version = version
|
|
||||||
cols = append(cols, "version")
|
|
||||||
}
|
|
||||||
runner.LastOnline = timeutil.TimeStampNow()
|
runner.LastOnline = timeutil.TimeStampNow()
|
||||||
if methodName == "UpdateTask" || methodName == "UpdateLog" {
|
if methodName == "UpdateTask" || methodName == "UpdateLog" {
|
||||||
runner.LastActive = timeutil.TimeStampNow()
|
runner.LastActive = timeutil.TimeStampNow()
|
||||||
|
|
|
@ -69,12 +69,6 @@ func (s *Service) Register(
|
||||||
}
|
}
|
||||||
|
|
||||||
labels := req.Msg.Labels
|
labels := req.Msg.Labels
|
||||||
// TODO: agent_labels should be removed from pb after Gitea 1.20 released.
|
|
||||||
// Old version runner's agent_labels slice is not empty and labels slice is empty.
|
|
||||||
// And due to compatibility with older versions, it is temporarily marked as Deprecated in pb, so use `//nolint` here.
|
|
||||||
if len(req.Msg.AgentLabels) > 0 && len(req.Msg.Labels) == 0 { //nolint:staticcheck
|
|
||||||
labels = req.Msg.AgentLabels //nolint:staticcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new runner
|
// create new runner
|
||||||
name, _ := util.SplitStringAtByteN(req.Msg.Name, 255)
|
name, _ := util.SplitStringAtByteN(req.Msg.Name, 255)
|
||||||
|
|
|
@ -878,7 +878,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// start with merging by checking
|
// 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) {
|
if errors.Is(err, pull_service.ErrIsClosed) {
|
||||||
ctx.NotFound()
|
ctx.NotFound()
|
||||||
} else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
|
} else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
|
||||||
|
@ -887,7 +887,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||||
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
|
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
|
||||||
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
|
} 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")
|
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")
|
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
|
||||||
} else if models.IsErrDisallowedToMerge(err) {
|
} else if models.IsErrDisallowedToMerge(err) {
|
||||||
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
|
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
|
||||||
|
|
|
@ -4,20 +4,26 @@
|
||||||
package private
|
package private
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"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/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/private"
|
"code.gitea.io/gitea/modules/private"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
timeutil "code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
|
@ -155,6 +161,14 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle pull request merging, a pull request action should push at least 1 commit
|
||||||
|
if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase {
|
||||||
|
handlePullRequestMerging(ctx, opts, ownerName, repoName, updates)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Push Options
|
// Handle Push Options
|
||||||
if len(opts.GitPushOptions) > 0 {
|
if len(opts.GitPushOptions) > 0 {
|
||||||
// load the repository
|
// load the repository
|
||||||
|
@ -302,3 +316,52 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
||||||
RepoWasEmpty: wasEmpty,
|
RepoWasEmpty: wasEmpty,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) {
|
||||||
|
return cache.GetWithContextCache(ctx, "hook_post_receive_user", id, func() (*user_model.User, error) {
|
||||||
|
return user_model.GetUserByID(ctx, id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
|
||||||
|
func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||||
|
Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pusher, err := loadContextCacheUser(ctx, opts.UserID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.MergedCommitID = updates[len(updates)-1].NewCommitID
|
||||||
|
pr.MergedUnix = timeutil.TimeStampNow()
|
||||||
|
pr.Merger = pusher
|
||||||
|
pr.MergerID = pusher.ID
|
||||||
|
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
// Removing an auto merge pull and ignore if not exist
|
||||||
|
if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) {
|
||||||
|
return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err)
|
||||||
|
}
|
||||||
|
if _, err := pr.SetMerged(ctx); err != nil {
|
||||||
|
return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to update PR to merged: %v", err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
49
routers/private/hook_post_receive_test.go
Normal file
49
routers/private/hook_post_receive_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package private
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
|
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/private"
|
||||||
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandlePullRequestMerging(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
pr, err := issues_model.GetUnmergedPullRequest(db.DefaultContext, 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext))
|
||||||
|
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
|
||||||
|
err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
||||||
|
|
||||||
|
ctx, resp := contexttest.MockPrivateContext(t, "/")
|
||||||
|
handlePullRequestMerging(ctx, &private.HookOptions{
|
||||||
|
PullRequestID: pr.ID,
|
||||||
|
UserID: 2,
|
||||||
|
}, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{
|
||||||
|
{NewCommitID: "01234567"},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 0, len(resp.Body.String()))
|
||||||
|
pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, pr.HasMerged)
|
||||||
|
assert.EqualValues(t, "01234567", pr.MergedCommitID)
|
||||||
|
|
||||||
|
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{ID: autoMerge.ID})
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -390,74 +389,6 @@ func ViewProject(ctx *context.Context) {
|
||||||
ctx.HTML(http.StatusOK, tplProjectsView)
|
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
|
// DeleteProjectBoard allows for the deletion of a project board
|
||||||
func DeleteProjectBoard(ctx *context.Context) {
|
func DeleteProjectBoard(ctx *context.Context) {
|
||||||
if ctx.Doer == nil {
|
if ctx.Doer == nil {
|
||||||
|
|
|
@ -1266,8 +1266,8 @@ func NewIssuePost(ctx *context.Context) {
|
||||||
ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
|
ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
|
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
||||||
ctx.ServerError("ChangeProjectAssign", err)
|
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
@ -382,17 +383,21 @@ func UpdateIssueProject(ctx *context.Context) {
|
||||||
ctx.ServerError("LoadProjects", err)
|
ctx.ServerError("LoadProjects", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if _, err := issues.LoadRepositories(ctx); err != nil {
|
||||||
|
ctx.ServerError("LoadProjects", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
projectID := ctx.FormInt64("id")
|
projectID := ctx.FormInt64("id")
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
if issue.Project != nil {
|
if issue.Project != nil && issue.Project.ID == projectID {
|
||||||
if 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
|
continue
|
||||||
}
|
}
|
||||||
}
|
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||||
|
|
||||||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
|
|
||||||
ctx.ServerError("ChangeProjectAssign", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1218,7 +1218,7 @@ func MergePullRequest(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// start with merging by checking
|
// 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 {
|
switch {
|
||||||
case errors.Is(err, pull_service.ErrIsClosed):
|
case errors.Is(err, pull_service.ErrIsClosed):
|
||||||
if issue.IsPull {
|
if issue.IsPull {
|
||||||
|
@ -1232,7 +1232,7 @@ func MergePullRequest(ctx *context.Context) {
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
|
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
|
||||||
case errors.Is(err, pull_service.ErrIsWorkInProgress):
|
case errors.Is(err, pull_service.ErrIsWorkInProgress):
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
|
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"))
|
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
||||||
case models.IsErrDisallowedToMerge(err):
|
case models.IsErrDisallowedToMerge(err):
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
||||||
|
@ -1537,14 +1537,12 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectID > 0 {
|
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||||
if !ctx.Repo.CanWrite(unit.TypeProjects) {
|
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
|
||||||
ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects")
|
if !errors.Is(err, util.ErrPermissionDenied) {
|
||||||
return
|
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||||
}
|
return
|
||||||
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil {
|
}
|
||||||
ctx.ServerError("ChangeProjectAssign", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -798,6 +798,7 @@ func SettingsPost(ctx *context.Context) {
|
||||||
ctx.Repo.GitRepo = nil
|
ctx.Repo.GitRepo = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldFullname := repo.FullName()
|
||||||
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
|
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
|
||||||
if errors.Is(err, user_model.ErrBlockedByUser) {
|
if errors.Is(err, user_model.ErrBlockedByUser) {
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_blocked_doer"), tplSettingsOptions, nil)
|
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_blocked_doer"), tplSettingsOptions, nil)
|
||||||
|
@ -812,8 +813,13 @@ func SettingsPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
|
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
|
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")
|
ctx.Redirect(repo.Link() + "/settings")
|
||||||
|
|
||||||
case "cancel_transfer":
|
case "cancel_transfer":
|
||||||
|
|
48
routers/web/shared/project/column.go
Normal file
48
routers/web/shared/project/column.go
Normal file
|
@ -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()
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ import (
|
||||||
"code.gitea.io/gitea/routers/web/repo/badges"
|
"code.gitea.io/gitea/routers/web/repo/badges"
|
||||||
repo_flags "code.gitea.io/gitea/routers/web/repo/flags"
|
repo_flags "code.gitea.io/gitea/routers/web/repo/flags"
|
||||||
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
|
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"
|
"code.gitea.io/gitea/routers/web/user"
|
||||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||||
"code.gitea.io/gitea/routers/web/user/setting/security"
|
"code.gitea.io/gitea/routers/web/user/setting/security"
|
||||||
|
@ -97,14 +98,14 @@ func optionsCorsHandler() func(next http.Handler) http.Handler {
|
||||||
// The Session plugin is expected to be executed second, in order to skip authentication
|
// The Session plugin is expected to be executed second, in order to skip authentication
|
||||||
// for users that have already signed in.
|
// for users that have already signed in.
|
||||||
func buildAuthGroup() *auth_service.Group {
|
func buildAuthGroup() *auth_service.Group {
|
||||||
group := auth_service.NewGroup(
|
group := auth_service.NewGroup()
|
||||||
&auth_service.OAuth2{}, // FIXME: this should be removed and only applied in download and oauth related routers
|
group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers
|
||||||
&auth_service.Basic{}, // FIXME: this should be removed and only applied in download and git/lfs routers
|
group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers
|
||||||
&auth_service.Session{},
|
|
||||||
)
|
|
||||||
if setting.Service.EnableReverseProxyAuth {
|
if setting.Service.EnableReverseProxyAuth {
|
||||||
group.Add(&auth_service.ReverseProxy{})
|
group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login
|
||||||
}
|
}
|
||||||
|
group.Add(&auth_service.Session{})
|
||||||
|
|
||||||
if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) {
|
if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) {
|
||||||
group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI
|
group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI
|
||||||
|
@ -976,6 +977,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
|
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
|
||||||
|
m.Post("/move", project.MoveColumns)
|
||||||
m.Post("/delete", org.DeleteProject)
|
m.Post("/delete", org.DeleteProject)
|
||||||
|
|
||||||
m.Get("/edit", org.RenderEditProject)
|
m.Get("/edit", org.RenderEditProject)
|
||||||
|
@ -1349,6 +1351,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
|
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
|
||||||
|
m.Post("/move", project.MoveColumns)
|
||||||
m.Post("/delete", repo.DeleteProject)
|
m.Post("/delete", repo.DeleteProject)
|
||||||
|
|
||||||
m.Get("/edit", repo.RenderEditProject)
|
m.Get("/edit", repo.RenderEditProject)
|
||||||
|
|
|
@ -229,12 +229,12 @@ func handlePull(pullID int64, sha string) {
|
||||||
return
|
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) {
|
if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) {
|
||||||
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
|
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Error("%-v CheckPullMergable: %v", pr, err)
|
log.Error("%-v CheckPullMergeable: %v", pr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,19 @@ func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptes
|
||||||
return ctx, resp
|
return ctx, resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) {
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
req := mockRequest(t, reqPath)
|
||||||
|
base, baseCleanUp := context.NewBaseContext(resp, req)
|
||||||
|
base.Data = middleware.GetContextData(req.Context())
|
||||||
|
base.Locale = &translation.MockLocale{}
|
||||||
|
ctx := &context.PrivateContext{Base: base}
|
||||||
|
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
||||||
|
return ctx, resp
|
||||||
|
}
|
||||||
|
|
||||||
// LoadRepo load a repo into a test context.
|
// LoadRepo load a repo into a test context.
|
||||||
func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
|
func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
|
||||||
var doer *user_model.User
|
var doer *user_model.User
|
||||||
|
|
|
@ -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) {
|
func (r *indexerNotifier) IssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
|
||||||
issue_indexer.UpdateIssueIndexer(ctx, issue.ID)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ var (
|
||||||
ErrHasMerged = errors.New("has already been merged")
|
ErrHasMerged = errors.New("has already been merged")
|
||||||
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
||||||
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
|
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")
|
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,8 +66,8 @@ const (
|
||||||
MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed
|
MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckPullMergable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
|
// CheckPullMergeable 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 {
|
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 {
|
return db.WithTx(stdCtx, func(ctx context.Context) error {
|
||||||
if pr.HasMerged {
|
if pr.HasMerged {
|
||||||
return ErrHasMerged
|
return ErrHasMerged
|
||||||
|
@ -97,7 +97,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pr.CanAutoMerge() && !pr.IsEmpty() {
|
if !pr.CanAutoMerge() && !pr.IsEmpty() {
|
||||||
return ErrNotMergableState
|
return ErrNotMergeableState
|
||||||
}
|
}
|
||||||
|
|
||||||
if pr.IsChecking() {
|
if pr.IsChecking() {
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
pull_model "code.gitea.io/gitea/models/pull"
|
|
||||||
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"
|
||||||
|
@ -168,12 +167,6 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
|
||||||
pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
|
pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
|
||||||
defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
|
defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
|
||||||
|
|
||||||
// Removing an auto merge pull and ignore if not exist
|
|
||||||
// FIXME: is this the correct point to do this? Shouldn't this be after IsMergeStyleAllowed?
|
|
||||||
if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
|
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
|
||||||
|
@ -190,17 +183,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
|
||||||
AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "", 0)
|
AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "", 0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
pr.MergedCommitID, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message)
|
_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.MergedUnix = timeutil.TimeStampNow()
|
// reload pull request because it has been updated by post receive hook
|
||||||
pr.Merger = doer
|
pr, err = issues_model.GetPullRequestByID(ctx, pr.ID)
|
||||||
pr.MergerID = doer.ID
|
if err != nil {
|
||||||
|
return err
|
||||||
if _, err := pr.SetMerged(ctx); err != nil {
|
|
||||||
log.Error("SetMerged %-v: %v", pr, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pr.LoadIssue(ctx); err != nil {
|
if err := pr.LoadIssue(ctx); err != nil {
|
||||||
|
@ -251,7 +242,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
|
||||||
}
|
}
|
||||||
|
|
||||||
// doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository
|
// doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository
|
||||||
func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
|
func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) {
|
||||||
// Clone base repo.
|
// Clone base repo.
|
||||||
mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID)
|
mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -324,11 +315,13 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
|
||||||
pr.BaseRepo.Name,
|
pr.BaseRepo.Name,
|
||||||
pr.ID,
|
pr.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger))
|
||||||
pushCmd := git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch)
|
pushCmd := git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch)
|
||||||
|
|
||||||
// Push back to upstream.
|
// Push back to upstream.
|
||||||
// TODO: this cause an api call to "/api/internal/hook/post-receive/...",
|
// This cause an api call to "/api/internal/hook/post-receive/...",
|
||||||
// that prevents us from doint the whole merge in one db transaction
|
// If it's merge, all db transaction and operations should be there but not here to prevent deadlock.
|
||||||
if err := pushCmd.Run(mergeCtx.RunOpts()); err != nil {
|
if err := pushCmd.Run(mergeCtx.RunOpts()); err != nil {
|
||||||
if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") {
|
if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") {
|
||||||
return "", &git.ErrPushOutOfDate{
|
return "", &git.ErrPushOutOfDate{
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"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/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update updates pull request with base branch.
|
// Update updates pull request with base branch.
|
||||||
|
@ -72,7 +73,7 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||||
BaseBranch: pr.HeadBranch,
|
BaseBranch: pr.HeadBranch,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message)
|
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
AddTestPullRequestTask(ctx, doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "", 0)
|
AddTestPullRequestTask(ctx, doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "", 0)
|
||||||
|
|
|
@ -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{
|
repo := &repo_model.Repository{
|
||||||
OwnerID: u.ID,
|
OwnerID: u.ID,
|
||||||
Owner: u,
|
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 {
|
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 {
|
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{
|
branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
ListOptions: db.ListOptionsAll,
|
ListOptions: db.ListOptionsAll,
|
||||||
|
@ -183,22 +194,10 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo
|
||||||
return fmt.Errorf("setDefaultBranch: %w", err)
|
return fmt.Errorf("setDefaultBranch: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = repo_module.UpdateRepository(ctx, repo, false); err != nil {
|
if err = repo_module.UpdateRepository(ctx, repo, false); err != nil {
|
||||||
return fmt.Errorf("updateRepository: %w", err)
|
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.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
|
||||||
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@ export default {
|
||||||
'declaration-property-unit-allowed-list': null,
|
'declaration-property-unit-allowed-list': null,
|
||||||
'declaration-property-unit-disallowed-list': {'line-height': ['em']},
|
'declaration-property-unit-disallowed-list': {'line-height': ['em']},
|
||||||
'declaration-property-value-allowed-list': null,
|
'declaration-property-value-allowed-list': null,
|
||||||
'declaration-property-value-disallowed-list': null,
|
'declaration-property-value-disallowed-list': {'word-break': ['break-word']},
|
||||||
'declaration-property-value-no-unknown': true,
|
'declaration-property-value-no-unknown': true,
|
||||||
'font-family-name-quotes': 'always-where-recommended',
|
'font-family-name-quotes': 'always-where-recommended',
|
||||||
'font-family-no-duplicate-names': true,
|
'font-family-no-duplicate-names': true,
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="project-board">
|
<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}}
|
{{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="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}}">
|
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
|
||||||
|
|
|
@ -13,13 +13,12 @@
|
||||||
|
|
||||||
{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
|
{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
|
||||||
|
|
||||||
<span class="tw-flex-1 gt-ellipsis tw-font-mono{{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
|
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}">
|
||||||
|
{{- RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
||||||
|
</span>
|
||||||
|
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
|
<button class="ui button ellipsis-button show-panel toggle" data-panel="[data-singular-commit-body-for='{{$tag}}']">...</button>
|
||||||
{{end}}
|
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
|
||||||
<pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<span class="shabox tw-flex tw-items-center">
|
<span class="shabox tw-flex tw-items-center">
|
||||||
|
@ -47,5 +46,10 @@
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
|
<pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}">
|
||||||
|
{{- RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
||||||
|
</pre>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,17 +5,17 @@ package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOrgProjectAccess(t *testing.T) {
|
func TestOrgProjectAccess(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&unit_model.DisabledRepoUnits, append(slices.Clone(unit_model.DisabledRepoUnits), unit_model.TypeProjects))()
|
||||||
// disable repo project unit
|
|
||||||
unit_model.DisabledRepoUnits = []unit_model.Type{unit_model.TypeProjects}
|
|
||||||
|
|
||||||
// repo project, 404
|
// repo project, 404
|
||||||
req := NewRequest(t, "GET", "/user2/repo1/projects")
|
req := NewRequest(t, "GET", "/user2/repo1/projects")
|
||||||
|
|
|
@ -4,10 +4,17 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"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/unittest"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrivateRepoProject(t *testing.T) {
|
func TestPrivateRepoProject(t *testing.T) {
|
||||||
|
@ -21,3 +28,56 @@ func TestPrivateRepoProject(t *testing.T) {
|
||||||
req = NewRequest(t, "GET", "/user31/-/projects")
|
req = NewRequest(t, "GET", "/user31/-/projects")
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
sess.MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMoveRepoProjectColumns(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,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"
|
||||||
"code.gitea.io/gitea/modules/hostmatcher"
|
"code.gitea.io/gitea/modules/hostmatcher"
|
||||||
|
"code.gitea.io/gitea/modules/queue"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
@ -600,3 +601,63 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
|
||||||
assert.EqualValues(t, "Closed", prStatus)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -898,6 +898,7 @@ input:-webkit-autofill:active,
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui .sha.label .shortsha {
|
.ui .sha.label .shortsha {
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
color: var(--color-console-fg);
|
color: var(--color-console-fg);
|
||||||
font-family: var(--fonts-monospace);
|
font-family: var(--fonts-monospace);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
word-break: break-word;
|
overflow-wrap: anywhere;
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.console img { max-width: 100%; }
|
.console img { max-width: 100%; }
|
||||||
|
|
|
@ -5,7 +5,6 @@ Gitea's private styles use `g-` prefix.
|
||||||
|
|
||||||
.gt-word-break {
|
.gt-word-break {
|
||||||
word-wrap: break-word !important;
|
word-wrap: break-word !important;
|
||||||
word-break: break-word; /* compat: Safari */
|
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -418,7 +418,7 @@ td .commit-summary {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .plain-text pre {
|
.repository.file.list .non-diff-file-content .plain-text pre {
|
||||||
word-break: break-word;
|
overflow-wrap: anywhere;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2487,14 +2487,10 @@ tbody.commit-list {
|
||||||
.commit-body {
|
.commit-body {
|
||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PR-comment */
|
|
||||||
.repository .timeline-item .commit-body {
|
|
||||||
margin-left: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.git-notes.top {
|
.git-notes.top {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
word-break: break-word;
|
overflow-wrap: anywhere;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: .25rem;
|
gap: .25rem;
|
||||||
color: var(--color-text-light-2);
|
color: var(--color-text-light-2);
|
||||||
word-break: break-word;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-item .flex-item-body a {
|
.flex-item .flex-item-body a {
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
||||||
const weekValues = Object.values(this.data);
|
const weekValues = Object.values(this.data);
|
||||||
const start = weekValues[0].week;
|
const start = weekValues[0].week;
|
||||||
const end = firstStartDateAfterDate(new Date());
|
const end = firstStartDateAfterDate(new Date());
|
||||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
const startDays = startDaysBetween(start, end);
|
||||||
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
|
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
|
||||||
this.errorText = '';
|
this.errorText = '';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default {
|
||||||
const weekValues = Object.values(total.weeks);
|
const weekValues = Object.values(total.weeks);
|
||||||
this.xAxisStart = weekValues[0].week;
|
this.xAxisStart = weekValues[0].week;
|
||||||
this.xAxisEnd = firstStartDateAfterDate(new Date());
|
this.xAxisEnd = firstStartDateAfterDate(new Date());
|
||||||
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
|
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
|
||||||
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
|
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
|
||||||
this.xAxisMin = this.xAxisStart;
|
this.xAxisMin = this.xAxisStart;
|
||||||
this.xAxisMax = this.xAxisEnd;
|
this.xAxisMax = this.xAxisEnd;
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const start = Object.values(data)[0].week;
|
const start = Object.values(data)[0].week;
|
||||||
const end = firstStartDateAfterDate(new Date());
|
const end = firstStartDateAfterDate(new Date());
|
||||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
const startDays = startDaysBetween(start, end);
|
||||||
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
|
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
|
||||||
this.errorText = '';
|
this.errorText = '';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -101,10 +101,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, {
|
const editor = monaco.editor.create(container, {
|
||||||
value: textarea.value,
|
value: textarea.value,
|
||||||
theme: 'gitea',
|
theme: 'gitea',
|
||||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'jquery';
|
||||||
import {contrastColor} from '../utils/color.js';
|
import {contrastColor} from '../utils/color.js';
|
||||||
import {createSortable} from '../modules/sortable.js';
|
import {createSortable} from '../modules/sortable.js';
|
||||||
import {POST, DELETE, PUT} from '../modules/fetch.js';
|
import {POST, DELETE, PUT} from '../modules/fetch.js';
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
function updateIssueCount(cards) {
|
function updateIssueCount(cards) {
|
||||||
const parent = cards.parentElement;
|
const parent = cards.parentElement;
|
||||||
|
@ -63,17 +62,20 @@ async function initRepoProjectSortable() {
|
||||||
delay: 500,
|
delay: 500,
|
||||||
onSort: async () => {
|
onSort: async () => {
|
||||||
boardColumns = mainBoard.getElementsByClassName('project-column');
|
boardColumns = mainBoard.getElementsByClassName('project-column');
|
||||||
for (let i = 0; i < boardColumns.length; i++) {
|
|
||||||
const column = boardColumns[i];
|
const columnSorting = {
|
||||||
if (parseInt(column.getAttribute('data-sorting')) !== i) {
|
columns: Array.from(boardColumns, (column, i) => ({
|
||||||
try {
|
columnID: parseInt(column.getAttribute('data-id')),
|
||||||
const bgColor = column.style.backgroundColor; // will be rgb() string
|
sorting: i,
|
||||||
const color = bgColor ? tinycolor(bgColor).toHexString() : '';
|
})),
|
||||||
await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
try {
|
||||||
}
|
await POST(mainBoard.getAttribute('data-url'), {
|
||||||
}
|
data: columnSorting,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc.js';
|
||||||
import {getCurrentLocale} from '../utils.js';
|
import {getCurrentLocale} from '../utils.js';
|
||||||
|
|
||||||
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
dayjs.extend(utc);
|
||||||
export function startDaysBetween(startDate, endDate) {
|
|
||||||
// Ensure the start date is a Sunday
|
|
||||||
while (startDate.getDay() !== 0) {
|
|
||||||
startDate.setDate(startDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = dayjs(startDate);
|
/**
|
||||||
const end = dayjs(endDate);
|
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||||
const startDays = [];
|
*
|
||||||
|
* @param startConfig The start date. Can take any type that `Date` accepts.
|
||||||
|
* @param endConfig The end date. Can take any type that `Date` accepts.
|
||||||
|
*/
|
||||||
|
export function startDaysBetween(startDate, endDate) {
|
||||||
|
const start = dayjs.utc(startDate);
|
||||||
|
const end = dayjs.utc(endDate);
|
||||||
|
|
||||||
let current = start;
|
let current = start;
|
||||||
|
|
||||||
|
// Ensure the start date is a Sunday
|
||||||
|
while (current.day() !== 0) {
|
||||||
|
current = current.add(1, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDays = [];
|
||||||
while (current.isBefore(end)) {
|
while (current.isBefore(end)) {
|
||||||
startDays.push(current.valueOf());
|
startDays.push(current.valueOf());
|
||||||
// we are adding 7 * 24 hours instead of 1 week because we don't want
|
current = current.add(1, 'week');
|
||||||
// date library to use local time zone to calculate 1 week from now.
|
|
||||||
// local time zone is problematic because of daylight saving time (dst)
|
|
||||||
// used on some countries
|
|
||||||
current = current.add(7 * 24, 'hour');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return startDays;
|
return startDays;
|
||||||
|
@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) {
|
||||||
if (!(inputDate instanceof Date)) {
|
if (!(inputDate instanceof Date)) {
|
||||||
throw new Error('Invalid date');
|
throw new Error('Invalid date');
|
||||||
}
|
}
|
||||||
const dayOfWeek = inputDate.getDay();
|
const dayOfWeek = inputDate.getUTCDay();
|
||||||
const daysUntilSunday = 7 - dayOfWeek;
|
const daysUntilSunday = 7 - dayOfWeek;
|
||||||
const resultDate = new Date(inputDate.getTime());
|
const resultDate = new Date(inputDate.getTime());
|
||||||
resultDate.setDate(resultDate.getDate() + daysUntilSunday);
|
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
|
||||||
return resultDate.valueOf();
|
return resultDate.valueOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue