From 276500c314db1c0ef360088753861ffc010a99da Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Wed, 6 Nov 2024 19:28:11 -0800
Subject: [PATCH 001/100] Move AddCollabrator and CreateRepositoryByExample to
service layer (#32419)
- [x] Move `CreateRepositoryByExample` to service layer
- [x] Move `AddCollabrator` to service layer
- [x] Add a new parameter for `AddCollabrator` so that changing mode
immediately after that will become unnecessary.
---
models/perm/access_mode.go | 3 +
modules/repository/collaborator.go | 48 ----
modules/repository/collaborator_test.go | 280 ----------------------
modules/repository/create.go | 143 -----------
modules/repository/main_test.go | 1 +
routers/api/v1/api.go | 2 +-
routers/api/v1/repo/collaborators.go | 27 +--
routers/web/repo/setting/collaboration.go | 5 +-
services/feed/action_test.go | 1 +
services/issue/main_test.go | 1 +
services/mailer/main_test.go | 1 +
services/repository/adopt.go | 2 +-
services/repository/collaboration.go | 49 ++++
services/repository/collaboration_test.go | 16 ++
services/repository/create.go | 141 ++++++++++-
services/repository/fork.go | 2 +-
services/repository/generate.go | 2 +-
services/repository/transfer.go | 6 +-
templates/swagger/v1_json.tmpl | 2 +-
19 files changed, 232 insertions(+), 500 deletions(-)
delete mode 100644 modules/repository/collaborator.go
delete mode 100644 modules/repository/collaborator_test.go
diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go
index 0364191e2e..6baeb5531a 100644
--- a/models/perm/access_mode.go
+++ b/models/perm/access_mode.go
@@ -60,3 +60,6 @@ func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode {
}
return util.Iif(slices.Contains(allowed, m), m, AccessModeNone)
}
+
+// ErrInvalidAccessMode is returned when an invalid access mode is used
+var ErrInvalidAccessMode = util.NewInvalidArgumentErrorf("Invalid access mode")
diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go
deleted file mode 100644
index f71c58fbdf..0000000000
--- a/modules/repository/collaborator.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repository
-
-import (
- "context"
-
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
-
- "xorm.io/builder"
-)
-
-func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
- if err := repo.LoadOwner(ctx); err != nil {
- return err
- }
-
- if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
- return user_model.ErrBlockedUser
- }
-
- return db.WithTx(ctx, func(ctx context.Context) error {
- has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
- "repo_id": repo.ID,
- "user_id": u.ID,
- })
- if err != nil {
- return err
- } else if has {
- return nil
- }
-
- if err = db.Insert(ctx, &repo_model.Collaboration{
- RepoID: repo.ID,
- UserID: u.ID,
- Mode: perm.AccessModeWrite,
- }); err != nil {
- return err
- }
-
- return access_model.RecalculateUserAccess(ctx, repo, u.ID)
- })
-}
diff --git a/modules/repository/collaborator_test.go b/modules/repository/collaborator_test.go
deleted file mode 100644
index 622f6abce4..0000000000
--- a/modules/repository/collaborator_test.go
+++ /dev/null
@@ -1,280 +0,0 @@
-// Copyright 2019 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repository
-
-import (
- "testing"
-
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- perm_model "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRepository_AddCollaborator(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- testSuccess := func(repoID, userID int64) {
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
- assert.NoError(t, repo.LoadOwner(db.DefaultContext))
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
- assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user))
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID})
- }
- testSuccess(1, 4)
- testSuccess(1, 4)
- testSuccess(3, 4)
-}
-
-func TestRepoPermissionPublicNonOrgRepo(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- // public non-organization repo
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- assert.NoError(t, repo.LoadUnits(db.DefaultContext))
-
- // plain user
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // change to collaborator
- assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // collaborator
- collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, collaborator)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // owner
- owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // admin
- admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-}
-
-func TestRepoPermissionPrivateNonOrgRepo(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- // private non-organization repo
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
- assert.NoError(t, repo.LoadUnits(db.DefaultContext))
-
- // plain user
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
- perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.False(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // change to collaborator to default write access
- assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // owner
- owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // admin
- admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-}
-
-func TestRepoPermissionPublicOrgRepo(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- // public organization repo
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
- assert.NoError(t, repo.LoadUnits(db.DefaultContext))
-
- // plain user
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
- perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // change to collaborator to default write access
- assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // org member team owner
- owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // org member team tester
- member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, member)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- }
- assert.True(t, perm.CanWrite(unit.TypeIssues))
- assert.False(t, perm.CanWrite(unit.TypeCode))
-
- // admin
- admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-}
-
-func TestRepoPermissionPrivateOrgRepo(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- // private organization repo
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
- assert.NoError(t, repo.LoadUnits(db.DefaultContext))
-
- // plain user
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
- perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.False(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // change to collaborator to default write access
- assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead))
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.False(t, perm.CanWrite(unit.Type))
- }
-
- // org member team owner
- owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // update team information and then check permission
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
- err = organization.UpdateTeamUnits(db.DefaultContext, team, nil)
- assert.NoError(t, err)
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-
- // org member team tester
- tester := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, tester)
- assert.NoError(t, err)
- assert.True(t, perm.CanWrite(unit.TypeIssues))
- assert.False(t, perm.CanWrite(unit.TypeCode))
- assert.False(t, perm.CanRead(unit.TypeCode))
-
- // org member team reviewer
- reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, reviewer)
- assert.NoError(t, err)
- assert.False(t, perm.CanRead(unit.TypeIssues))
- assert.False(t, perm.CanWrite(unit.TypeCode))
- assert.True(t, perm.CanRead(unit.TypeCode))
-
- // admin
- admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin)
- assert.NoError(t, err)
- for _, unit := range repo.Units {
- assert.True(t, perm.CanRead(unit.Type))
- assert.True(t, perm.CanWrite(unit.Type))
- }
-}
diff --git a/modules/repository/create.go b/modules/repository/create.go
index 4f18b9b3fa..b4f7033bd7 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -11,160 +11,17 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/models"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/models/webhook"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
-// CreateRepositoryByExample creates a repository for the user/organization.
-func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) {
- if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
- return err
- }
-
- has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name)
- if err != nil {
- return fmt.Errorf("IsRepositoryExist: %w", err)
- } else if has {
- return repo_model.ErrRepoAlreadyExist{
- Uname: u.Name,
- Name: repo.Name,
- }
- }
-
- repoPath := repo_model.RepoPath(u.Name, repo.Name)
- isExist, err := util.IsExist(repoPath)
- if err != nil {
- log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
- return err
- }
- if !overwriteOrAdopt && isExist {
- log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
- return repo_model.ErrRepoFilesAlreadyExist{
- Uname: u.Name,
- Name: repo.Name,
- }
- }
-
- if err = db.Insert(ctx, repo); err != nil {
- return err
- }
- if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil {
- return err
- }
-
- // insert units for repo
- defaultUnits := unit.DefaultRepoUnits
- if isFork {
- defaultUnits = unit.DefaultForkRepoUnits
- }
- units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
- for _, tp := range defaultUnits {
- if tp == unit.TypeIssues {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: tp,
- Config: &repo_model.IssuesConfig{
- EnableTimetracker: setting.Service.DefaultEnableTimetracking,
- AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
- EnableDependencies: setting.Service.DefaultEnableDependencies,
- },
- })
- } else if tp == unit.TypePullRequests {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: tp,
- Config: &repo_model.PullRequestsConfig{
- AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
- DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
- AllowRebaseUpdate: true,
- },
- })
- } else if tp == unit.TypeProjects {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: tp,
- Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
- })
- } else {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: tp,
- })
- }
- }
-
- if err = db.Insert(ctx, units); err != nil {
- return err
- }
-
- // Remember visibility preference.
- u.LastRepoVisibility = repo.IsPrivate
- if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil {
- return fmt.Errorf("UpdateUserCols: %w", err)
- }
-
- if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil {
- return fmt.Errorf("IncrUserRepoNum: %w", err)
- }
- u.NumRepos++
-
- // Give access to all members in teams with access to all repositories.
- if u.IsOrganization() {
- teams, err := organization.FindOrgTeams(ctx, u.ID)
- if err != nil {
- return fmt.Errorf("FindOrgTeams: %w", err)
- }
- for _, t := range teams {
- if t.IncludesAllRepositories {
- if err := models.AddRepository(ctx, t, repo); err != nil {
- return fmt.Errorf("AddRepository: %w", err)
- }
- }
- }
-
- if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil {
- return fmt.Errorf("IsUserRepoAdmin: %w", err)
- } else if !isAdmin {
- // Make creator repo admin if it wasn't assigned automatically
- if err = AddCollaborator(ctx, repo, doer); err != nil {
- return fmt.Errorf("AddCollaborator: %w", err)
- }
- if err = repo_model.ChangeCollaborationAccessMode(ctx, repo, doer.ID, perm.AccessModeAdmin); err != nil {
- return fmt.Errorf("ChangeCollaborationAccessModeCtx: %w", err)
- }
- }
- } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
- // Organization automatically called this in AddRepository method.
- return fmt.Errorf("RecalculateAccesses: %w", err)
- }
-
- if setting.Service.AutoWatchNewRepos {
- if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
- return fmt.Errorf("WatchRepo: %w", err)
- }
- }
-
- if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil {
- return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err)
- }
-
- return nil
-}
-
const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
// getDirectorySize returns the disk consumption for a given path
diff --git a/modules/repository/main_test.go b/modules/repository/main_test.go
index f81dfcdafb..799e8c17c3 100644
--- a/modules/repository/main_test.go
+++ b/modules/repository/main_test.go
@@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest"
+ _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
)
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index bfc601c835..23f466873b 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1172,7 +1172,7 @@ func Routes() *web.Router {
m.Get("", reqAnyRepoReader(), repo.ListCollaborators)
m.Group("/{collaborator}", func() {
m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator).
- Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
+ Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddOrUpdateCollaborator).
Delete(reqAdmin(), repo.DeleteCollaborator)
m.Get("/permission", repo.GetRepoPermissions)
})
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index 39c9ba527d..ea9d8b0f37 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -12,7 +12,6 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -123,11 +122,11 @@ func IsCollaborator(ctx *context.APIContext) {
}
}
-// AddCollaborator add a collaborator to a repository
-func AddCollaborator(ctx *context.APIContext) {
+// AddOrUpdateCollaborator add or update a collaborator to a repository
+func AddOrUpdateCollaborator(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/collaborators/{collaborator} repository repoAddCollaborator
// ---
- // summary: Add a collaborator to a repository
+ // summary: Add or Update a collaborator to a repository
// produces:
// - application/json
// parameters:
@@ -177,20 +176,18 @@ func AddCollaborator(ctx *context.APIContext) {
return
}
- if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.Error(http.StatusForbidden, "AddCollaborator", err)
- } else {
- ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
- }
- return
+ p := perm.AccessModeWrite
+ if form.Permission != nil {
+ p = perm.ParseAccessMode(*form.Permission)
}
- if form.Permission != nil {
- if err := repo_model.ChangeCollaborationAccessMode(ctx, ctx.Repo.Repository, collaborator.ID, perm.ParseAccessMode(*form.Permission)); err != nil {
- ctx.Error(http.StatusInternalServerError, "ChangeCollaborationAccessMode", err)
- return
+ if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "AddOrUpdateCollaborator", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "AddOrUpdateCollaborator", err)
}
+ return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go
index 31f9f76d0f..18ecff8250 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -14,7 +14,6 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/mailer"
@@ -100,12 +99,12 @@ func CollaborationPost(ctx *context.Context) {
}
}
- if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
+ if err = repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, u, perm.AccessModeWrite); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
} else {
- ctx.ServerError("AddCollaborator", err)
+ ctx.ServerError("AddOrUpdateCollaborator", err)
}
return
}
diff --git a/services/feed/action_test.go b/services/feed/action_test.go
index e1b071d8f6..60cf7fbb49 100644
--- a/services/feed/action_test.go
+++ b/services/feed/action_test.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
"github.com/stretchr/testify/assert"
diff --git a/services/issue/main_test.go b/services/issue/main_test.go
index 5dac54183b..819c5d98c3 100644
--- a/services/issue/main_test.go
+++ b/services/issue/main_test.go
@@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest"
+ _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
)
diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go
index f803c736ca..5591bea02b 100644
--- a/services/mailer/main_test.go
+++ b/services/mailer/main_test.go
@@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest"
+ _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
)
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 3d6fe71a09..615f4d482c 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -66,7 +66,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
}
}
- if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
+ if err := CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
return err
}
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index 4a43ae2a28..abe0489fc5 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -9,11 +9,60 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+
+ "xorm.io/builder"
)
+func AddOrUpdateCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User, mode perm.AccessMode) error {
+ // only allow valid access modes, read, write and admin
+ if mode < perm.AccessModeRead || mode > perm.AccessModeAdmin {
+ return perm.ErrInvalidAccessMode
+ }
+
+ if err := repo.LoadOwner(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
+ return user_model.ErrBlockedUser
+ }
+
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ collaboration, has, err := db.Get[repo_model.Collaboration](ctx, builder.Eq{
+ "repo_id": repo.ID,
+ "user_id": u.ID,
+ })
+ if err != nil {
+ return err
+ } else if has {
+ if collaboration.Mode == mode {
+ return nil
+ }
+ if _, err = db.GetEngine(ctx).
+ Where("repo_id=?", repo.ID).
+ And("user_id=?", u.ID).
+ Cols("mode").
+ Update(&repo_model.Collaboration{
+ Mode: mode,
+ }); err != nil {
+ return err
+ }
+ } else if err = db.Insert(ctx, &repo_model.Collaboration{
+ RepoID: repo.ID,
+ UserID: u.ID,
+ Mode: mode,
+ }); err != nil {
+ return err
+ }
+
+ return access_model.RecalculateUserAccess(ctx, repo, u.ID)
+ })
+}
+
// DeleteCollaboration removes collaboration relation between the user and repository.
func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
collaboration := &repo_model.Collaboration{
diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go
index a2eb06b81a..2b9a5d0b8b 100644
--- a/services/repository/collaboration_test.go
+++ b/services/repository/collaboration_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -14,6 +15,21 @@ import (
"github.com/stretchr/testify/assert"
)
+func TestRepository_AddCollaborator(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ testSuccess := func(repoID, userID int64) {
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
+ assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
+ assert.NoError(t, AddOrUpdateCollaborator(db.DefaultContext, repo, user, perm.AccessModeWrite))
+ unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID})
+ }
+ testSuccess(1, 4)
+ testSuccess(1, 4)
+ testSuccess(3, 4)
+}
+
func TestRepository_DeleteCollaboration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/services/repository/create.go b/services/repository/create.go
index 282b2d3e58..261ac7fccc 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -12,9 +12,15 @@ import (
"strings"
"time"
+ "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
@@ -243,7 +249,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
var rollbackRepo *repo_model.Repository
if err := db.WithTx(ctx, func(ctx context.Context) error {
- if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
+ if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
return err
}
@@ -335,3 +341,136 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
return repo, nil
}
+
+// CreateRepositoryByExample creates a repository for the user/organization.
+func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) {
+ if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
+ return err
+ }
+
+ has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name)
+ if err != nil {
+ return fmt.Errorf("IsRepositoryExist: %w", err)
+ } else if has {
+ return repo_model.ErrRepoAlreadyExist{
+ Uname: u.Name,
+ Name: repo.Name,
+ }
+ }
+
+ repoPath := repo_model.RepoPath(u.Name, repo.Name)
+ isExist, err := util.IsExist(repoPath)
+ if err != nil {
+ log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
+ return err
+ }
+ if !overwriteOrAdopt && isExist {
+ log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
+ return repo_model.ErrRepoFilesAlreadyExist{
+ Uname: u.Name,
+ Name: repo.Name,
+ }
+ }
+
+ if err = db.Insert(ctx, repo); err != nil {
+ return err
+ }
+ if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil {
+ return err
+ }
+
+ // insert units for repo
+ defaultUnits := unit.DefaultRepoUnits
+ if isFork {
+ defaultUnits = unit.DefaultForkRepoUnits
+ }
+ units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
+ for _, tp := range defaultUnits {
+ if tp == unit.TypeIssues {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Config: &repo_model.IssuesConfig{
+ EnableTimetracker: setting.Service.DefaultEnableTimetracking,
+ AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
+ EnableDependencies: setting.Service.DefaultEnableDependencies,
+ },
+ })
+ } else if tp == unit.TypePullRequests {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Config: &repo_model.PullRequestsConfig{
+ AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
+ DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
+ AllowRebaseUpdate: true,
+ },
+ })
+ } else if tp == unit.TypeProjects {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
+ })
+ } else {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ })
+ }
+ }
+
+ if err = db.Insert(ctx, units); err != nil {
+ return err
+ }
+
+ // Remember visibility preference.
+ u.LastRepoVisibility = repo.IsPrivate
+ if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil {
+ return fmt.Errorf("UpdateUserCols: %w", err)
+ }
+
+ if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil {
+ return fmt.Errorf("IncrUserRepoNum: %w", err)
+ }
+ u.NumRepos++
+
+ // Give access to all members in teams with access to all repositories.
+ if u.IsOrganization() {
+ teams, err := organization.FindOrgTeams(ctx, u.ID)
+ if err != nil {
+ return fmt.Errorf("FindOrgTeams: %w", err)
+ }
+ for _, t := range teams {
+ if t.IncludesAllRepositories {
+ if err := models.AddRepository(ctx, t, repo); err != nil {
+ return fmt.Errorf("AddRepository: %w", err)
+ }
+ }
+ }
+
+ if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil {
+ return fmt.Errorf("IsUserRepoAdmin: %w", err)
+ } else if !isAdmin {
+ // Make creator repo admin if it wasn't assigned automatically
+ if err = AddOrUpdateCollaborator(ctx, repo, doer, perm.AccessModeAdmin); err != nil {
+ return fmt.Errorf("AddCollaborator: %w", err)
+ }
+ }
+ } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
+ // Organization automatically called this in AddRepository method.
+ return fmt.Errorf("RecalculateAccesses: %w", err)
+ }
+
+ if setting.Service.AutoWatchNewRepos {
+ if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
+ return fmt.Errorf("WatchRepo: %w", err)
+ }
+ }
+
+ if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil {
+ return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err)
+ }
+
+ return nil
+}
diff --git a/services/repository/fork.go b/services/repository/fork.go
index e114555679..5b24015a03 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -134,7 +134,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
}()
err = db.WithTx(ctx, func(txCtx context.Context) error {
- if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
+ if err = CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
return err
}
diff --git a/services/repository/generate.go b/services/repository/generate.go
index 2b95bbcd4d..f2280de8b2 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -343,7 +343,7 @@ func generateRepository(ctx context.Context, doer, owner *user_model.User, templ
ObjectFormatName: templateRepo.ObjectFormatName,
}
- if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
+ if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
return nil, err
}
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 7ad6b46fa4..301d895337 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -20,7 +20,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
)
@@ -419,10 +418,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
return err
}
if !hasAccess {
- if err := repo_module.AddCollaborator(ctx, repo, newOwner); err != nil {
- return err
- }
- if err := repo_model.ChangeCollaborationAccessMode(ctx, repo, newOwner.ID, perm.AccessModeRead); err != nil {
+ if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil {
return err
}
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 01b12460ac..b9fb1910a3 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -5095,7 +5095,7 @@
"tags": [
"repository"
],
- "summary": "Add a collaborator to a repository",
+ "summary": "Add or Update a collaborator to a repository",
"operationId": "repoAddCollaborator",
"parameters": [
{
From 145e26698791221b007c7dd460fb506cb0237235 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Thu, 7 Nov 2024 11:57:07 +0800
Subject: [PATCH 002/100] Support quote selected comments to reply (#32431)
Many existing tests were quite hacky, these could be improved later.

---
modules/markup/html.go | 5 +-
modules/markup/html_codepreview_test.go | 2 +-
modules/markup/html_internal_test.go | 12 +-
modules/markup/html_test.go | 16 ++-
modules/markup/markdown/markdown_test.go | 12 +-
modules/markup/sanitizer_default.go | 1 +
modules/templates/util_render_test.go | 16 +--
routers/api/v1/misc/markup_test.go | 6 +-
templates/repo/diff/comments.tmpl | 2 +-
templates/repo/issue/view_content.tmpl | 2 +-
.../repo/issue/view_content/comments.tmpl | 4 +-
.../repo/issue/view_content/context_menu.tmpl | 18 +--
.../repo/issue/view_content/conversation.tmpl | 2 +-
web_src/js/features/repo-issue-edit.ts | 84 ++++++++-----
web_src/js/markup/html2markdown.test.ts | 24 ++++
web_src/js/markup/html2markdown.ts | 119 ++++++++++++++++++
16 files changed, 255 insertions(+), 70 deletions(-)
create mode 100644 web_src/js/markup/html2markdown.test.ts
create mode 100644 web_src/js/markup/html2markdown.ts
diff --git a/modules/markup/html.go b/modules/markup/html.go
index a9c3dc9ba2..e2eefefc4b 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -442,7 +442,10 @@ func createLink(href, content, class string) *html.Node {
a := &html.Node{
Type: html.ElementNode,
Data: atom.A.String(),
- Attr: []html.Attribute{{Key: "href", Val: href}},
+ Attr: []html.Attribute{
+ {Key: "href", Val: href},
+ {Key: "data-markdown-generated-content"},
+ },
}
if class != "" {
diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go
index d33630d040..a90de278f5 100644
--- a/modules/markup/html_codepreview_test.go
+++ b/modules/markup/html_codepreview_test.go
@@ -30,5 +30,5 @@ func TestRenderCodePreview(t *testing.T) {
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "
code preview
")
- test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`)
+ test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`)
}
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 74089cffdd..8f516751b0 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -33,11 +33,9 @@ func numericIssueLink(baseURL, class string, index int, marker string) string {
// link an HTML link
func link(href, class, contents string) string {
- if class != "" {
- class = " class=\"" + class + "\""
- }
-
- return fmt.Sprintf("%s ", href, class, contents)
+ extra := ` data-markdown-generated-content=""`
+ extra += util.Iif(class != "", ` class="`+class+`"`, "")
+ return fmt.Sprintf(`%s `, href, extra, contents)
}
var numericMetas = map[string]string{
@@ -353,7 +351,9 @@ func TestRender_FullIssueURLs(t *testing.T) {
Metas: localMetas,
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
assert.NoError(t, err)
- assert.Equal(t, expected, result.String())
+ actual := result.String()
+ actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, expected, actual)
}
test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6",
"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6")
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 32858dbd6b..82aded4407 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -116,7 +116,9 @@ func TestRender_CrossReferences(t *testing.T) {
Metas: localMetas,
}, input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ actual := strings.TrimSpace(buffer)
+ actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, strings.TrimSpace(expected), actual)
}
test(
@@ -156,7 +158,9 @@ func TestRender_links(t *testing.T) {
},
}, input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ actual := strings.TrimSpace(buffer)
+ actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, strings.TrimSpace(expected), actual)
}
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
@@ -267,7 +271,9 @@ func TestRender_email(t *testing.T) {
},
}, input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
+ actual := strings.TrimSpace(res)
+ actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, strings.TrimSpace(expected), actual)
}
// Text that should be turned into email link
@@ -616,7 +622,9 @@ func TestPostProcess_RenderDocument(t *testing.T) {
Metas: localMetas,
}, strings.NewReader(input), &res)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
+ actual := strings.TrimSpace(res.String())
+ actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, strings.TrimSpace(expected), actual)
}
// Issue index shouldn't be post processing in a document.
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index cfb821ab19..ad38e7a088 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -311,7 +311,8 @@ func TestTotal_RenderWiki(t *testing.T) {
IsWiki: true,
}, sameCases[i])
assert.NoError(t, err)
- assert.Equal(t, template.HTML(answers[i]), line)
+ actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, answers[i], actual)
}
testCases := []string{
@@ -336,7 +337,8 @@ func TestTotal_RenderWiki(t *testing.T) {
IsWiki: true,
}, testCases[i])
assert.NoError(t, err)
- assert.Equal(t, template.HTML(testCases[i+1]), line)
+ actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
+ assert.EqualValues(t, testCases[i+1], actual)
}
}
@@ -356,7 +358,8 @@ func TestTotal_RenderString(t *testing.T) {
Metas: localMetas,
}, sameCases[i])
assert.NoError(t, err)
- assert.Equal(t, template.HTML(answers[i]), line)
+ actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, answers[i], actual)
}
testCases := []string{}
@@ -996,7 +999,8 @@ space
for i, c := range cases {
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
- assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
+ actual := strings.ReplaceAll(string(result), ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, c.Expected, actual, "Unexpected result in testcase %v", i)
}
}
diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go
index 669dc24eae..476ae5e26f 100644
--- a/modules/markup/sanitizer_default.go
+++ b/modules/markup/sanitizer_default.go
@@ -107,6 +107,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
"start", "summary", "tabindex", "target",
"title", "type", "usemap", "valign", "value",
"vspace", "width", "itemprop",
+ "data-markdown-generated-content",
}
generalSafeElements := []string{
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 3e4ea04c63..0a6e89c5f2 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -129,18 +129,18 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
@mention-user test
#123
space`
-
- assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitBody(testInput(), testMetas))
+ actual := strings.ReplaceAll(string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)), ` data-markdown-generated-content=""`, "")
+ assert.EqualValues(t, expected, actual)
}
func TestRenderCommitMessage(t *testing.T) {
- expected := `space @mention-user `
+ expected := `space @mention-user `
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
}
func TestRenderCommitMessageLinkSubject(t *testing.T) {
- expected := `space @mention-user `
+ expected := `space @mention-user `
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
}
@@ -168,7 +168,8 @@ mail@domain.com
space
`
expected = strings.ReplaceAll(expected, "", " ")
- assert.EqualValues(t, expected, newTestRenderUtils().RenderIssueTitle(testInput(), testMetas))
+ actual := strings.ReplaceAll(string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)), ` data-markdown-generated-content=""`, "")
+ assert.EqualValues(t, expected, actual)
}
func TestRenderMarkdownToHtml(t *testing.T) {
@@ -193,7 +194,8 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
#123
space
`
- assert.Equal(t, expected, string(newTestRenderUtils().MarkdownToHtml(testInput())))
+ actual := strings.ReplaceAll(string(newTestRenderUtils().MarkdownToHtml(testInput())), ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, expected, actual)
}
func TestRenderLabels(t *testing.T) {
@@ -211,5 +213,5 @@ func TestRenderLabels(t *testing.T) {
func TestUserMention(t *testing.T) {
rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user")
- assert.EqualValues(t, `@no-such-user @mention-user @mention-user
`, strings.TrimSpace(string(rendered)))
+ assert.EqualValues(t, `@no-such-user @mention-user @mention-user
`, strings.TrimSpace(string(rendered)))
}
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index e2ab7141b7..abffdf3516 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -38,7 +38,8 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
web.SetForm(ctx, &options)
Markup(ctx)
- assert.Equal(t, expectedBody, resp.Body.String())
+ actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, expectedBody, actual)
assert.Equal(t, expectedCode, resp.Code)
resp.Body.Reset()
}
@@ -58,7 +59,8 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
web.SetForm(ctx, &options)
Markdown(ctx)
- assert.Equal(t, responseBody, resp.Body.String())
+ actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "")
+ assert.Equal(t, responseBody, actual)
assert.Equal(t, responseCode, resp.Code)
resp.Body.Reset()
}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 0ed231b07e..2d716688b9 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -49,7 +49,7 @@
{{end}}
{{end}}
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
- {{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
+ {{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 74440c5ef2..1f9bbd86aa 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -48,7 +48,7 @@
{{if not $.Repository.IsArchived}}
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
{{end}}
- {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
+ {{template "repo/issue/view_content/context_menu" dict "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 3047533154..477b6b33c6 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -55,7 +55,7 @@
{{if not $.Repository.IsArchived}}
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
{{end}}
- {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
+ {{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
@@ -430,7 +430,7 @@
{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
{{if not $.Repository.IsArchived}}
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
- {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
+ {{template "repo/issue/view_content/context_menu" dict "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
{{end}}
diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl
index 9e38442c36..749a2fa0dd 100644
--- a/templates/repo/issue/view_content/context_menu.tmpl
+++ b/templates/repo/issue/view_content/context_menu.tmpl
@@ -5,29 +5,29 @@
diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts
index af97ee4eab..9d146951bd 100644
--- a/web_src/js/features/repo-issue-edit.ts
+++ b/web_src/js/features/repo-issue-edit.ts
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import {handleReply} from './repo-issue.ts';
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {POST} from '../modules/fetch.ts';
@@ -7,11 +6,14 @@ import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts';
import {attachRefIssueContextPopup} from './contextpopup.ts';
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
+import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
-async function onEditContent(event) {
- event.preventDefault();
+async function tryOnEditContent(e) {
+ const clickTarget = e.target.closest('.edit-content');
+ if (!clickTarget) return;
- const segment = this.closest('.header').nextElementSibling;
+ e.preventDefault();
+ const segment = clickTarget.closest('.header').nextElementSibling;
const editContentZone = segment.querySelector('.edit-content-zone');
const renderContent = segment.querySelector('.render-content');
const rawContent = segment.querySelector('.raw-content');
@@ -102,33 +104,53 @@ async function onEditContent(event) {
triggerUploadStateChanged(comboMarkdownEditor.container);
}
+function extractSelectedMarkdown(container: HTMLElement) {
+ const selection = window.getSelection();
+ if (!selection.rangeCount) return '';
+ const range = selection.getRangeAt(0);
+ if (!container.contains(range.commonAncestorContainer)) return '';
+
+ // todo: if commonAncestorContainer parent has "[data-markdown-original-content]" attribute, use the parent's markdown content
+ // otherwise, use the selected HTML content and respect all "[data-markdown-original-content]/[data-markdown-generated-content]" attributes
+ const contents = selection.getRangeAt(0).cloneContents();
+ const el = document.createElement('div');
+ el.append(contents);
+ return convertHtmlToMarkdown(el);
+}
+
+async function tryOnQuoteReply(e) {
+ const clickTarget = (e.target as HTMLElement).closest('.quote-reply');
+ if (!clickTarget) return;
+
+ e.preventDefault();
+ const contentToQuoteId = clickTarget.getAttribute('data-target');
+ const targetRawToQuote = document.querySelector(`#${contentToQuoteId}.raw-content`);
+ const targetMarkupToQuote = targetRawToQuote.parentElement.querySelector('.render-content.markup');
+ let contentToQuote = extractSelectedMarkdown(targetMarkupToQuote);
+ if (!contentToQuote) contentToQuote = targetRawToQuote.textContent;
+ const quotedContent = `${contentToQuote.replace(/^/mg, '> ')}\n`;
+
+ let editor;
+ if (clickTarget.classList.contains('quote-reply-diff')) {
+ const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
+ editor = await handleReply(replyBtn);
+ } else {
+ // for normal issue/comment page
+ editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'));
+ }
+
+ if (editor.value()) {
+ editor.value(`${editor.value()}\n\n${quotedContent}`);
+ } else {
+ editor.value(quotedContent);
+ }
+ editor.focus();
+ editor.moveCursorToEnd();
+}
+
export function initRepoIssueCommentEdit() {
- // Edit issue or comment content
- $(document).on('click', '.edit-content', onEditContent);
-
- // Quote reply
- $(document).on('click', '.quote-reply', async function (event) {
- event.preventDefault();
- const target = this.getAttribute('data-target');
- const quote = document.querySelector(`#${target}`).textContent.replace(/\n/g, '\n> ');
- const content = `> ${quote}\n\n`;
-
- let editor;
- if (this.classList.contains('quote-reply-diff')) {
- const replyBtn = this.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
- editor = await handleReply(replyBtn);
- } else {
- // for normal issue/comment page
- editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
- }
- if (editor) {
- if (editor.value()) {
- editor.value(`${editor.value()}\n\n${content}`);
- } else {
- editor.value(content);
- }
- editor.focus();
- editor.moveCursorToEnd();
- }
+ document.addEventListener('click', (e) => {
+ tryOnEditContent(e); // Edit issue or comment content
+ tryOnQuoteReply(e); // Quote reply to the comment editor
});
}
diff --git a/web_src/js/markup/html2markdown.test.ts b/web_src/js/markup/html2markdown.test.ts
new file mode 100644
index 0000000000..99a63956a0
--- /dev/null
+++ b/web_src/js/markup/html2markdown.test.ts
@@ -0,0 +1,24 @@
+import {convertHtmlToMarkdown} from './html2markdown.ts';
+import {createElementFromHTML} from '../utils/dom.ts';
+
+const h = createElementFromHTML;
+
+test('convertHtmlToMarkdown', () => {
+ expect(convertHtmlToMarkdown(h(`h `))).toBe('# h');
+ expect(convertHtmlToMarkdown(h(`txt `))).toBe('**txt**');
+ expect(convertHtmlToMarkdown(h(`txt `))).toBe('_txt_');
+ expect(convertHtmlToMarkdown(h(`txt`))).toBe('~~txt~~');
+
+ expect(convertHtmlToMarkdown(h(`txt `))).toBe('[txt](link)');
+ expect(convertHtmlToMarkdown(h(`https://link `))).toBe('https://link');
+
+ expect(convertHtmlToMarkdown(h(` `))).toBe('');
+ expect(convertHtmlToMarkdown(h(` `))).toBe('');
+ expect(convertHtmlToMarkdown(h(` `))).toBe(' ');
+
+ expect(convertHtmlToMarkdown(h(`txt
`))).toBe('txt\n');
+ expect(convertHtmlToMarkdown(h(`a\nb `))).toBe('> a\n> b\n');
+
+ expect(convertHtmlToMarkdown(h(`a `))).toBe('1. a\n * b\n\n');
+ expect(convertHtmlToMarkdown(h(` a `))).toBe('1. [x] a\n');
+});
diff --git a/web_src/js/markup/html2markdown.ts b/web_src/js/markup/html2markdown.ts
new file mode 100644
index 0000000000..c690e0c8b1
--- /dev/null
+++ b/web_src/js/markup/html2markdown.ts
@@ -0,0 +1,119 @@
+import {htmlEscape} from 'escape-goat';
+
+type Processors = {
+ [tagName: string]: (el: HTMLElement) => string | HTMLElement | void;
+}
+
+type ProcessorContext = {
+ elementIsFirst: boolean;
+ elementIsLast: boolean;
+ listNestingLevel: number;
+}
+
+function prepareProcessors(ctx:ProcessorContext): Processors {
+ const processors = {
+ H1(el) {
+ const level = parseInt(el.tagName.slice(1));
+ el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`;
+ },
+ STRONG(el) {
+ return `**${el.textContent}**`;
+ },
+ EM(el) {
+ return `_${el.textContent}_`;
+ },
+ DEL(el) {
+ return `~~${el.textContent}~~`;
+ },
+
+ A(el) {
+ const text = el.textContent || 'link';
+ const href = el.getAttribute('href');
+ if (/^https?:/.test(text) && text === href) {
+ return text;
+ }
+ return href ? `[${text}](${href})` : text;
+ },
+ IMG(el) {
+ const alt = el.getAttribute('alt') || 'image';
+ const src = el.getAttribute('src');
+ const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : '';
+ const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : '';
+ if (widthAttr || heightAttr) {
+ return ` `;
+ }
+ return ``;
+ },
+
+ P(el) {
+ el.textContent = `${el.textContent}\n`;
+ },
+ BLOCKQUOTE(el) {
+ el.textContent = `${el.textContent.replace(/^/mg, '> ')}\n`;
+ },
+
+ OL(el) {
+ const preNewLine = ctx.listNestingLevel ? '\n' : '';
+ el.textContent = `${preNewLine}${el.textContent}\n`;
+ },
+ LI(el) {
+ const parent = el.parentNode;
+ const bullet = parent.tagName === 'OL' ? `1. ` : '* ';
+ const nestingIdentLevel = Math.max(0, ctx.listNestingLevel - 1);
+ el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`;
+ return el;
+ },
+ INPUT(el) {
+ return el.checked ? '[x] ' : '[ ] ';
+ },
+
+ CODE(el) {
+ const text = el.textContent;
+ if (el.parentNode && el.parentNode.tagName === 'PRE') {
+ el.textContent = `\`\`\`\n${text}\n\`\`\`\n`;
+ return el;
+ }
+ if (text.includes('`')) {
+ return `\`\` ${text} \`\``;
+ }
+ return `\`${text}\``;
+ },
+ };
+ processors['UL'] = processors.OL;
+ for (let level = 2; level <= 6; level++) {
+ processors[`H${level}`] = processors.H1;
+ }
+ return processors;
+}
+
+function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement) {
+ if (el.hasAttribute('data-markdown-generated-content')) return el.textContent;
+ if (el.tagName === 'A' && el.children.length === 1 && el.children[0].tagName === 'IMG') {
+ return processElement(ctx, processors, el.children[0] as HTMLElement);
+ }
+
+ const isListContainer = el.tagName === 'OL' || el.tagName === 'UL';
+ if (isListContainer) ctx.listNestingLevel++;
+ for (let i = 0; i < el.children.length; i++) {
+ ctx.elementIsFirst = i === 0;
+ ctx.elementIsLast = i === el.children.length - 1;
+ processElement(ctx, processors, el.children[i] as HTMLElement);
+ }
+ if (isListContainer) ctx.listNestingLevel--;
+
+ if (processors[el.tagName]) {
+ const ret = processors[el.tagName](el);
+ if (ret && ret !== el) {
+ el.replaceWith(typeof ret === 'string' ? document.createTextNode(ret) : ret);
+ }
+ }
+}
+
+export function convertHtmlToMarkdown(el: HTMLElement): string {
+ const div = document.createElement('div');
+ div.append(el);
+ const ctx = {} as ProcessorContext;
+ ctx.listNestingLevel = 0;
+ processElement(ctx, prepareProcessors(ctx), el);
+ return div.textContent;
+}
From 331e878e81d57235a53199383087c7649797a887 Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Wed, 6 Nov 2024 22:41:49 -0800
Subject: [PATCH 003/100] Add new event commit status creation and webhook
implementation (#27151)
This PR introduces a new event which is similar as Github's. When a new
commit status submitted, the event will be trigged. That means, now we
can receive all feedback from CI/CD system in webhooks or other notify
systems.
ref:
https://docs.github.com/en/webhooks/webhook-events-and-payloads#status
Fix #20749
---
modules/repository/commits.go | 8 ++---
modules/structs/hook.go | 30 +++++++++++++-----
modules/webhook/type.go | 1 +
services/actions/commit_status.go | 6 ++--
services/automerge/notify.go | 11 +++++++
services/notify/notifier.go | 3 ++
services/notify/notify.go | 7 +++++
services/notify/null.go | 4 +++
.../repository/commitstatus/commitstatus.go | 11 +++----
services/webhook/notifier.go | 31 +++++++++++++++++++
10 files changed, 90 insertions(+), 22 deletions(-)
diff --git a/modules/repository/commits.go b/modules/repository/commits.go
index ede60429a1..6e4b75d5ca 100644
--- a/modules/repository/commits.go
+++ b/modules/repository/commits.go
@@ -42,8 +42,8 @@ func NewPushCommits() *PushCommits {
return &PushCommits{}
}
-// toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
-func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) {
+// ToAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
+func ToAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) {
var err error
authorUsername := ""
author, ok := emailUsers[commit.AuthorEmail]
@@ -105,7 +105,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
emailUsers := make(map[string]*user_model.User)
for i, commit := range pc.Commits {
- apiCommit, err := pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit)
+ apiCommit, err := ToAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit)
if err != nil {
return nil, nil, err
}
@@ -117,7 +117,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
}
if pc.HeadCommit != nil && headCommit == nil {
var err error
- headCommit, err = pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit)
+ headCommit, err = ToAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit)
if err != nil {
return nil, nil, err
}
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index db8b20e7e5..ce5742e5c7 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -262,13 +262,6 @@ func (p *ReleasePayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
-// __________ .__
-// \______ \__ __ _____| |__
-// | ___/ | \/ ___/ | \
-// | | | | /\___ \| Y \
-// |____| |____//____ >___| /
-// \/ \/
-
// PushPayload represents a payload information of push event.
type PushPayload struct {
Ref string `json:"ref"`
@@ -509,3 +502,26 @@ type WorkflowDispatchPayload struct {
func (p *WorkflowDispatchPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
+
+// CommitStatusPayload represents a payload information of commit status event.
+type CommitStatusPayload struct {
+ // TODO: add Branches per https://docs.github.com/en/webhooks/webhook-events-and-payloads#status
+ Commit *PayloadCommit `json:"commit"`
+ Context string `json:"context"`
+ // swagger:strfmt date-time
+ CreatedAt time.Time `json:"created_at"`
+ Description string `json:"description"`
+ ID int64 `json:"id"`
+ Repo *Repository `json:"repository"`
+ Sender *User `json:"sender"`
+ SHA string `json:"sha"`
+ State string `json:"state"`
+ TargetURL string `json:"target_url"`
+ // swagger:strfmt date-time
+ UpdatedAt *time.Time `json:"updated_at"`
+}
+
+// JSONPayload implements Payload
+func (p *CommitStatusPayload) JSONPayload() ([]byte, error) {
+ return json.MarshalIndent(p, "", " ")
+}
diff --git a/modules/webhook/type.go b/modules/webhook/type.go
index 0013691c02..fbec889272 100644
--- a/modules/webhook/type.go
+++ b/modules/webhook/type.go
@@ -32,6 +32,7 @@ const (
HookEventRelease HookEventType = "release"
HookEventPackage HookEventType = "package"
HookEventSchedule HookEventType = "schedule"
+ HookEventStatus HookEventType = "status"
)
// Event returns the HookEventType as an event string
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index 8d86ec4dfa..7f52c9d31b 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -128,18 +128,16 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
if err != nil {
return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err)
}
- if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{
+ status := git_model.CommitStatus{
SHA: sha,
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
Description: description,
Context: ctxname,
CreatorID: creator.ID,
State: state,
- }); err != nil {
- return fmt.Errorf("NewCommitStatus: %w", err)
}
- return nil
+ return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &status)
}
func toCommitStatus(status actions_model.Status) api.CommitStatusState {
diff --git a/services/automerge/notify.go b/services/automerge/notify.go
index cb078214f6..b6bbca333b 100644
--- a/services/automerge/notify.go
+++ b/services/automerge/notify.go
@@ -6,9 +6,12 @@ package automerge
import (
"context"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/repository"
notify_service "code.gitea.io/gitea/services/notify"
)
@@ -44,3 +47,11 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
// as reviews could have blocked a pending automerge let's recheck
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
}
+
+func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
+ if status.State.IsSuccess() {
+ if err := StartPRCheckAndAutoMergeBySHA(ctx, commit.Sha1, repo); err != nil {
+ log.Error("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, sender.ID, commit.Sha1, err)
+ }
+ }
+}
diff --git a/services/notify/notifier.go b/services/notify/notifier.go
index ed053a812a..29bbb5702b 100644
--- a/services/notify/notifier.go
+++ b/services/notify/notifier.go
@@ -6,6 +6,7 @@ package notify
import (
"context"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
@@ -74,4 +75,6 @@ type Notifier interface {
PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository)
+
+ CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus)
}
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 0c8262ef7a..3b5f24340b 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -6,6 +6,7 @@ package notify
import (
"context"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
@@ -367,3 +368,9 @@ func ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
notifier.ChangeDefaultBranch(ctx, repo)
}
}
+
+func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
+ for _, notifier := range notifiers {
+ notifier.CreateCommitStatus(ctx, repo, commit, sender, status)
+ }
+}
diff --git a/services/notify/null.go b/services/notify/null.go
index dddd421bef..7354efd701 100644
--- a/services/notify/null.go
+++ b/services/notify/null.go
@@ -6,6 +6,7 @@ package notify
import (
"context"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
@@ -208,3 +209,6 @@ func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, p
// ChangeDefaultBranch places a place holder function
func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
}
+
+func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
+}
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index adc59abed8..f369a303e6 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -18,8 +18,9 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/automerge"
+ "code.gitea.io/gitea/services/notify"
)
func getCacheKey(repoID int64, brancheName string) string {
@@ -103,6 +104,8 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
return err
}
+ notify.CreateCommitStatus(ctx, repo, repo_module.CommitToPushCommit(commit), creator, status)
+
defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
@@ -114,12 +117,6 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
}
}
- if status.State.IsSuccess() {
- if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil {
- return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
- }
- }
-
return nil
}
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 38fad7f5e8..cc263947e9 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -6,6 +6,7 @@ package webhook
import (
"context"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/perm"
@@ -861,6 +862,36 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode
}
}
+func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
+ apiSender := convert.ToUser(ctx, sender, nil)
+ apiCommit, err := repository.ToAPIPayloadCommit(ctx, map[string]*user_model.User{}, repo.RepoPath(), repo.HTMLURL(), commit)
+ if err != nil {
+ log.Error("commits.ToAPIPayloadCommits failed: %v", err)
+ return
+ }
+
+ payload := api.CommitStatusPayload{
+ Context: status.Context,
+ CreatedAt: status.CreatedUnix.AsTime().UTC(),
+ Description: status.Description,
+ ID: status.ID,
+ SHA: commit.Sha1,
+ State: status.State.String(),
+ TargetURL: status.TargetURL,
+
+ Commit: apiCommit,
+ Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
+ Sender: apiSender,
+ }
+ if !status.UpdatedUnix.IsZero() {
+ t := status.UpdatedUnix.AsTime().UTC()
+ payload.UpdatedAt = &t
+ }
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventStatus, &payload); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
func (m *webhookNotifier) SyncCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
m.CreateRef(ctx, pusher, repo, refFullName, refID)
}
From fb030624780f426472b8464aac0d50228f93017a Mon Sep 17 00:00:00 2001
From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:56:53 -0800
Subject: [PATCH 004/100] Only provide the commit summary for Discord webhook
push events (#32432)
Resolves #32371.
#31970 should have just showed the commit summary, but
`strings.SplitN()` was misused such that we did not perform any
splitting at all and just used the message. This was not caught in the
unit test made in that PR since the test commit summary was > 50 (which
truncated away the commit description).
This snapshot resolves this and adds another unit test to ensure that we
only show the commit summary.
---
services/webhook/discord.go | 2 +-
services/webhook/discord_test.go | 16 +++++++++++++++-
services/webhook/general_test.go | 2 +-
3 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 59e87a7e1f..c562d98168 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -156,7 +156,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
// for each commit, generate attachment text
for i, commit := range p.Commits {
// limit the commit message display to just the summary, otherwise it would be hard to read
- message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 1)[0], "\r")
+ message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 2)[0], "\r")
// a limit of 50 is set because GitHub does the same
if utf8.RuneCountInString(message) > 50 {
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index fbb4b24ef1..36b99d452e 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -80,12 +80,26 @@ func TestDiscordPayload(t *testing.T) {
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
- t.Run("PushWithLongCommitMessage", func(t *testing.T) {
+ t.Run("PushWithMultilineCommitMessage", func(t *testing.T) {
p := pushTestMultilineCommitMessagePayload()
pl, err := dc.Push(p)
require.NoError(t, err)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
+ assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1", pl.Embeds[0].Description)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
+ })
+
+ t.Run("PushWithLongCommitSummary", func(t *testing.T) {
+ p := pushTestPayloadWithCommitMessage("This is a commit summary âš ï¸âš ï¸âš ï¸âš ï¸ containing ä½ å¥½ âš ï¸âš ï¸ï¸\n\nThis is the message body")
+
+ pl, err := dc.Push(p)
+ require.NoError(t, err)
+
assert.Len(t, pl.Embeds, 1)
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary âš ï¸âš ï¸âš ï¸âš ï¸ containing ä½ å¥½... - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary âš ï¸âš ï¸âš ï¸âš ï¸ containing ä½ å¥½... - user1", pl.Embeds[0].Description)
diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go
index d6a77c9442..ef1ec7f324 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -68,7 +68,7 @@ func pushTestPayload() *api.PushPayload {
}
func pushTestMultilineCommitMessagePayload() *api.PushPayload {
- return pushTestPayloadWithCommitMessage("This is a commit summary âš ï¸âš ï¸âš ï¸âš ï¸ containing ä½ å¥½ âš ï¸âš ï¸ï¸\n\nThis is the message body.")
+ return pushTestPayloadWithCommitMessage("chore: This is a commit summary\n\nThis is a commit description.")
}
func pushTestPayloadWithCommitMessage(message string) *api.PushPayload {
From 028e6120943107fdfb25bf8ada9e3fc2e14e37e0 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Fri, 8 Nov 2024 09:44:20 +0800
Subject: [PATCH 005/100] Split issue sidebar into small templates (#32444)
Only move code
---
.../repo/issue/branch_selector_field.tmpl | 2 -
.../issue/sidebar/allow_maintainer_edit.tmpl | 15 +
.../repo/issue/sidebar/assignee_list.tmpl | 46 ++
templates/repo/issue/sidebar/due_date.tmpl | 29 +
.../issue/sidebar/issue_dependencies.tmpl | 149 ++++
.../repo/issue/sidebar/issue_management.tmpl | 118 +++
.../repo/issue/sidebar/milestone_list.tmpl | 23 +
.../repo/issue/sidebar/participant_list.tmpl | 11 +
.../repo/issue/sidebar/project_list.tmpl | 53 ++
.../repo/issue/sidebar/reference_link.tmpl | 8 +
.../repo/issue/sidebar/reviewer_list.tmpl | 116 +++
.../issue/sidebar/stopwatch_timetracker.tmpl | 75 ++
.../issue/sidebar/watch_notification.tmpl | 9 +
templates/repo/issue/sidebar/wip_switch.tmpl | 7 +
.../repo/issue/view_content/sidebar.tmpl | 690 +-----------------
15 files changed, 675 insertions(+), 676 deletions(-)
create mode 100644 templates/repo/issue/sidebar/allow_maintainer_edit.tmpl
create mode 100644 templates/repo/issue/sidebar/assignee_list.tmpl
create mode 100644 templates/repo/issue/sidebar/due_date.tmpl
create mode 100644 templates/repo/issue/sidebar/issue_dependencies.tmpl
create mode 100644 templates/repo/issue/sidebar/issue_management.tmpl
create mode 100644 templates/repo/issue/sidebar/milestone_list.tmpl
create mode 100644 templates/repo/issue/sidebar/participant_list.tmpl
create mode 100644 templates/repo/issue/sidebar/project_list.tmpl
create mode 100644 templates/repo/issue/sidebar/reference_link.tmpl
create mode 100644 templates/repo/issue/sidebar/reviewer_list.tmpl
create mode 100644 templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
create mode 100644 templates/repo/issue/sidebar/watch_notification.tmpl
create mode 100644 templates/repo/issue/sidebar/wip_switch.tmpl
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl
index 5793a8bfda..643222ca72 100644
--- a/templates/repo/issue/branch_selector_field.tmpl
+++ b/templates/repo/issue/branch_selector_field.tmpl
@@ -44,6 +44,4 @@
-
-
{{end}}
diff --git a/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl b/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl
new file mode 100644
index 0000000000..43736deeed
--- /dev/null
+++ b/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl
@@ -0,0 +1,15 @@
+{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
+ {{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
+
+
+
+ {{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}
+
+
+
+ {{end}}
+{{end}}
diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl
new file mode 100644
index 0000000000..260f7c5be4
--- /dev/null
+++ b/templates/repo/issue/sidebar/assignee_list.tmpl
@@ -0,0 +1,46 @@
+
+
+
+
+
{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
+
+ {{range .Issue.Assignees}}
+
+
+
+ {{end}}
+
+
diff --git a/templates/repo/issue/sidebar/due_date.tmpl b/templates/repo/issue/sidebar/due_date.tmpl
new file mode 100644
index 0000000000..e6e19f6f86
--- /dev/null
+++ b/templates/repo/issue/sidebar/due_date.tmpl
@@ -0,0 +1,29 @@
+
+{{ctx.Locale.Tr "repo.issues.due_date"}}
+
diff --git a/templates/repo/issue/sidebar/issue_dependencies.tmpl b/templates/repo/issue/sidebar/issue_dependencies.tmpl
new file mode 100644
index 0000000000..f372ff81b2
--- /dev/null
+++ b/templates/repo/issue/sidebar/issue_dependencies.tmpl
@@ -0,0 +1,149 @@
+{{if .Repository.IsDependenciesEnabled ctx}}
+
+
+
+ {{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}}
+
{{ctx.Locale.Tr "repo.issues.dependency.title"}}
+
+
+ {{if .Issue.IsPull}}
+ {{ctx.Locale.Tr "repo.issues.dependency.pr_no_dependencies"}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.issues.dependency.issue_no_dependencies"}}
+ {{end}}
+
+ {{end}}
+
+ {{if or .BlockingDependencies .BlockingDependenciesNotPermitted}}
+
+ {{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}
+
+
+ {{range .BlockingDependencies}}
+
+ {{end}}
+ {{if .BlockingDependenciesNotPermitted}}
+
+ {{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}
+
+ {{end}}
+
+ {{end}}
+
+ {{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}}
+
+ {{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}
+
+
+ {{range .BlockedByDependencies}}
+
+ {{end}}
+ {{if $.CanCreateIssueDependencies}}
+ {{range .BlockedByDependenciesNotPermitted}}
+
+
+
+ {{svg "octicon-lock" 16}}
+
+ #{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
+
+
+
+ {{.Repository.OwnerName}}/{{.Repository.Name}}
+
+
+
+
+ {{end}}
+ {{else if .BlockedByDependenciesNotPermitted}}
+
+ {{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}
+
+ {{end}}
+
+ {{end}}
+
+ {{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
+
+ {{end}}
+
+
+ {{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
+
+
+
+
+
+
+
{{if .Issue.IsPull}}
+ {{ctx.Locale.Tr "repo.issues.dependency.pr_remove_text"}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.issues.dependency.issue_remove_text"}}
+ {{end}}
+
+ {{$ModalButtonCancelText := ctx.Locale.Tr "repo.issues.dependency.cancel"}}
+ {{$ModalButtonOkText := ctx.Locale.Tr "repo.issues.dependency.remove"}}
+ {{template "base/modal_actions_confirm" (dict "." . "ModalButtonCancelText" $ModalButtonCancelText "ModalButtonOkText" $ModalButtonOkText)}}
+
+ {{end}}
+{{end}}
diff --git a/templates/repo/issue/sidebar/issue_management.tmpl b/templates/repo/issue/sidebar/issue_management.tmpl
new file mode 100644
index 0000000000..3342d99212
--- /dev/null
+++ b/templates/repo/issue/sidebar/issue_management.tmpl
@@ -0,0 +1,118 @@
+{{if and .IsRepoAdmin (not .Repository.IsArchived)}}
+
+
+ {{if or .PinEnabled .Issue.IsPinned}}
+
+ {{end}}
+
+
+ {{if .Issue.IsLocked}}
+ {{svg "octicon-key"}}
+ {{ctx.Locale.Tr "repo.issues.unlock"}}
+ {{else}}
+ {{svg "octicon-lock"}}
+ {{ctx.Locale.Tr "repo.issues.lock"}}
+ {{end}}
+
+
+
+
+
+ {{if .Issue.IsLocked}}
+ {{ctx.Locale.Tr "repo.issues.unlock.notice_1"}}
+ {{ctx.Locale.Tr "repo.issues.unlock.notice_2"}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.issues.lock.notice_1"}}
+ {{ctx.Locale.Tr "repo.issues.lock.notice_2"}}
+ {{ctx.Locale.Tr "repo.issues.lock.notice_3"}}
+ {{end}}
+
+
+
+
+
+
+ {{svg "octicon-trash"}}
+ {{ctx.Locale.Tr "repo.issues.delete"}}
+
+
+{{end}}
diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl
new file mode 100644
index 0000000000..e9ca02f77a
--- /dev/null
+++ b/templates/repo/issue/sidebar/milestone_list.tmpl
@@ -0,0 +1,23 @@
+
+
+
+
{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
+
+ {{if .Issue.Milestone}}
+
+ {{end}}
+
+
diff --git a/templates/repo/issue/sidebar/participant_list.tmpl b/templates/repo/issue/sidebar/participant_list.tmpl
new file mode 100644
index 0000000000..91c36fc01e
--- /dev/null
+++ b/templates/repo/issue/sidebar/participant_list.tmpl
@@ -0,0 +1,11 @@
+{{if .Participants}}
+
+ {{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}
+
+{{end}}
diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl
new file mode 100644
index 0000000000..ec79f8032f
--- /dev/null
+++ b/templates/repo/issue/sidebar/project_list.tmpl
@@ -0,0 +1,53 @@
+{{if .IsProjectsEnabled}}
+
+
+
+
+
{{ctx.Locale.Tr "repo.issues.new.no_projects"}}
+
+ {{if .Issue.Project}}
+
+ {{end}}
+
+
+{{end}}
diff --git a/templates/repo/issue/sidebar/reference_link.tmpl b/templates/repo/issue/sidebar/reference_link.tmpl
new file mode 100644
index 0000000000..6b8f120c7b
--- /dev/null
+++ b/templates/repo/issue/sidebar/reference_link.tmpl
@@ -0,0 +1,8 @@
+
+
+ {{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
+
+ {{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}
+ {{svg "octicon-copy" 14}}
+
+
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl
new file mode 100644
index 0000000000..cf4d067c0f
--- /dev/null
+++ b/templates/repo/issue/sidebar/reviewer_list.tmpl
@@ -0,0 +1,116 @@
+
+
+
+
+
{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}
+
+ {{range .PullReviewers}}
+
+
+ {{if .User}}
+
+ {{else if .Team}}
+ {{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
+ {{end}}
+
+
+ {{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
+
+ {{svg "octicon-x" 20}}
+
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
+
+
+
+
+ {{end}}
+ {{if .Review.Stale}}
+
+ {{svg "octicon-hourglass" 16}}
+
+ {{end}}
+ {{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
+
{{svg (Iif .Checked "octicon-trash" "octicon-sync")}}
+ {{end}}
+
+ {{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
+
+
+
+ {{end}}
+ {{range .OriginalReviews}}
+
+
+
+
+ {{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
+
+
+
+ {{end}}
+
+
diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
new file mode 100644
index 0000000000..9a49664b0e
--- /dev/null
+++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
@@ -0,0 +1,75 @@
+{{if .Repository.IsTimetrackerEnabled ctx}}
+ {{if and .CanUseTimetracker (not .Repository.IsArchived)}}
+
+
+
{{ctx.Locale.Tr "repo.issues.tracker"}}
+
+
+
+ {{if $.IsStopwatchRunning}}
+
+ {{svg "octicon-stopwatch" 16 "tw-mr-2"}}
+ {{ctx.Locale.Tr "repo.issues.stop_tracking"}}
+
+
+ {{svg "octicon-trash" 16 "tw-mr-2"}}
+ {{ctx.Locale.Tr "repo.issues.cancel_tracking"}}
+
+ {{else}}
+ {{if .HasUserStopwatch}}
+
+ {{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}
+
+ {{end}}
+
+ {{svg "octicon-stopwatch" 16 "tw-mr-2"}}
+ {{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
+
+
+
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.add_time_short"}}
+ {{ctx.Locale.Tr "repo.issues.add_time_cancel"}}
+
+
+
+ {{svg "octicon-plus" 16 "tw-mr-2"}}
+ {{ctx.Locale.Tr "repo.issues.add_time_short"}}
+
+ {{end}}
+
+
+ {{end}}
+ {{if .WorkingUsers}}
+
+
+ {{end}}
+{{end}}
diff --git a/templates/repo/issue/sidebar/watch_notification.tmpl b/templates/repo/issue/sidebar/watch_notification.tmpl
new file mode 100644
index 0000000000..aafee87ba3
--- /dev/null
+++ b/templates/repo/issue/sidebar/watch_notification.tmpl
@@ -0,0 +1,9 @@
+{{if and $.IssueWatch (not .Repository.IsArchived)}}
+
+
+
{{ctx.Locale.Tr "notification.notifications"}}
+
+ {{template "repo/issue/view_content/watching" .}}
+
+
+{{end}}
diff --git a/templates/repo/issue/sidebar/wip_switch.tmpl b/templates/repo/issue/sidebar/wip_switch.tmpl
new file mode 100644
index 0000000000..2f8994673e
--- /dev/null
+++ b/templates/repo/issue/sidebar/wip_switch.tmpl
@@ -0,0 +1,7 @@
+{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
+
+{{end}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 9c1acae0cf..7afb76968a 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -1,682 +1,24 @@
- {{template "repo/issue/branch_selector_field" .}}
- {{if .Issue.IsPull}}
-
-
+ {{template "repo/issue/branch_selector_field" $}}
-
-
{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}
-
- {{range .PullReviewers}}
-
-
- {{if .User}}
-
- {{else if .Team}}
- {{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
- {{end}}
-
-
- {{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
-
- {{svg "octicon-x" 20}}
-
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
-
-
-
-
- {{end}}
- {{if .Review.Stale}}
-
- {{svg "octicon-hourglass" 16}}
-
- {{end}}
- {{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
-
{{svg (Iif .Checked "octicon-trash" "octicon-sync")}}
- {{end}}
-
- {{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
-
-
-
- {{end}}
- {{range .OriginalReviews}}
-
-
-
-
- {{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
-
-
-
- {{end}}
-
-
- {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
-
- {{end}}
+ {{if .Issue.IsPull}}
+ {{template "repo/issue/sidebar/reviewer_list" $}}
+ {{template "repo/issue/sidebar/wip_switch" $}}
{{end}}
- {{template "repo/issue/labels/labels_selector_field" .}}
+ {{template "repo/issue/labels/labels_selector_field" $}}
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
-
-
-
-
-
{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
-
- {{if .Issue.Milestone}}
-
- {{end}}
-
-
-
- {{if .IsProjectsEnabled}}
-
-
-
-
-
{{ctx.Locale.Tr "repo.issues.new.no_projects"}}
-
- {{if .Issue.Project}}
-
- {{end}}
-
-
- {{end}}
-
-
-
-
-
-
-
{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
-
- {{range .Issue.Assignees}}
-
-
-
- {{end}}
-
-
-
-
-
- {{if .Participants}}
-
{{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}
-
- {{end}}
-
- {{if and $.IssueWatch (not .Repository.IsArchived)}}
-
-
-
-
{{ctx.Locale.Tr "notification.notifications"}}
-
- {{template "repo/issue/view_content/watching" .}}
-
-
- {{end}}
- {{if .Repository.IsTimetrackerEnabled ctx}}
- {{if and .CanUseTimetracker (not .Repository.IsArchived)}}
-
-
-
{{ctx.Locale.Tr "repo.issues.tracker"}}
-
-
-
- {{if $.IsStopwatchRunning}}
-
- {{svg "octicon-stopwatch" 16 "tw-mr-2"}}
- {{ctx.Locale.Tr "repo.issues.stop_tracking"}}
-
-
- {{svg "octicon-trash" 16 "tw-mr-2"}}
- {{ctx.Locale.Tr "repo.issues.cancel_tracking"}}
-
- {{else}}
- {{if .HasUserStopwatch}}
-
- {{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}
-
- {{end}}
-
- {{svg "octicon-stopwatch" 16 "tw-mr-2"}}
- {{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
-
-
-
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.add_time_short"}}
- {{ctx.Locale.Tr "repo.issues.add_time_cancel"}}
-
-
-
- {{svg "octicon-plus" 16 "tw-mr-2"}}
- {{ctx.Locale.Tr "repo.issues.add_time_short"}}
-
- {{end}}
-
-
- {{end}}
- {{if .WorkingUsers}}
-
-
- {{end}}
- {{end}}
-
-
-
{{ctx.Locale.Tr "repo.issues.due_date"}}
-
-
- {{if .Repository.IsDependenciesEnabled ctx}}
-
-
-
- {{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}}
-
{{ctx.Locale.Tr "repo.issues.dependency.title"}}
-
-
- {{if .Issue.IsPull}}
- {{ctx.Locale.Tr "repo.issues.dependency.pr_no_dependencies"}}
- {{else}}
- {{ctx.Locale.Tr "repo.issues.dependency.issue_no_dependencies"}}
- {{end}}
-
- {{end}}
-
- {{if or .BlockingDependencies .BlockingDependenciesNotPermitted}}
-
- {{ctx.Locale.Tr "repo.issues.dependency.blocks_short"}}
-
-
- {{range .BlockingDependencies}}
-
- {{end}}
- {{if .BlockingDependenciesNotPermitted}}
-
- {{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}
-
- {{end}}
-
- {{end}}
-
- {{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}}
-
- {{ctx.Locale.Tr "repo.issues.dependency.blocked_by_short"}}
-
-
- {{range .BlockedByDependencies}}
-
- {{end}}
- {{if $.CanCreateIssueDependencies}}
- {{range .BlockedByDependenciesNotPermitted}}
-
-
-
- {{svg "octicon-lock" 16}}
-
- #{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
-
-
-
- {{.Repository.OwnerName}}/{{.Repository.Name}}
-
-
-
-
- {{end}}
- {{else if .BlockedByDependenciesNotPermitted}}
-
- {{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}
-
- {{end}}
-
- {{end}}
-
- {{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
-
- {{end}}
-
-
- {{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
-
-
-
-
-
-
-
{{if .Issue.IsPull}}
- {{ctx.Locale.Tr "repo.issues.dependency.pr_remove_text"}}
- {{else}}
- {{ctx.Locale.Tr "repo.issues.dependency.issue_remove_text"}}
- {{end}}
-
- {{$ModalButtonCancelText := ctx.Locale.Tr "repo.issues.dependency.cancel"}}
- {{$ModalButtonOkText := ctx.Locale.Tr "repo.issues.dependency.remove"}}
- {{template "base/modal_actions_confirm" (dict "." . "ModalButtonCancelText" $ModalButtonCancelText "ModalButtonOkText" $ModalButtonOkText)}}
-
- {{end}}
- {{end}}
-
-
-
- {{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
-
- {{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}
- {{svg "octicon-copy" 14}}
-
-
-
- {{if and .IsRepoAdmin (not .Repository.IsArchived)}}
-
-
- {{if or .PinEnabled .Issue.IsPinned}}
-
- {{end}}
-
-
- {{if .Issue.IsLocked}}
- {{svg "octicon-key"}}
- {{ctx.Locale.Tr "repo.issues.unlock"}}
- {{else}}
- {{svg "octicon-lock"}}
- {{ctx.Locale.Tr "repo.issues.lock"}}
- {{end}}
-
-
-
-
-
- {{if .Issue.IsLocked}}
- {{ctx.Locale.Tr "repo.issues.unlock.notice_1"}}
- {{ctx.Locale.Tr "repo.issues.unlock.notice_2"}}
- {{else}}
- {{ctx.Locale.Tr "repo.issues.lock.notice_1"}}
- {{ctx.Locale.Tr "repo.issues.lock.notice_2"}}
- {{ctx.Locale.Tr "repo.issues.lock.notice_3"}}
- {{end}}
-
-
-
-
-
-
- {{svg "octicon-trash"}}
- {{ctx.Locale.Tr "repo.issues.delete"}}
-
-
- {{end}}
-
- {{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}}
- {{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
-
-
-
- {{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}
-
-
-
- {{end}}
- {{end}}
+ {{template "repo/issue/sidebar/milestone_list" $}}
+ {{template "repo/issue/sidebar/project_list" $}}
+ {{template "repo/issue/sidebar/assignee_list" $}}
+ {{template "repo/issue/sidebar/participant_list" $}}
+ {{template "repo/issue/sidebar/watch_notification" $}}
+ {{template "repo/issue/sidebar/stopwatch_timetracker" $}}
+ {{template "repo/issue/sidebar/due_date" $}}
+ {{template "repo/issue/sidebar/issue_dependencies" $}}
+ {{template "repo/issue/sidebar/reference_link" $}}
+ {{template "repo/issue/sidebar/issue_management" $}}
+ {{template "repo/issue/sidebar/allow_maintainer_edit" $}}
From 623a2d41cc866d31e6348c58cccf336b67a4b2c7 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Fri, 8 Nov 2024 10:21:13 +0800
Subject: [PATCH 006/100] Refactor issue page info (#32445)
Fix a longstanding TODO since 2021 (#14826) / 2018 (#2531)
---
routers/web/repo/issue.go | 6 +++---
templates/repo/issue/view_content.tmpl | 7 -------
templates/repo/issue/view_title.tmpl | 6 ++++++
templates/repo/pulls/files.tmpl | 4 ----
web_src/js/features/repo-issue-content.ts | 9 ++++-----
web_src/js/features/repo-issue.ts | 10 ++++------
web_src/js/types.ts | 7 +++++++
web_src/js/utils.ts | 14 ++++++++++++--
8 files changed, 36 insertions(+), 27 deletions(-)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 1ee6e98afb..c4fc535446 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1443,11 +1443,11 @@ func ViewIssue(ctx *context.Context) {
}
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
- ctx.Data["IssueType"] = "pulls"
+ ctx.Data["IssueDependencySearchType"] = "pulls"
} else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
- ctx.Data["IssueType"] = "issues"
+ ctx.Data["IssueDependencySearchType"] = "issues"
} else {
- ctx.Data["IssueType"] = "all"
+ ctx.Data["IssueDependencySearchType"] = "all"
}
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 1f9bbd86aa..3f7b2dc78f 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -1,11 +1,4 @@
-
-
-
-
-
-
-
{{$createdStr:= DateUtils.TimeSince .Issue.CreatedUnix}}
block and the block becomes visible upon user interaction, it
+ // would initially set a incorrect height and the correct height is set during this callback.
+ (new IntersectionObserver(() => {
+ updateIframeHeight();
+ }, {root: document.documentElement})).observe(iframe);
+
iframe.addEventListener('load', () => {
pre.replaceWith(mermaidBlock);
mermaidBlock.classList.remove('tw-hidden');
- iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
+ updateIframeHeight();
setTimeout(() => { // avoid flash of iframe background
mermaidBlock.classList.remove('is-loading');
iframe.classList.remove('tw-invisible');
From 58c634b8549fb279aec72cecd6a48511803db067 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Sun, 10 Nov 2024 16:26:42 +0800
Subject: [PATCH 012/100] Refactor sidebar label selector (#32460)
Introduce `issueSidebarLabelsData` to handle all sidebar labels related data.
---
routers/web/repo/compare.go | 15 +-
routers/web/repo/issue.go | 181 ++++++++++--------
routers/web/repo/issue_label.go | 6 +-
routers/web/repo/issue_label_test.go | 2 +-
routers/web/web.go | 2 +-
templates/repo/issue/labels/label.tmpl | 7 -
.../issue/labels/labels_selector_field.tmpl | 46 -----
.../repo/issue/labels/labels_sidebar.tmpl | 11 --
templates/repo/issue/new_form.tmpl | 16 +-
templates/repo/issue/sidebar/label_list.tmpl | 51 +++++
.../repo/issue/sidebar/label_list_item.tmpl | 11 ++
.../repo/issue/sidebar/reviewer_list.tmpl | 16 +-
.../repo/issue/view_content/sidebar.tmpl | 5 +-
web_src/css/repo.css | 8 +-
web_src/css/repo/issue-label.css | 5 +-
web_src/js/features/common-page.ts | 4 +-
.../features/repo-issue-sidebar-combolist.ts | 46 +++--
web_src/js/features/repo-issue-sidebar.md | 27 +++
web_src/js/features/repo-issue-sidebar.ts | 22 +--
web_src/js/features/repo-issue.ts | 15 +-
web_src/js/index.ts | 3 +-
web_src/js/utils/dom.ts | 10 +-
22 files changed, 276 insertions(+), 233 deletions(-)
delete mode 100644 templates/repo/issue/labels/label.tmpl
delete mode 100644 templates/repo/issue/labels/labels_selector_field.tmpl
delete mode 100644 templates/repo/issue/labels/labels_sidebar.tmpl
create mode 100644 templates/repo/issue/sidebar/label_list.tmpl
create mode 100644 templates/repo/issue/sidebar/label_list_item.tmpl
create mode 100644 web_src/js/features/repo-issue-sidebar.md
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 3477ba36e8..9a7d3dfbf6 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -788,7 +788,11 @@ func CompareDiff(ctx *context.Context) {
if !nothingToCompare {
// Setup information for new form.
- RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
+ retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, true)
+ if ctx.Written() {
+ return
+ }
+ labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, true)
if ctx.Written() {
return
}
@@ -796,6 +800,10 @@ func CompareDiff(ctx *context.Context) {
if ctx.Written() {
return
}
+ _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, labelsData)
+ if len(templateErrs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ }
}
}
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
@@ -808,11 +816,6 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
ctx.Data["IsDiffCompare"] = true
- _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
-
- if len(templateErrs) > 0 {
- ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
- }
if content, ok := ctx.Data["content"].(string); ok && content != "" {
// If a template content is set, prepend the "content". In this case that's only
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 7fa8d428d3..a4e2fd8cea 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -870,51 +870,112 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
ctx.Data["IssueSidebarReviewersData"] = data
}
-// RetrieveRepoMetas find all the meta information of a repository
-func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
- if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
- return nil
+type issueSidebarLabelsData struct {
+ Repository *repo_model.Repository
+ RepoLink string
+ IssueID int64
+ IsPullRequest bool
+ AllLabels []*issues_model.Label
+ RepoLabels []*issues_model.Label
+ OrgLabels []*issues_model.Label
+ SelectedLabelIDs string
+}
+
+func makeSelectedStringIDs[KeyType, ItemType comparable](
+ allLabels []*issues_model.Label, candidateKey func(candidate *issues_model.Label) KeyType,
+ selectedItems []ItemType, selectedKey func(selected ItemType) KeyType,
+) string {
+ selectedIDSet := make(container.Set[string])
+ allLabelMap := map[KeyType]*issues_model.Label{}
+ for _, label := range allLabels {
+ allLabelMap[candidateKey(label)] = label
}
+ for _, item := range selectedItems {
+ if label, ok := allLabelMap[selectedKey(item)]; ok {
+ label.IsChecked = true
+ selectedIDSet.Add(strconv.FormatInt(label.ID, 10))
+ }
+ }
+ ids := selectedIDSet.Values()
+ sort.Strings(ids)
+ return strings.Join(ids, ",")
+}
+
+func (d *issueSidebarLabelsData) SetSelectedLabels(labels []*issues_model.Label) {
+ d.SelectedLabelIDs = makeSelectedStringIDs(
+ d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
+ labels, func(label *issues_model.Label) int64 { return label.ID },
+ )
+}
+
+func (d *issueSidebarLabelsData) SetSelectedLabelNames(labelNames []string) {
+ d.SelectedLabelIDs = makeSelectedStringIDs(
+ d.AllLabels, func(label *issues_model.Label) string { return strings.ToLower(label.Name) },
+ labelNames, strings.ToLower,
+ )
+}
+
+func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
+ d.SelectedLabelIDs = makeSelectedStringIDs(
+ d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
+ labelIDs, func(labelID int64) int64 { return labelID },
+ )
+}
+
+func retrieveRepoLabels(ctx *context.Context, repo *repo_model.Repository, issueID int64, isPull bool) *issueSidebarLabelsData {
+ labelsData := &issueSidebarLabelsData{
+ Repository: repo,
+ RepoLink: ctx.Repo.RepoLink,
+ IssueID: issueID,
+ IsPullRequest: isPull,
+ }
+ ctx.Data["IssueSidebarLabelsData"] = labelsData
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return nil
}
- ctx.Data["Labels"] = labels
+ labelsData.RepoLabels = labels
+
if repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
return nil
}
+ labelsData.OrgLabels = orgLabels
+ }
+ labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
+ labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
+ return labelsData
+}
- ctx.Data["OrgLabels"] = orgLabels
- labels = append(labels, orgLabels...)
+// retrieveRepoMetasForIssueWriter finds some the meta information of a repository for an issue/pr writer
+func retrieveRepoMetasForIssueWriter(ctx *context.Context, repo *repo_model.Repository, isPull bool) {
+ if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
+ return
}
RetrieveRepoMilestonesAndAssignees(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
retrieveProjects(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
PrepareBranchList(ctx)
if ctx.Written() {
- return nil
+ return
}
-
// Contains true if the user can create issue dependencies
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
-
- return labels
}
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, labelsData *issueSidebarLabelsData) (bool, map[string]error) {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return false, nil
@@ -951,26 +1012,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
ctx.Data["Fields"] = template.Fields
ctx.Data["TemplateFile"] = template.FileName
}
- labelIDs := make([]string, 0, len(template.Labels))
- if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
- ctx.Data["Labels"] = repoLabels
- if ctx.Repo.Owner.IsOrganization() {
- if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
- ctx.Data["OrgLabels"] = orgLabels
- repoLabels = append(repoLabels, orgLabels...)
- }
- }
- for _, metaLabel := range template.Labels {
- for _, repoLabel := range repoLabels {
- if strings.EqualFold(repoLabel.Name, metaLabel) {
- repoLabel.IsChecked = true
- labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
- break
- }
- }
- }
- }
+ labelsData.SetSelectedLabelNames(template.Labels)
+
selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
@@ -983,8 +1027,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/
template.Ref = git.BranchPrefix + template.Ref
}
- ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
- ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
+
ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
@@ -1042,8 +1085,14 @@ func NewIssue(ctx *context.Context) {
}
}
- RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
-
+ retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, false)
+ if ctx.Written() {
+ return
+ }
+ labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, false)
+ if ctx.Written() {
+ return
+ }
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
@@ -1052,7 +1101,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
+ templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, labelsData)
for k, v := range errs {
ret.TemplateErrors[k] = v
}
@@ -1161,34 +1210,25 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
err error
)
- labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
+ retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, isPull)
+ if ctx.Written() {
+ return ret
+ }
+ labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, isPull)
if ctx.Written() {
return ret
}
var labelIDs []int64
- hasSelected := false
// Check labels.
if len(form.LabelIDs) > 0 {
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
if err != nil {
return ret
}
- labelIDMark := make(container.Set[int64])
- labelIDMark.AddMultiple(labelIDs...)
-
- for i := range labels {
- if labelIDMark.Contains(labels[i].ID) {
- labels[i].IsChecked = true
- hasSelected = true
- }
- }
+ labelsData.SetSelectedLabelIDs(labelIDs)
}
- ctx.Data["Labels"] = labels
- ctx.Data["HasSelectedLabel"] = hasSelected
- ctx.Data["label_ids"] = form.LabelIDs
-
// Check milestone.
milestoneID := form.MilestoneID
if milestoneID > 0 {
@@ -1579,38 +1619,15 @@ func ViewIssue(ctx *context.Context) {
}
}
- // Metas.
- // Check labels.
- labelIDMark := make(container.Set[int64])
- for _, label := range issue.Labels {
- labelIDMark.Add(label.ID)
- }
- labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByRepoID", err)
+ retrieveRepoMetasForIssueWriter(ctx, repo, issue.IsPull)
+ if ctx.Written() {
return
}
- ctx.Data["Labels"] = labels
-
- if repo.Owner.IsOrganization() {
- orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByOrgID", err)
- return
- }
- ctx.Data["OrgLabels"] = orgLabels
-
- labels = append(labels, orgLabels...)
+ labelsData := retrieveRepoLabels(ctx, repo, issue.ID, issue.IsPull)
+ if ctx.Written() {
+ return
}
-
- hasSelected := false
- for i := range labels {
- if labelIDMark.Contains(labels[i].ID) {
- labels[i].IsChecked = true
- hasSelected = true
- }
- }
- ctx.Data["HasSelectedLabel"] = hasSelected
+ labelsData.SetSelectedLabels(issue.Labels)
// Check milestone and assignee.
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 81bee4dbb5..4874baaa54 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -53,11 +53,11 @@ func InitializeLabels(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
-// RetrieveLabels find all the labels of a repository and organization
-func RetrieveLabels(ctx *context.Context) {
+// RetrieveLabelsForList find all the labels of a repository and organization, it is only used by "/labels" page to list all labels
+func RetrieveLabelsForList(ctx *context.Context) {
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
- ctx.ServerError("RetrieveLabels.GetLabels", err)
+ ctx.ServerError("RetrieveLabelsForList.GetLabels", err)
return
}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 93fc72300b..c86a03da51 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, testCase.RepoID)
ctx.Req.Form.Set("sort", testCase.Sort)
- RetrieveLabels(ctx)
+ RetrieveLabelsForList(ctx)
assert.False(t, ctx.Written())
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
assert.True(t, ok)
diff --git a/routers/web/web.go b/routers/web/web.go
index 29dd8a8edc..907bf88f6f 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1163,7 +1163,7 @@ func registerRoutes(m *web.Router) {
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
m.Get("/pulls/posters", repo.PullPosters)
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
- m.Get("/labels", repo.RetrieveLabels, repo.Labels)
+ m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
m.Get("/milestones", repo.Milestones)
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
m.Group("/{type:issues|pulls}", func() {
diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl
deleted file mode 100644
index f40c792da7..0000000000
--- a/templates/repo/issue/labels/label.tmpl
+++ /dev/null
@@ -1,7 +0,0 @@
-
- {{- ctx.RenderUtils.RenderLabel .label -}}
-
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
deleted file mode 100644
index 96fb658664..0000000000
--- a/templates/repo/issue/labels/labels_selector_field.tmpl
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
- {{ctx.Locale.Tr "repo.issues.new.labels"}}
- {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
-
-
- {{if or .Labels .OrgLabels}}
-
- {{svg "octicon-search" 16}}
-
-
- {{end}}
- {{ctx.Locale.Tr "repo.issues.new.clear_labels"}}
- {{if or .Labels .OrgLabels}}
- {{$previousExclusiveScope := "_no_scope"}}
- {{range .Labels}}
- {{$exclusiveScope := .ExclusiveScope}}
- {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
-
- {{end}}
- {{$previousExclusiveScope = $exclusiveScope}}
- {{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}} {{ctx.RenderUtils.RenderLabel .}}
- {{if .Description}}{{ctx.Locale.Tr "repo.issues.new.no_items"}}
- {{end}}
-
-
diff --git a/templates/repo/issue/labels/labels_sidebar.tmpl b/templates/repo/issue/labels/labels_sidebar.tmpl
deleted file mode 100644
index 0b7b9b8969..0000000000
--- a/templates/repo/issue/labels/labels_sidebar.tmpl
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- {{ctx.Locale.Tr "repo.issues.new.no_label"}}
- {{range .root.Labels}}
- {{template "repo/issue/labels/label" dict "root" $.root "label" .}}
- {{end}}
- {{range .root.OrgLabels}}
- {{template "repo/issue/labels/label" dict "root" $.root "label" .}}
- {{end}}
-
-
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 190d52cf47..65d359e9dc 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -18,15 +18,15 @@
{{range .Fields}}
{{if eq .Type "input"}}
- {{template "repo/issue/fields/input" "item" .}}
+ {{template "repo/issue/fields/input" dict "item" .}}
{{else if eq .Type "markdown"}}
- {{template "repo/issue/fields/markdown" "item" .}}
+ {{template "repo/issue/fields/markdown" dict "item" .}}
{{else if eq .Type "textarea"}}
- {{template "repo/issue/fields/textarea" "item" . "root" $}}
+ {{template "repo/issue/fields/textarea" dict "item" . "root" $}}
{{else if eq .Type "dropdown"}}
- {{template "repo/issue/fields/dropdown" "item" .}}
+ {{template "repo/issue/fields/dropdown" dict "item" .}}
{{else if eq .Type "checkboxes"}}
- {{template "repo/issue/fields/checkboxes" "item" .}}
+ {{template "repo/issue/fields/checkboxes" dict "item" .}}
{{end}}
{{end}}
{{else}}
@@ -49,13 +49,11 @@
{{template "repo/issue/branch_selector_field" $}}
{{if .PageIsComparePull}}
- {{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
{{end}}
-
- {{template "repo/issue/labels/labels_selector_field" .}}
- {{template "repo/issue/labels/labels_sidebar" dict "root" $}}
+ {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl
new file mode 100644
index 0000000000..e9f4baa433
--- /dev/null
+++ b/templates/repo/issue/sidebar/label_list.tmpl
@@ -0,0 +1,51 @@
+{{$data := .}}
+{{$canChange := and ctx.RootData.HasIssuesOrPullsWritePermission (not $data.Repository.IsArchived)}}
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.new.labels"}} {{if $canChange}}{{svg "octicon-gear"}}{{end}}
+
+
+ {{if not $data.AllLabels}}
+ {{ctx.Locale.Tr "repo.issues.new.no_items"}}
+ {{else}}
+
+ {{svg "octicon-search" 16}}
+
+
+ {{ctx.Locale.Tr "repo.issues.new.clear_labels"}}
+ {{$previousExclusiveScope := "_no_scope"}}
+ {{range .RepoLabels}}
+ {{$exclusiveScope := .ExclusiveScope}}
+ {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
+
+ {{end}}
+ {{$previousExclusiveScope = $exclusiveScope}}
+ {{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
+ {{end}}
+
+ {{$previousExclusiveScope = "_no_scope"}}
+ {{range .OrgLabels}}
+ {{$exclusiveScope := .ExclusiveScope}}
+ {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
+
+ {{end}}
+ {{$previousExclusiveScope = $exclusiveScope}}
+ {{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
+ {{end}}
+ {{end}}
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.new.no_label"}}
+ {{range $data.AllLabels}}
+ {{if .IsChecked}}
+
+ {{- ctx.RenderUtils.RenderLabel . -}}
+
+ {{end}}
+ {{end}}
+
+
diff --git a/templates/repo/issue/sidebar/label_list_item.tmpl b/templates/repo/issue/sidebar/label_list_item.tmpl
new file mode 100644
index 0000000000..ad878e918b
--- /dev/null
+++ b/templates/repo/issue/sidebar/label_list_item.tmpl
@@ -0,0 +1,11 @@
+{{$label := .Label}}
+
+ {{svg (Iif $label.ExclusiveScope "octicon-dot-fill" "octicon-check")}}
+ {{ctx.RenderUtils.RenderLabel $label}}
+
+ {{if $label.Description}}{{$label.Description | ctx.RenderUtils.RenderEmoji}} {{end}}
+ {{template "repo/issue/labels/label_archived" $label}}
+
+
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl
index 2d3218e927..cf7b97c02b 100644
--- a/templates/repo/issue/sidebar/reviewer_list.tmpl
+++ b/templates/repo/issue/sidebar/reviewer_list.tmpl
@@ -1,11 +1,9 @@
-{{$data := .IssueSidebarReviewersData}}
+{{$data := .}}
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
-
+
{{/* match CreateIssueForm */}}
-
-
+
+
{{ctx.Locale.Tr "repo.issues.review.reviewers"}} {{if and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
@@ -19,7 +17,8 @@
{{if .User}}
- {{svg "octicon-check"}} {{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
+ {{svg "octicon-check"}}
+ {{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
{{end}}
{{end}}
@@ -29,7 +28,8 @@
{{if .Team}}
- {{svg "octicon-check"}} {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
+ {{svg "octicon-check"}}
+ {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
{{end}}
{{end}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 7a40274759..0fae1e9e1c 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -2,13 +2,12 @@
{{template "repo/issue/branch_selector_field" $}}
{{if .Issue.IsPull}}
- {{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
{{template "repo/issue/sidebar/wip_switch" $}}
{{end}}
- {{template "repo/issue/labels/labels_selector_field" $}}
- {{template "repo/issue/labels/labels_sidebar" dict "root" $}}
+ {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
{{template "repo/issue/sidebar/milestone_list" $}}
{{template "repo/issue/sidebar/project_list" $}}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 185a5f6f55..ff8342d29a 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -50,7 +50,7 @@
width: 300px;
}
-.issue-sidebar-combo .ui.dropdown .item:not(.checked) svg.octicon-check {
+.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark {
visibility: hidden;
}
/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */
@@ -62,6 +62,8 @@
.issue-content-right .dropdown > .menu {
max-width: 270px;
min-width: 0;
+ max-height: 500px;
+ overflow-x: auto;
}
@media (max-width: 767.98px) {
@@ -110,10 +112,6 @@
left: 0;
}
-.repository .select-label .desc {
- padding-left: 23px;
-}
-
/* For the secondary pointing menu, respect its own border-bottom */
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
diff --git a/web_src/css/repo/issue-label.css b/web_src/css/repo/issue-label.css
index 9b4b144a00..0a25d31da9 100644
--- a/web_src/css/repo/issue-label.css
+++ b/web_src/css/repo/issue-label.css
@@ -47,6 +47,7 @@
}
.archived-label-hint {
- float: right;
- margin: -12px;
+ position: absolute;
+ top: 10px;
+ right: 5px;
}
diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts
index beec92d152..56c5915b6d 100644
--- a/web_src/js/features/common-page.ts
+++ b/web_src/js/features/common-page.ts
@@ -32,13 +32,13 @@ export function initGlobalDropdown() {
const $uiDropdowns = fomanticQuery('.ui.dropdown');
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
- $uiDropdowns.filter(':not(.custom)').dropdown();
+ $uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
// The "jump" means this dropdown is mainly used for "menu" purpose,
// clicking an item will jump to somewhere else or trigger an action/function.
// When a dropdown is used for non-refresh actions with tippy,
// it must have this "jump" class to hide the tippy when dropdown is closed.
- $uiDropdowns.filter('.jump').dropdown({
+ $uiDropdowns.filter('.jump').dropdown('setting', {
action: 'hide',
onShow() {
// hide associated tooltip while dropdown is open
diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts
index d541615988..f408eb43ba 100644
--- a/web_src/js/features/repo-issue-sidebar-combolist.ts
+++ b/web_src/js/features/repo-issue-sidebar-combolist.ts
@@ -1,6 +1,6 @@
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {POST} from '../modules/fetch.ts';
-import {queryElemChildren, toggleElem} from '../utils/dom.ts';
+import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
// if there are draft comments, confirm before reloading, to avoid losing comments
export function issueSidebarReloadConfirmDraftComment() {
@@ -27,20 +27,37 @@ function collectCheckedValues(elDropdown: HTMLElement) {
}
export function initIssueSidebarComboList(container: HTMLElement) {
- if (!container) return;
-
const updateUrl = container.getAttribute('data-update-url');
const elDropdown = container.querySelector(':scope > .ui.dropdown');
const elList = container.querySelector(':scope > .ui.list');
const elComboValue = container.querySelector(':scope > .combo-value');
- const initialValues = collectCheckedValues(elDropdown);
+ let initialValues = collectCheckedValues(elDropdown);
elDropdown.addEventListener('click', (e) => {
const elItem = (e.target as HTMLElement).closest('.item');
if (!elItem) return;
e.preventDefault();
- if (elItem.getAttribute('data-can-change') !== 'true') return;
- elItem.classList.toggle('checked');
+ if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
+
+ if (elItem.matches('.clear-selection')) {
+ queryElems(elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
+ elComboValue.value = '';
+ return;
+ }
+
+ const scope = elItem.getAttribute('data-scope');
+ if (scope) {
+ // scoped items could only be checked one at a time
+ const elSelected = elDropdown.querySelector(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
+ if (elSelected === elItem) {
+ elItem.classList.toggle('checked');
+ } else {
+ queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
+ elItem.classList.toggle('checked', true);
+ }
+ } else {
+ elItem.classList.toggle('checked');
+ }
elComboValue.value = collectCheckedValues(elDropdown).join(',');
});
@@ -61,29 +78,28 @@ export function initIssueSidebarComboList(container: HTMLElement) {
if (changed) issueSidebarReloadConfirmDraftComment();
};
- const syncList = (changedValues) => {
+ const syncUiList = (changedValues) => {
const elEmptyTip = elList.querySelector('.item.empty-list');
queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
for (const value of changedValues) {
- const el = elDropdown.querySelector(`.menu > .item[data-value="${value}"]`);
+ const el = elDropdown.querySelector(`.menu > .item[data-value="${CSS.escape(value)}"]`);
const listItem = el.cloneNode(true) as HTMLElement;
- listItem.querySelector('svg.octicon-check')?.remove();
+ queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
elList.append(listItem);
}
const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
toggleElem(elEmptyTip, !hasItems);
};
- fomanticQuery(elDropdown).dropdown({
+ fomanticQuery(elDropdown).dropdown('setting', {
action: 'nothing', // do not hide the menu if user presses Enter
fullTextSearch: 'exact',
async onHide() {
+ // TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
const changedValues = collectCheckedValues(elDropdown);
- if (updateUrl) {
- await updateToBackend(changedValues); // send requests to backend and reload the page
- } else {
- syncList(changedValues); // only update the list in the sidebar
- }
+ syncUiList(changedValues);
+ if (updateUrl) await updateToBackend(changedValues);
+ initialValues = changedValues;
},
});
}
diff --git a/web_src/js/features/repo-issue-sidebar.md b/web_src/js/features/repo-issue-sidebar.md
new file mode 100644
index 0000000000..3022b52d05
--- /dev/null
+++ b/web_src/js/features/repo-issue-sidebar.md
@@ -0,0 +1,27 @@
+A sidebar combo (dropdown+list) is like this:
+
+```html
+
+
+
+
+ clear
+
+ ...
+ ...
+
+
+
+
+ no item
+ ...
+
+
+```
+
+When the selected items change, the `combo-value` input will be updated.
+If there is `data-update-url`, it also calls backend to attach/detach the changed items.
+
+Also, the changed items will be syncronized to the `ui list` items.
+
+The items with the same data-scope only allow one selected at a time.
diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts
index 4a1ef02aab..52878848e8 100644
--- a/web_src/js/features/repo-issue-sidebar.ts
+++ b/web_src/js/features/repo-issue-sidebar.ts
@@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
import {updateIssuesMeta} from './repo-common.ts';
import {svg} from '../svg.ts';
import {htmlEscape} from 'escape-goat';
-import {toggleElem} from '../utils/dom.ts';
+import {queryElems, toggleElem} from '../utils/dom.ts';
import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
function initBranchSelector() {
@@ -28,7 +28,7 @@ function initBranchSelector() {
} else {
// for new issue, only update UI&form, do not send request/reload
const selectedHiddenSelector = this.getAttribute('data-id-selector');
- document.querySelector(selectedHiddenSelector).value = selectedValue;
+ document.querySelector(selectedHiddenSelector).value = selectedValue;
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
}
});
@@ -53,7 +53,7 @@ function initListSubmits(selector, outerSelector) {
for (const [elementId, item] of itemEntries) {
await updateIssuesMeta(
item['update-url'],
- item.action,
+ item['action'],
item['issue-id'],
elementId,
);
@@ -80,14 +80,14 @@ function initListSubmits(selector, outerSelector) {
if (scope) {
// Enable only clicked item for scoped labels
if (this.getAttribute('data-scope') !== scope) {
- return true;
+ return;
}
if (this !== clickedItem && !this.classList.contains('checked')) {
- return true;
+ return;
}
} else if (this !== clickedItem) {
// Toggle for other labels
- return true;
+ return;
}
if (this.classList.contains('checked')) {
@@ -258,13 +258,13 @@ export function initRepoIssueSidebar() {
initRepoIssueDue();
// TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
- initListSubmits('select-label', 'labels');
initListSubmits('select-assignees', 'assignees');
initListSubmits('select-assignees-modify', 'assignees');
- selectItem('.select-project', '#project_id');
- selectItem('.select-milestone', '#milestone_id');
selectItem('.select-assignee', '#assignee_id');
- // init the combo list: a dropdown for selecting reviewers, and a list for showing selected reviewers and related actions
- initIssueSidebarComboList(document.querySelector('.issue-sidebar-combo[data-sidebar-combo-for="reviewers"]'));
+ selectItem('.select-project', '#project_id');
+ selectItem('.select-milestone', '#milestone_id');
+
+ // init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
+ queryElems(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
}
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index 92916ec8d7..7457531ece 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -98,6 +98,7 @@ export function initRepoIssueSidebarList() {
});
});
+ // FIXME: it is wrong place to init ".ui.dropdown.label-filter"
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
if (e.altKey && e.key === 'Enter') {
const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
@@ -106,7 +107,6 @@ export function initRepoIssueSidebarList() {
}
}
});
- $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
}
export function initRepoIssueCommentDelete() {
@@ -652,19 +652,6 @@ function initIssueTemplateCommentEditors($commentForm) {
}
}
-// This function used to show and hide archived label on issue/pr
-// page in the sidebar where we select the labels
-// If we have any archived label tagged to issue and pr. We will show that
-// archived label with checked classed otherwise we will hide it
-// with the help of this function.
-// This function runs globally.
-export function initArchivedLabelHandler() {
- if (!document.querySelector('.archived-label-hint')) return;
- for (const label of document.querySelectorAll('[data-is-archived]')) {
- toggleElem(label, label.classList.contains('checked'));
- }
-}
-
export function initRepoCommentFormAndSidebar() {
const $commentForm = $('.comment.form');
if (!$commentForm.length) return;
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 487aac97aa..eeead37333 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -30,7 +30,7 @@ import {
initRepoIssueWipTitle,
initRepoPullRequestMergeInstruction,
initRepoPullRequestAllowMaintainerEdit,
- initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
+ initRepoPullRequestReview, initRepoIssueSidebarList,
} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
@@ -182,7 +182,6 @@ onDomReady(() => {
initRepoIssueContentHistory,
initRepoIssueList,
initRepoIssueSidebarList,
- initArchivedLabelHandler,
initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking,
initRepoIssueWipTitle,
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 79ce05a7ad..29b34dd1e3 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest';
import type $ from 'jquery';
type ElementArg = Element | string | NodeListOf | Array | ReturnType;
-type ElementsCallback = (el: Element) => Promisable;
+type ElementsCallback = (el: T) => Promisable;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable;
type ArrayLikeIterable = ArrayLike & Iterable; // for NodeListOf and Array
@@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
return res[0];
}
-function applyElemsCallback(elems: ArrayLikeIterable, fn?: ElementsCallback): ArrayLikeIterable {
+function applyElemsCallback(elems: ArrayLikeIterable, fn?: ElementsCallback): ArrayLikeIterable {
if (fn) {
for (const el of elems) {
fn(el);
@@ -67,7 +67,7 @@ function applyElemsCallback(elems: ArrayLikeIterable, fn?:
return elems;
}
-export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable {
+export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable {
const elems = Array.from(el.parentNode.children) as T[];
return applyElemsCallback(elems.filter((child: Element) => {
return child !== el && child.matches(selector);
@@ -75,13 +75,13 @@ export function queryElemSiblings(el: Element, selector = '*'
}
// it works like jQuery.children: only the direct children are selected
-export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable {
+export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable {
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
}
// it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
-export function queryElems(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable {
+export function queryElems(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable {
return applyElemsCallback(parent.querySelectorAll(selector), fn);
}
From a928739456b78072136a1a264a68758571238aac Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Mon, 11 Nov 2024 04:07:54 +0800
Subject: [PATCH 013/100] Refactor sidebar assignee&milestone&project selectors
(#32465)
Follow #32460
Now the code could be much clearer than before and easier to maintain. A
lot of legacy code is removed.
Manually tested.
This PR is large enough, that fine tunes could be deferred to the future if
there is no bug found or design problem.
Screenshots:

---
modules/base/tool.go | 3 +
modules/base/tool_test.go | 1 +
modules/container/set.go | 4 +-
modules/container/set_test.go | 2 +
modules/templates/helper.go | 1 +
routers/web/repo/compare.go | 12 +-
routers/web/repo/issue.go | 474 +++++++++---------
routers/web/repo/pull.go | 2 +-
services/forms/repo_form.go | 1 -
.../repo/issue/milestone/select_menu.tmpl | 38 --
templates/repo/issue/new_form.tmpl | 138 +----
.../repo/issue/sidebar/assignee_list.tmpl | 69 ++-
templates/repo/issue/sidebar/label_list.tmpl | 18 +-
.../repo/issue/sidebar/label_list_item.tmpl | 2 +-
.../repo/issue/sidebar/milestone_list.tmpl | 64 ++-
.../repo/issue/sidebar/participant_list.tmpl | 2 +-
.../repo/issue/sidebar/project_list.tmpl | 68 ++-
.../repo/issue/sidebar/reviewer_list.tmpl | 24 +-
.../repo/issue/view_content/sidebar.tmpl | 13 +-
web_src/css/repo.css | 6 -
.../features/repo-issue-sidebar-combolist.ts | 164 ++++--
web_src/js/features/repo-issue-sidebar.md | 6 +-
web_src/js/features/repo-issue-sidebar.ts | 219 +-------
23 files changed, 503 insertions(+), 828 deletions(-)
delete mode 100644 templates/repo/issue/milestone/select_menu.tmpl
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 9e43030f40..928c80700b 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -147,6 +147,9 @@ func StringsToInt64s(strs []string) ([]int64, error) {
}
ints := make([]int64, 0, len(strs))
for _, s := range strs {
+ if s == "" {
+ continue
+ }
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, err
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 4af8b9bc4d..86cccdf209 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -152,6 +152,7 @@ func TestStringsToInt64s(t *testing.T) {
}
testSuccess(nil, nil)
testSuccess([]string{}, []int64{})
+ testSuccess([]string{""}, []int64{})
testSuccess([]string{"-1234"}, []int64{-1234})
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
diff --git a/modules/container/set.go b/modules/container/set.go
index adb77dcac7..105533f203 100644
--- a/modules/container/set.go
+++ b/modules/container/set.go
@@ -31,8 +31,8 @@ func (s Set[T]) AddMultiple(values ...T) {
}
}
-// Contains determines whether a set contains the specified elements.
-// Returns true if the set contains the specified element; otherwise, false.
+// Contains determines whether a set contains all these elements.
+// Returns true if the set contains all these elements; otherwise, false.
func (s Set[T]) Contains(values ...T) bool {
ret := true
for _, value := range values {
diff --git a/modules/container/set_test.go b/modules/container/set_test.go
index 1502236034..a8b7ff8190 100644
--- a/modules/container/set_test.go
+++ b/modules/container/set_test.go
@@ -18,7 +18,9 @@ func TestSet(t *testing.T) {
assert.True(t, s.Contains("key1"))
assert.True(t, s.Contains("key2"))
+ assert.True(t, s.Contains("key1", "key2"))
assert.False(t, s.Contains("key3"))
+ assert.False(t, s.Contains("key1", "key3"))
assert.True(t, s.Remove("key2"))
assert.False(t, s.Contains("key2"))
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index efaa10624b..3ef11772dc 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -31,6 +31,7 @@ func NewFuncMap() template.FuncMap {
"ctx": func() any { return nil }, // template context function
"DumpVar": dumpVar,
+ "NIL": func() any { return nil },
// -----------------------------------------------------------------
// html/template related functions
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 9a7d3dfbf6..a5fdba3fde 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -788,19 +788,11 @@ func CompareDiff(ctx *context.Context) {
if !nothingToCompare {
// Setup information for new form.
- retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, true)
+ pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, true)
if ctx.Written() {
return
}
- labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, true)
- if ctx.Written() {
- return
- }
- RetrieveRepoReviewers(ctx, ctx.Repo.Repository, nil, true)
- if ctx.Written() {
- return
- }
- _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, labelsData)
+ _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a4e2fd8cea..72f89bd27d 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -431,7 +431,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return 0
}
- retrieveProjects(ctx, repo)
+ retrieveProjectsForIssueList(ctx, repo)
if ctx.Written() {
return
}
@@ -556,37 +556,147 @@ func renderMilestones(ctx *context.Context) {
ctx.Data["ClosedMilestones"] = closedMilestones
}
-// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
-func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
+type issueSidebarMilestoneData struct {
+ SelectedMilestoneID int64
+ OpenMilestones []*issues_model.Milestone
+ ClosedMilestones []*issues_model.Milestone
+}
+
+type issueSidebarAssigneesData struct {
+ SelectedAssigneeIDs string
+ CandidateAssignees []*user_model.User
+}
+
+type IssuePageMetaData struct {
+ RepoLink string
+ Repository *repo_model.Repository
+ Issue *issues_model.Issue
+ IsPullRequest bool
+ CanModifyIssueOrPull bool
+
+ ReviewersData *issueSidebarReviewersData
+ LabelsData *issueSidebarLabelsData
+ MilestonesData *issueSidebarMilestoneData
+ ProjectsData *issueSidebarProjectsData
+ AssigneesData *issueSidebarAssigneesData
+}
+
+func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, isPull bool) *IssuePageMetaData {
+ data := &IssuePageMetaData{
+ RepoLink: ctx.Repo.RepoLink,
+ Repository: repo,
+ Issue: issue,
+ IsPullRequest: isPull,
+
+ ReviewersData: &issueSidebarReviewersData{},
+ LabelsData: &issueSidebarLabelsData{},
+ MilestonesData: &issueSidebarMilestoneData{},
+ ProjectsData: &issueSidebarProjectsData{},
+ AssigneesData: &issueSidebarAssigneesData{},
+ }
+ ctx.Data["IssuePageMetaData"] = data
+
+ if isPull {
+ data.retrieveReviewersData(ctx)
+ if ctx.Written() {
+ return data
+ }
+ }
+ data.retrieveLabelsData(ctx)
+ if ctx.Written() {
+ return data
+ }
+
+ data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
+ if !data.CanModifyIssueOrPull {
+ return data
+ }
+
+ data.retrieveAssigneesDataForIssueWriter(ctx)
+ if ctx.Written() {
+ return data
+ }
+
+ data.retrieveMilestonesDataForIssueWriter(ctx)
+ if ctx.Written() {
+ return data
+ }
+
+ data.retrieveProjectsDataForIssueWriter(ctx)
+ if ctx.Written() {
+ return data
+ }
+
+ PrepareBranchList(ctx)
+ if ctx.Written() {
+ return data
+ }
+
+ ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
+ return data
+}
+
+func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Context) {
var err error
- ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
- RepoID: repo.ID,
+ if d.Issue != nil {
+ d.MilestonesData.SelectedMilestoneID = d.Issue.MilestoneID
+ }
+ d.MilestonesData.OpenMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+ RepoID: d.Repository.ID,
IsClosed: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
return
}
- ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
- RepoID: repo.ID,
+ d.MilestonesData.ClosedMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+ RepoID: d.Repository.ID,
IsClosed: optional.Some(true),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
return
}
+}
- assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
+func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) {
+ var err error
+ d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
if err != nil {
ctx.ServerError("GetRepoAssignees", err)
return
}
- ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
-
+ d.AssigneesData.CandidateAssignees = shared_user.MakeSelfOnTop(ctx.Doer, d.AssigneesData.CandidateAssignees)
+ if d.Issue != nil {
+ _ = d.Issue.LoadAssignees(ctx)
+ ids := make([]string, 0, len(d.Issue.Assignees))
+ for _, a := range d.Issue.Assignees {
+ ids = append(ids, strconv.FormatInt(a.ID, 10))
+ }
+ d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
+ }
+ // FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
handleTeamMentions(ctx)
}
-func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
+func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) {
+ ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
+}
+
+type issueSidebarProjectsData struct {
+ SelectedProjectID int64
+ OpenProjects []*project_model.Project
+ ClosedProjects []*project_model.Project
+}
+
+func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
+ if d.Issue != nil && d.Issue.Project != nil {
+ d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
+ }
+ d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
+}
+
+func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) {
// Distinguish whether the owner of the repository
// is an individual or an organization
repoOwnerType := project_model.TypeIndividual
@@ -609,7 +719,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
- return
+ return nil, nil
}
closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptionsAll,
@@ -619,7 +729,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
- return
+ return nil, nil
}
}
@@ -632,7 +742,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
- return
+ return nil, nil
}
openProjects = append(openProjects, openProjects2...)
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
@@ -643,13 +753,11 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
})
if err != nil {
ctx.ServerError("GetProjects", err)
- return
+ return nil, nil
}
closedProjects = append(closedProjects, closedProjects2...)
}
-
- ctx.Data["OpenProjects"] = openProjects
- ctx.Data["ClosedProjects"] = closedProjects
+ return openProjects, closedProjects
}
// repoReviewerSelection items to bee shown
@@ -665,10 +773,6 @@ type repoReviewerSelection struct {
}
type issueSidebarReviewersData struct {
- Repository *repo_model.Repository
- RepoOwnerName string
- RepoLink string
- IssueID int64
CanChooseReviewer bool
OriginalReviews issues_model.ReviewList
TeamReviewers []*repoReviewerSelection
@@ -677,41 +781,44 @@ type issueSidebarReviewersData struct {
}
// RetrieveRepoReviewers find all reviewers of a repository. If issue is nil, it means the doer is creating a new PR.
-func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
- data := &issueSidebarReviewersData{}
- data.RepoLink = ctx.Repo.RepoLink
- data.Repository = repo
- data.RepoOwnerName = repo.OwnerName
- data.CanChooseReviewer = canChooseReviewer
+func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
+ data := d.ReviewersData
+ repo := d.Repository
+ if ctx.Doer != nil && ctx.IsSigned {
+ if d.Issue == nil {
+ data.CanChooseReviewer = true
+ } else {
+ data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue)
+ }
+ }
var posterID int64
var isClosed bool
var reviews issues_model.ReviewList
- if issue == nil {
+ if d.Issue == nil {
posterID = ctx.Doer.ID
} else {
- posterID = issue.PosterID
- if issue.OriginalAuthorID > 0 {
+ posterID = d.Issue.PosterID
+ if d.Issue.OriginalAuthorID > 0 {
posterID = 0 // for migrated PRs, no poster ID
}
- data.IssueID = issue.ID
- isClosed = issue.IsClosed || issue.PullRequest.HasMerged
+ isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged
- originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID)
+ originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID)
if err != nil {
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
return
}
data.OriginalReviews = originalAuthorReviews
- reviews, err = issues_model.GetReviewsByIssueID(ctx, issue.ID)
+ reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
if err != nil {
ctx.ServerError("GetReviewersByIssueID", err)
return
}
- if len(reviews) == 0 && !canChooseReviewer {
+ if len(reviews) == 0 && !data.CanChooseReviewer {
return
}
}
@@ -724,7 +831,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
reviewers []*user_model.User
)
- if canChooseReviewer {
+ if data.CanChooseReviewer {
var err error
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
if err != nil {
@@ -760,7 +867,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
tmp.ItemID = -review.ReviewerTeamID
}
- if canChooseReviewer {
+ if data.CanChooseReviewer {
// Users who can choose reviewers can also remove review requests
tmp.CanChange = true
} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
@@ -770,7 +877,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
pullReviews = append(pullReviews, tmp)
- if canChooseReviewer {
+ if data.CanChooseReviewer {
if tmp.IsTeam {
teamReviewersResult = append(teamReviewersResult, tmp)
} else {
@@ -811,7 +918,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
data.CurrentPullReviewers = currentPullReviewers
}
- if canChooseReviewer && reviewersResult != nil {
+ if data.CanChooseReviewer && reviewersResult != nil {
preadded := len(reviewersResult)
for _, reviewer := range reviewers {
found := false
@@ -839,7 +946,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
data.Reviewers = reviewersResult
}
- if canChooseReviewer && teamReviewersResult != nil {
+ if data.CanChooseReviewer && teamReviewersResult != nil {
preadded := len(teamReviewersResult)
for _, team := range teamReviewers {
found := false
@@ -866,15 +973,9 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
data.TeamReviewers = teamReviewersResult
}
-
- ctx.Data["IssueSidebarReviewersData"] = data
}
type issueSidebarLabelsData struct {
- Repository *repo_model.Repository
- RepoLink string
- IssueID int64
- IsPullRequest bool
AllLabels []*issues_model.Label
RepoLabels []*issues_model.Label
OrgLabels []*issues_model.Label
@@ -922,60 +1023,30 @@ func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
)
}
-func retrieveRepoLabels(ctx *context.Context, repo *repo_model.Repository, issueID int64, isPull bool) *issueSidebarLabelsData {
- labelsData := &issueSidebarLabelsData{
- Repository: repo,
- RepoLink: ctx.Repo.RepoLink,
- IssueID: issueID,
- IsPullRequest: isPull,
- }
- ctx.Data["IssueSidebarLabelsData"] = labelsData
+func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
+ repo := d.Repository
+ labelsData := d.LabelsData
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
- return nil
+ return
}
labelsData.RepoLabels = labels
if repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
- return nil
+ return
}
labelsData.OrgLabels = orgLabels
}
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
- return labelsData
-}
-
-// retrieveRepoMetasForIssueWriter finds some the meta information of a repository for an issue/pr writer
-func retrieveRepoMetasForIssueWriter(ctx *context.Context, repo *repo_model.Repository, isPull bool) {
- if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
- return
- }
-
- RetrieveRepoMilestonesAndAssignees(ctx, repo)
- if ctx.Written() {
- return
- }
-
- retrieveProjects(ctx, repo)
- if ctx.Written() {
- return
- }
-
- PrepareBranchList(ctx)
- if ctx.Written() {
- return
- }
- // Contains true if the user can create issue dependencies
- ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
}
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, labelsData *issueSidebarLabelsData) (bool, map[string]error) {
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error) {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return false, nil
@@ -1013,24 +1084,20 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
ctx.Data["TemplateFile"] = template.FileName
}
- labelsData.SetSelectedLabelNames(template.Labels)
+ metaData.LabelsData.SetSelectedLabelNames(template.Labels)
- selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
- if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
+ if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, true); err == nil {
for _, userID := range userIDs {
- selectedAssigneeIDs = append(selectedAssigneeIDs, userID)
selectedAssigneeIDStrings = append(selectedAssigneeIDStrings, strconv.FormatInt(userID, 10))
}
}
+ metaData.AssigneesData.SelectedAssigneeIDs = strings.Join(selectedAssigneeIDStrings, ",")
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/
template.Ref = git.BranchPrefix + template.Ref
}
- ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
- ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
- ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
ctx.Data["Reference"] = template.Ref
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
return true, templateErrs
@@ -1057,42 +1124,19 @@ func NewIssue(ctx *context.Context) {
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
- milestoneID := ctx.FormInt64("milestone")
- if milestoneID > 0 {
- milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
- if err != nil {
- log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
- } else {
- ctx.Data["milestone_id"] = milestoneID
- ctx.Data["Milestone"] = milestone
- }
+ pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, false)
+ if ctx.Written() {
+ return
}
- projectID := ctx.FormInt64("project")
- if projectID > 0 && isProjectsEnabled {
- project, err := project_model.GetProjectByID(ctx, projectID)
- if err != nil {
- log.Error("GetProjectByID: %d: %v", projectID, err)
- } else if project.RepoID != ctx.Repo.Repository.ID {
- log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
- } else {
- ctx.Data["project_id"] = projectID
- ctx.Data["Project"] = project
- }
-
+ pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
+ pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project")
+ if pageMetaData.ProjectsData.SelectedProjectID > 0 {
if len(ctx.Req.URL.Query().Get("project")) > 0 {
ctx.Data["redirect_after_creation"] = "project"
}
}
- retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, false)
- if ctx.Written() {
- return
- }
- labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, false)
- if ctx.Written() {
- return
- }
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
@@ -1101,7 +1145,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, labelsData)
+ templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
for k, v := range errs {
ret.TemplateErrors[k] = v
}
@@ -1196,8 +1240,16 @@ func DeleteIssue(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
}
-// ValidateRepoMetas check and returns repository's meta information
-func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
+func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(ItemType) KeyType) container.Set[KeyType] {
+ s := make(container.Set[KeyType])
+ for _, item := range slice {
+ s.Add(keyFunc(item))
+ }
+ return s
+}
+
+// ValidateRepoMetasForNewIssue check and returns repository's meta information
+func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
LabelIDs, AssigneeIDs []int64
MilestoneID, ProjectID int64
@@ -1205,126 +1257,76 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
TeamReviewers []*organization.Team
},
) {
- var (
- repo = ctx.Repo.Repository
- err error
- )
-
- retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, isPull)
- if ctx.Written() {
- return ret
- }
- labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, isPull)
+ pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, isPull)
if ctx.Written() {
return ret
}
- var labelIDs []int64
- // Check labels.
- if len(form.LabelIDs) > 0 {
- labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
- if err != nil {
- return ret
- }
- labelsData.SetSelectedLabelIDs(labelIDs)
+ inputLabelIDs, _ := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
+ candidateLabels := toSet(pageMetaData.LabelsData.AllLabels, func(label *issues_model.Label) int64 { return label.ID })
+ if len(inputLabelIDs) > 0 && !candidateLabels.Contains(inputLabelIDs...) {
+ ctx.NotFound("", nil)
+ return ret
}
+ pageMetaData.LabelsData.SetSelectedLabelIDs(inputLabelIDs)
- // Check milestone.
- milestoneID := form.MilestoneID
- if milestoneID > 0 {
- milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
- if err != nil {
- ctx.ServerError("GetMilestoneByID", err)
- return ret
- }
- if milestone.RepoID != repo.ID {
- ctx.ServerError("GetMilestoneByID", err)
- return ret
- }
- ctx.Data["Milestone"] = milestone
- ctx.Data["milestone_id"] = milestoneID
+ allMilestones := append(slices.Clone(pageMetaData.MilestonesData.OpenMilestones), pageMetaData.MilestonesData.ClosedMilestones...)
+ candidateMilestones := toSet(allMilestones, func(milestone *issues_model.Milestone) int64 { return milestone.ID })
+ if form.MilestoneID > 0 && !candidateMilestones.Contains(form.MilestoneID) {
+ ctx.NotFound("", nil)
+ return ret
}
+ pageMetaData.MilestonesData.SelectedMilestoneID = form.MilestoneID
- if form.ProjectID > 0 {
- p, err := project_model.GetProjectByID(ctx, form.ProjectID)
- if err != nil {
- ctx.ServerError("GetProjectByID", err)
- return ret
- }
- if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
- ctx.NotFound("", nil)
- return ret
- }
-
- ctx.Data["Project"] = p
- ctx.Data["project_id"] = form.ProjectID
+ allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
+ candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
+ if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) {
+ ctx.NotFound("", nil)
+ return ret
}
+ pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
- // Check assignees
- var assigneeIDs []int64
- if len(form.AssigneeIDs) > 0 {
- assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
- if err != nil {
- return ret
- }
-
- // Check if the passed assignees actually exists and is assignable
- for _, aID := range assigneeIDs {
- assignee, err := user_model.GetUserByID(ctx, aID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return ret
- }
-
- valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
- if err != nil {
- ctx.ServerError("CanBeAssigned", err)
- return ret
- }
-
- if !valid {
- ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
- return ret
- }
- }
+ candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
+ inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
+ if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) {
+ ctx.NotFound("", nil)
+ return ret
}
+ pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs
- // Keep the old assignee id thingy for compatibility reasons
- if form.AssigneeID > 0 {
- assigneeIDs = append(assigneeIDs, form.AssigneeID)
- }
-
- // Check reviewers
+ // Check if the passed reviewers (user/team) actually exist
var reviewers []*user_model.User
var teamReviewers []*organization.Team
- if isPull && len(form.ReviewerIDs) > 0 {
- reviewerIDs, err := base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
- if err != nil {
- return ret
+ reviewerIDs, _ := base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
+ if isPull && len(reviewerIDs) > 0 {
+ userReviewersMap := map[int64]*user_model.User{}
+ teamReviewersMap := map[int64]*organization.Team{}
+ for _, r := range pageMetaData.ReviewersData.Reviewers {
+ userReviewersMap[r.User.ID] = r.User
+ }
+ for _, r := range pageMetaData.ReviewersData.TeamReviewers {
+ teamReviewersMap[r.Team.ID] = r.Team
}
- // Check if the passed reviewers (user/team) actually exist
for _, rID := range reviewerIDs {
- // negative reviewIDs represent team requests
- if rID < 0 {
- teamReviewer, err := organization.GetTeamByID(ctx, -rID)
- if err != nil {
- ctx.ServerError("GetTeamByID", err)
+ if rID < 0 { // negative reviewIDs represent team requests
+ team, ok := teamReviewersMap[-rID]
+ if !ok {
+ ctx.NotFound("", nil)
return ret
}
- teamReviewers = append(teamReviewers, teamReviewer)
- continue
+ teamReviewers = append(teamReviewers, team)
+ } else {
+ user, ok := userReviewersMap[rID]
+ if !ok {
+ ctx.NotFound("", nil)
+ return ret
+ }
+ reviewers = append(reviewers, user)
}
-
- reviewer, err := user_model.GetUserByID(ctx, rID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return ret
- }
- reviewers = append(reviewers, reviewer)
}
}
- ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = labelIDs, assigneeIDs, milestoneID, form.ProjectID
+ ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, form.ProjectID
ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers
return ret
}
@@ -1344,7 +1346,7 @@ func NewIssuePost(ctx *context.Context) {
attachments []string
)
- validateRet := ValidateRepoMetas(ctx, *form, false)
+ validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false)
if ctx.Written() {
return
}
@@ -1619,37 +1621,11 @@ func ViewIssue(ctx *context.Context) {
}
}
- retrieveRepoMetasForIssueWriter(ctx, repo, issue.IsPull)
+ pageMetaData := retrieveRepoIssueMetaData(ctx, repo, issue, issue.IsPull)
if ctx.Written() {
return
}
- labelsData := retrieveRepoLabels(ctx, repo, issue.ID, issue.IsPull)
- if ctx.Written() {
- return
- }
- labelsData.SetSelectedLabels(issue.Labels)
-
- // Check milestone and assignee.
- if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
- RetrieveRepoMilestonesAndAssignees(ctx, repo)
- retrieveProjects(ctx, repo)
-
- if ctx.Written() {
- return
- }
- }
-
- if issue.IsPull {
- canChooseReviewer := false
- if ctx.Doer != nil && ctx.IsSigned {
- canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
- }
-
- RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
- if ctx.Written() {
- return
- }
- }
+ pageMetaData.LabelsData.SetSelectedLabels(issue.Labels)
if ctx.IsSigned {
// Update issue-user.
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index dd9671efbe..bb814eab6e 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1269,7 +1269,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
- validateRet := ValidateRepoMetas(ctx, *form, true)
+ validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
if ctx.Written() {
return
}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 83f2dd6caa..d27bbca894 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -451,7 +451,6 @@ type CreateIssueForm struct {
Ref string `form:"ref"`
MilestoneID int64
ProjectID int64
- AssigneeID int64
Content string
Files []string
AllowMaintainerEdit bool
diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl
deleted file mode 100644
index 9b0492ce52..0000000000
--- a/templates/repo/issue/milestone/select_menu.tmpl
+++ /dev/null
@@ -1,38 +0,0 @@
-{{if or .OpenMilestones .ClosedMilestones}}
-
- {{svg "octicon-search" 16}}
-
-
-
-{{end}}
-{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}
-{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
-
- {{ctx.Locale.Tr "repo.issues.new.no_items"}}
-
-{{else}}
- {{if .OpenMilestones}}
-
-
- {{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
-
- {{range .OpenMilestones}}
-
- {{svg "octicon-milestone" 16 "tw-mr-1"}}
- {{.Name}}
-
- {{end}}
- {{end}}
- {{if .ClosedMilestones}}
-
-
- {{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}
-
- {{range .ClosedMilestones}}
-
- {{svg "octicon-milestone" 16 "tw-mr-1"}}
- {{.Name}}
-
- {{end}}
- {{end}}
-{{end}}
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 65d359e9dc..ceaaebc4d5 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -49,142 +49,22 @@
{{template "repo/issue/branch_selector_field" $}}
{{if .PageIsComparePull}}
- {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
{{end}}
- {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
-
-
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.milestone"}}
- {{if .HasIssuesOrPullsWritePermission}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
-
-
- {{template "repo/issue/milestone/select_menu" .}}
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
-
- {{if .Milestone}}
-
- {{svg "octicon-milestone" 18 "tw-mr-2"}}
- {{.Milestone.Name}}
-
- {{end}}
-
-
-
+ {{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
+ {{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
{{if .IsProjectsEnabled}}
-
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.projects"}}
- {{if .HasIssuesOrPullsWritePermission}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
-
-
- {{if or .OpenProjects .ClosedProjects}}
-
- {{svg "octicon-search" 16}}
-
-
- {{end}}
- {{ctx.Locale.Tr "repo.issues.new.clear_projects"}}
- {{if and (not .OpenProjects) (not .ClosedProjects)}}
-
- {{ctx.Locale.Tr "repo.issues.new.no_items"}}
-
- {{else}}
- {{if .OpenProjects}}
-
-
- {{ctx.Locale.Tr "repo.issues.new.open_projects"}}
-
- {{range .OpenProjects}}
-
- {{svg .IconName 18 "tw-mr-2"}}{{.Title}}
-
- {{end}}
- {{end}}
- {{if .ClosedProjects}}
-
-
- {{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
-
- {{range .ClosedProjects}}
-
- {{svg .IconName 18 "tw-mr-2"}}{{.Title}}
-
- {{end}}
- {{end}}
- {{end}}
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.no_projects"}}
-
- {{if .Project}}
-
- {{svg .Project.IconName 18 "tw-mr-2"}}{{.Project.Title}}
-
- {{end}}
-
-
+ {{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}
{{end}}
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.assignees"}}
- {{if .HasIssuesOrPullsWritePermission}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
-
-
-
- {{svg "octicon-search" 16}}
-
-
- {{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}
- {{range .Assignees}}
-
- {{svg "octicon-check"}}
-
- {{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}}
-
-
- {{end}}
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
-
-
- {{range .Assignees}}
-
- {{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
-
- {{end}}
-
-
+ {{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
+
{{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}
-
-
-
-
-
+
+
+
{{end}}
diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl
index 260f7c5be4..bee6123e52 100644
--- a/templates/repo/issue/sidebar/assignee_list.tmpl
+++ b/templates/repo/issue/sidebar/assignee_list.tmpl
@@ -1,46 +1,35 @@
+{{$pageMeta := .}}
+{{$data := .AssigneesData}}
+{{$issueAssignees := NIL}}{{if $pageMeta.Issue}}{{$issueAssignees = $pageMeta.Issue.Assignees}}{{end}}
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.assignees"}}
- {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
-
-
-
- {{svg "octicon-search" 16}}
-
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.new.assignees"}} {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
+
+
+
+ {{svg "octicon-search" 16}}
+
+
+ {{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}
+ {{range $data.CandidateAssignees}}
+
+ {{svg "octicon-check"}}
+ {{ctx.AvatarUtils.Avatar . 20}} {{template "repo/search_name" .}}
+
+ {{end}}
- {{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}
- {{range .Assignees}}
-
- {{$AssigneeID := .ID}}
-
- {{$checked := false}}
- {{range $.Issue.Assignees}}
- {{if eq .ID $AssigneeID}}
- {{$checked = true}}
- {{end}}
- {{end}}
- {{svg "octicon-check"}}
-
- {{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
-
+
+
+ {{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
+ {{range $issueAssignees}}
+
+ {{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
{{end}}
-
- {{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
-
- {{range .Issue.Assignees}}
-
-
- {{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
- {{.GetDisplayName}}
-
-
- {{end}}
-
-
diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl
index e9f4baa433..ed80047661 100644
--- a/templates/repo/issue/sidebar/label_list.tmpl
+++ b/templates/repo/issue/sidebar/label_list.tmpl
@@ -1,10 +1,12 @@
-{{$data := .}}
-{{$canChange := and ctx.RootData.HasIssuesOrPullsWritePermission (not $data.Repository.IsArchived)}}
-
+{{$pageMeta := .}}
+{{$data := .LabelsData}}
+
-
+
- {{ctx.Locale.Tr "repo.issues.new.labels"}} {{if $canChange}}{{svg "octicon-gear"}}{{end}}
+ {{ctx.Locale.Tr "repo.issues.new.labels"}} {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
{{if not $data.AllLabels}}
@@ -16,7 +18,7 @@
{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}
{{$previousExclusiveScope := "_no_scope"}}
- {{range .RepoLabels}}
+ {{range $data.RepoLabels}}
{{$exclusiveScope := .ExclusiveScope}}
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
@@ -26,7 +28,7 @@
{{end}}
{{$previousExclusiveScope = "_no_scope"}}
- {{range .OrgLabels}}
+ {{range $data.OrgLabels}}
{{$exclusiveScope := .ExclusiveScope}}
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
@@ -42,7 +44,7 @@
{{ctx.Locale.Tr "repo.issues.new.no_label"}}
{{range $data.AllLabels}}
{{if .IsChecked}}
-
+
{{- ctx.RenderUtils.RenderLabel . -}}
{{end}}
diff --git a/templates/repo/issue/sidebar/label_list_item.tmpl b/templates/repo/issue/sidebar/label_list_item.tmpl
index ad878e918b..5c6808d95b 100644
--- a/templates/repo/issue/sidebar/label_list_item.tmpl
+++ b/templates/repo/issue/sidebar/label_list_item.tmpl
@@ -1,5 +1,5 @@
{{$label := .Label}}
-
{{svg (Iif $label.ExclusiveScope "octicon-dot-fill" "octicon-check")}}
diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl
index e9ca02f77a..4f2b4cb06f 100644
--- a/templates/repo/issue/sidebar/milestone_list.tmpl
+++ b/templates/repo/issue/sidebar/milestone_list.tmpl
@@ -1,22 +1,52 @@
+{{$pageMeta := .}}
+{{$data := .MilestonesData}}
+{{$issueMilestone := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Milestone}}{{$issueMilestone = $pageMeta.Issue.Milestone}}{{end}}
-
-
- {{ctx.Locale.Tr "repo.issues.new.milestone"}}
- {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
-
-
- {{template "repo/issue/milestone/select_menu" .}}
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.new.milestone"}} {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
+
+
+ {{if and (not $data.OpenMilestones) (not $data.ClosedMilestones)}}
+ {{ctx.Locale.Tr "repo.issues.new.no_items"}}
+ {{else}}
+
+ {{svg "octicon-search"}}
+
+
+
+ {{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}
+ {{if $data.OpenMilestones}}
+
+ {{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
+ {{range $data.OpenMilestones}}
+
+ {{svg "octicon-milestone" 18}} {{.Name}}
+
+ {{end}}
+ {{end}}
+ {{if $data.ClosedMilestones}}
+
+ {{ctx.Locale.Tr "repo.issues.new.closed_milestone"}}
+ {{range $data.ClosedMilestones}}
+
+ {{svg "octicon-milestone" 18}} {{.Name}}
+
+ {{end}}
+ {{end}}
+ {{end}}
+
-
-
- {{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
-
- {{if .Issue.Milestone}}
-
- {{svg "octicon-milestone" 18 "tw-mr-2"}}
- {{.Issue.Milestone.Name}}
+
+
+ {{ctx.Locale.Tr "repo.issues.new.no_milestone"}}
+ {{if $issueMilestone}}
+
+ {{svg "octicon-milestone" 18}} {{$issueMilestone.Name}}
{{end}}
diff --git a/templates/repo/issue/sidebar/participant_list.tmpl b/templates/repo/issue/sidebar/participant_list.tmpl
index 91c36fc01e..11debf95c4 100644
--- a/templates/repo/issue/sidebar/participant_list.tmpl
+++ b/templates/repo/issue/sidebar/participant_list.tmpl
@@ -4,7 +4,7 @@
{{range .Participants}}
- {{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}}
+ {{ctx.AvatarUtils.Avatar . 20 "tw-my-0.5 tw-mr-1"}}
{{end}}
diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl
index ec79f8032f..ab1243cadd 100644
--- a/templates/repo/issue/sidebar/project_list.tmpl
+++ b/templates/repo/issue/sidebar/project_list.tmpl
@@ -1,53 +1,49 @@
-{{if .IsProjectsEnabled}}
-
-
-
-
- {{ctx.Locale.Tr "repo.issues.new.projects"}}
- {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
+{{$pageMeta := .}}
+{{$data := .ProjectsData}}
+{{$issueProject := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Project}}{{$issueProject = $pageMeta.Issue.Project}}{{end}}
+
+
+
+
+
+ {{ctx.Locale.Tr "repo.issues.new.projects"}} {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
-
- {{if or .OpenProjects .ClosedProjects}}
+
+ {{if or $data.OpenProjects $data.ClosedProjects}}
{{svg "octicon-search" 16}}
{{end}}
- {{ctx.Locale.Tr "repo.issues.new.clear_projects"}}
- {{if .OpenProjects}}
+ {{ctx.Locale.Tr "repo.issues.new.clear_projects"}}
+ {{if $data.OpenProjects}}
-
- {{ctx.Locale.Tr "repo.issues.new.open_projects"}}
-
- {{range .OpenProjects}}
-
- {{svg .IconName 18 "tw-mr-2"}}{{.Title}}
+ {{ctx.Locale.Tr "repo.issues.new.open_projects"}}
+ {{range $data.OpenProjects}}
+
+ {{svg .IconName 18}} {{.Title}}
{{end}}
{{end}}
- {{if .ClosedProjects}}
+ {{if $data.ClosedProjects}}
-
- {{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
-
- {{range .ClosedProjects}}
-
- {{svg .IconName 18 "tw-mr-2"}}{{.Title}}
+ {{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
+ {{range $data.ClosedProjects}}
+
+ {{svg .IconName 18}} {{.Title}}
{{end}}
{{end}}
-
- {{ctx.Locale.Tr "repo.issues.new.no_projects"}}
-
- {{if .Issue.Project}}
-
- {{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
-
- {{end}}
-
+
+ {{ctx.Locale.Tr "repo.issues.new.no_projects"}}
+ {{if $issueProject}}
+
+ {{svg $issueProject.IconName 18}} {{$issueProject.Title}}
+
+ {{end}}
-{{end}}
+
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl
index cf7b97c02b..e990fc5afc 100644
--- a/templates/repo/issue/sidebar/reviewer_list.tmpl
+++ b/templates/repo/issue/sidebar/reviewer_list.tmpl
@@ -1,10 +1,14 @@
-{{$data := .}}
+{{$pageMeta := .}}
+{{$data := .ReviewersData}}
+{{$repoOwnerName := $pageMeta.Repository.OwnerName}}
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
-
+
{{/* match CreateIssueForm */}}
- {{ctx.Locale.Tr "repo.issues.review.reviewers"}} {{if and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
+ {{ctx.Locale.Tr "repo.issues.review.reviewers"}} {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
{{if $hasCandidates}}
@@ -29,7 +33,7 @@
{{svg "octicon-check"}}
- {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
+ {{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
{{end}}
{{end}}
@@ -47,7 +51,7 @@
{{if .User}}
{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}
{{else if .Team}}
- {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
+ {{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
{{end}}
@@ -64,13 +68,13 @@
{{if .Requested}}
+ data-url="{{$pageMeta.RepoLink}}/issues/request_review?action=detach&issue_ids={{$pageMeta.Issue.ID}}&id={{.ItemID}}">
{{svg "octicon-trash"}}
{{else}}
+ data-url="{{$pageMeta.RepoLink}}/issues/request_review?action=attach&issue_ids={{$pageMeta.Issue.ID}}&id={{.ItemID}}">
{{svg "octicon-sync"}}
{{end}}
@@ -84,8 +88,8 @@
{{range $data.OriginalReviews}}
- {{$originalURLHostname := $data.Repository.GetOriginalURLHostname}}
- {{$originalURL := $data.Repository.OriginalURL}}
+ {{$originalURLHostname := $pageMeta.Repository.GetOriginalURLHostname}}
+ {{$originalURL := $pageMeta.Repository.OriginalURL}}
{{svg (MigrationIcon $originalURLHostname) 20}} {{.OriginalAuthor}}
@@ -108,7 +112,7 @@
-
{{.Description | ctx.RenderUtils.RenderEmoji}}{{end}} -
{{template "repo/issue/labels/label_archived" .}}
- - {{end}} - - {{$previousExclusiveScope = "_no_scope"}} - {{range .OrgLabels}} - {{$exclusiveScope := .ExclusiveScope}} - {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} - - {{end}} - {{$previousExclusiveScope = $exclusiveScope}} - {{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}} {{ctx.RenderUtils.RenderLabel .}} - {{if .Description}}{{.Description | ctx.RenderUtils.RenderEmoji}}{{end}} -
{{template "repo/issue/labels/label_archived" .}}
- - {{end}} - {{else}} -