mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Merge b3e6d4bfe6f1a374e75fb79331e17408f43e1d6e into 40faa6dc78a5b5730a1609ba39daefddac08aa63
This commit is contained in:
commit
13f6285416
85
models/issues/issue_dev_link.go
Normal file
85
models/issues/issue_dev_link.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueDevLinkType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
IssueDevLinkTypeBranch IssueDevLinkType = iota + 1
|
||||||
|
IssueDevLinkTypePullRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueDevLink struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
IssueID int64 `xorm:"INDEX"`
|
||||||
|
LinkType IssueDevLinkType
|
||||||
|
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
|
||||||
|
LinkIndex string // branch name, pull request number or commit sha
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
|
Repo *repo_model.Repository `xorm:"-"` // current repo of issue
|
||||||
|
LinkedRepo *repo_model.Repository `xorm:"-"`
|
||||||
|
PullRequest *PullRequest `xorm:"-"`
|
||||||
|
Branch *git_model.Branch `xorm:"-"`
|
||||||
|
DisplayBranch bool `xorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(IssueDevLink))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssueDevLink) BranchFullName() string {
|
||||||
|
if i.Repo.ID == i.LinkedRepo.ID {
|
||||||
|
return i.Branch.Name
|
||||||
|
}
|
||||||
|
return i.LinkedRepo.FullName() + ":" + i.Branch.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueDevLinks represents a list of issue development links
|
||||||
|
type IssueDevLinks []*IssueDevLink
|
||||||
|
|
||||||
|
// FindIssueDevLinksByIssueID returns a list of issue development links by issue ID
|
||||||
|
func FindIssueDevLinksByIssueID(ctx context.Context, issueID int64) (IssueDevLinks, error) {
|
||||||
|
links := make(IssueDevLinks, 0, 5)
|
||||||
|
return links, db.GetEngine(ctx).Where("issue_id = ?", issueID).Find(&links)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindDevLinksByBranch(ctx context.Context, repoID, linkedRepoID int64, branchName string) (IssueDevLinks, error) {
|
||||||
|
links := make(IssueDevLinks, 0, 5)
|
||||||
|
return links, db.GetEngine(ctx).
|
||||||
|
Join("INNER", "issue", "issue_dev_link.issue_id = issue.id").
|
||||||
|
Where("link_type = ? AND link_index = ? AND linked_repo_id = ?",
|
||||||
|
IssueDevLinkTypeBranch, branchName, linkedRepoID).
|
||||||
|
And("issue.repo_id=?", repoID).
|
||||||
|
Find(&links)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateIssueDevLink(ctx context.Context, link *IssueDevLink) error {
|
||||||
|
_, err := db.GetEngine(ctx).Insert(link)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteIssueDevLinkByBranchName(ctx context.Context, repoID int64, branchName string) error {
|
||||||
|
_, err := db.GetEngine(ctx).
|
||||||
|
Where("linked_repo_id = ? AND link_type = ? AND link_index = ?",
|
||||||
|
repoID, IssueDevLinkTypeBranch, branchName).
|
||||||
|
Delete(new(IssueDevLink))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteIssueDevLinkByPullRequestID(ctx context.Context, pullID int64) error {
|
||||||
|
pullIDStr := strconv.FormatInt(pullID, 10)
|
||||||
|
_, err := db.GetEngine(ctx).Where("link_type = ? AND link_index = ?", IssueDevLinkTypePullRequest, pullIDStr).
|
||||||
|
Delete(new(IssueDevLink))
|
||||||
|
return err
|
||||||
|
}
|
@ -792,6 +792,10 @@ func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err = sess.In("issue_id", issueIDs).Delete(&IssueDevLink{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var attachments []*repo_model.Attachment
|
var attachments []*repo_model.Attachment
|
||||||
err = sess.In("issue_id", issueIDs).Find(&attachments)
|
err = sess.In("issue_id", issueIDs).Find(&attachments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -374,6 +374,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
|
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
|
||||||
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
|
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
|
||||||
newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
|
newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
|
||||||
|
newMigration(314, "Add table issue_dev_link", v1_24.CreateTableIssueDevLink),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
22
models/migrations/v1_24/v314.go
Normal file
22
models/migrations/v1_24/v314.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_24 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateTableIssueDevLink(x *xorm.Engine) error {
|
||||||
|
type IssueDevLink struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
IssueID int64 `xorm:"INDEX"`
|
||||||
|
LinkType int
|
||||||
|
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
|
||||||
|
LinkIndex string // branch name, pull request number or commit sha
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
|
}
|
||||||
|
return x.Sync(new(IssueDevLink))
|
||||||
|
}
|
@ -614,7 +614,7 @@ func (repo *Repository) IsOwnedBy(userID int64) bool {
|
|||||||
|
|
||||||
// CanCreateBranch returns true if repository meets the requirements for creating new branches.
|
// CanCreateBranch returns true if repository meets the requirements for creating new branches.
|
||||||
func (repo *Repository) CanCreateBranch() bool {
|
func (repo *Repository) CanCreateBranch() bool {
|
||||||
return !repo.IsMirror
|
return !repo.IsMirror && !repo.IsArchived
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
|
// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
|
||||||
|
@ -1656,6 +1656,17 @@ issues.label.filter_sort.alphabetically = Alphabetically
|
|||||||
issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically
|
issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically
|
||||||
issues.label.filter_sort.by_size = Smallest size
|
issues.label.filter_sort.by_size = Smallest size
|
||||||
issues.label.filter_sort.reverse_by_size = Largest size
|
issues.label.filter_sort.reverse_by_size = Largest size
|
||||||
|
issues.development = Development
|
||||||
|
issues.maybefixed = May be fixed by %s
|
||||||
|
issues.create_branch_from_issue_success = Create branch %s from issue successfully
|
||||||
|
issues.create_branch_from_repository = Repository the branch to be created
|
||||||
|
issues.base_branch = Base Branch in this repository
|
||||||
|
issues.pr.completed = Completed
|
||||||
|
issues.pr.conflicted = Merge conflicts
|
||||||
|
issues.pr.not_exist_issue = Reference issue does not exist.
|
||||||
|
issues.branch.latest = Latest commit %s
|
||||||
|
issues.link.created = Created %s
|
||||||
|
issues.create_branch_from_issue_error_is_pull = Issue links cannot be created with pull request
|
||||||
issues.num_participants = %d Participants
|
issues.num_participants = %d Participants
|
||||||
issues.attachment.open_tab = `Click to see "%s" in a new tab`
|
issues.attachment.open_tab = `Click to see "%s" in a new tab`
|
||||||
issues.attachment.download = `Click to download "%s"`
|
issues.attachment.download = `Click to download "%s"`
|
||||||
@ -2709,6 +2720,7 @@ branch.create_from = from "%s"
|
|||||||
branch.create_success = Branch "%s" has been created.
|
branch.create_success = Branch "%s" has been created.
|
||||||
branch.branch_already_exists = Branch "%s" already exists in this repository.
|
branch.branch_already_exists = Branch "%s" already exists in this repository.
|
||||||
branch.branch_name_conflict = Branch name "%s" conflicts with the already existing branch "%s".
|
branch.branch_name_conflict = Branch name "%s" conflicts with the already existing branch "%s".
|
||||||
|
branch.branch_not_exist = Branch "%s" do not exists in this repository.
|
||||||
branch.tag_collision = Branch "%s" cannot be created as a tag with same name already exists in the repository.
|
branch.tag_collision = Branch "%s" cannot be created as a tag with same name already exists in the repository.
|
||||||
branch.deleted_by = Deleted by %s
|
branch.deleted_by = Deleted by %s
|
||||||
branch.restore_success = Branch "%s" has been restored.
|
branch.restore_success = Branch "%s" has been restored.
|
||||||
|
@ -112,6 +112,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := issues_model.DeleteIssueDevLinkByBranchName(ctx, repo.ID, update.RefFullName.BranchName()); err != nil {
|
||||||
|
log.Error("Failed to DeleteIssueDevLinkByBranchName: %s/%s %s Error: %v", ownerName, repoName, update.RefFullName.BranchName(), err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
branchesToSync = append(branchesToSync, update)
|
branchesToSync = append(branchesToSync, update)
|
||||||
|
|
||||||
|
@ -852,6 +852,21 @@ func CompareDiff(ctx *context.Context) {
|
|||||||
ctx.Data["AllowMaintainerEdit"] = false
|
ctx.Data["AllowMaintainerEdit"] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refIssueIndex := ctx.FormInt64("ref_issue_index")
|
||||||
|
if refIssueIndex > 0 {
|
||||||
|
refIssue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, refIssueIndex)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Warning(ctx.Tr("repo.issues.pr.not_exist_issue"), true)
|
||||||
|
} else {
|
||||||
|
keyword := "Resolve"
|
||||||
|
if len(setting.Repository.PullRequest.CloseKeywords) > 0 {
|
||||||
|
keyword = setting.Repository.PullRequest.CloseKeywords[0]
|
||||||
|
}
|
||||||
|
ctx.Data["TitleQuery"] = fmt.Sprintf("%s %s", keyword, refIssue.Title)
|
||||||
|
ctx.Data["BodyQuery"] = fmt.Sprintf("%s #%d", keyword, refIssueIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplCompare)
|
ctx.HTML(http.StatusOK, tplCompare)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
114
routers/web/repo/issue_dev.go
Normal file
114
routers/web/repo/issue_dev.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/routers/utils"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateBranchFromIssue(ctx *context.Context) {
|
||||||
|
if ctx.HasError() { // form binding error check
|
||||||
|
ctx.JSONError(ctx.GetErrMsg())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := GetActionIssue(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IsPull {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.create_branch_from_issue_error_is_pull"))
|
||||||
|
ctx.JSONRedirect(issue.Link())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*forms.NewBranchForm)
|
||||||
|
repo := ctx.Repo.Repository
|
||||||
|
gitRepo := ctx.Repo.GitRepo
|
||||||
|
// if create branch in a forked repository
|
||||||
|
if form.RepoID > 0 && form.RepoID != repo.ID {
|
||||||
|
var err error
|
||||||
|
repo, err = repo_model.GetRepositoryByID(ctx, form.RepoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRepositoryByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("OpenRepository", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
canCreateBranch := perm.CanWrite(unit_model.TypeCode) && repo.CanCreateBranch()
|
||||||
|
if !canCreateBranch {
|
||||||
|
ctx.HTTPError(http.StatusForbidden, "No permission to create branch in this repository")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo_service.CreateNewBranch(ctx, ctx.Doer, repo, gitRepo, form.SourceBranchName, form.NewBranchName); err != nil {
|
||||||
|
switch {
|
||||||
|
case git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err):
|
||||||
|
ctx.JSONError(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
|
||||||
|
case git_model.IsErrBranchNameConflict(err):
|
||||||
|
e := err.(git_model.ErrBranchNameConflict)
|
||||||
|
ctx.JSONError(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
|
||||||
|
case git_model.IsErrBranchNotExist(err):
|
||||||
|
ctx.JSONError(ctx.Tr("repo.branch.branch_not_exist", form.SourceBranchName))
|
||||||
|
case git.IsErrPushRejected(err):
|
||||||
|
e := err.(*git.ErrPushRejected)
|
||||||
|
if len(e.Message) == 0 {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
|
||||||
|
} else {
|
||||||
|
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||||
|
"Message": ctx.Tr("repo.editor.push_rejected"),
|
||||||
|
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
||||||
|
"Details": utils.SanitizeFlashErrorString(e.Message),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("UpdatePullRequest.HTMLString", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSONError(flashError)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ctx.ServerError("CreateNewBranch", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issues_model.CreateIssueDevLink(ctx, &issues_model.IssueDevLink{
|
||||||
|
IssueID: issue.ID,
|
||||||
|
LinkType: issues_model.IssueDevLinkTypeBranch,
|
||||||
|
LinkedRepoID: repo.ID,
|
||||||
|
LinkIndex: form.NewBranchName,
|
||||||
|
}); err != nil {
|
||||||
|
ctx.ServerError("CreateIssueDevLink", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.issues.create_branch_from_issue_success", form.NewBranchName))
|
||||||
|
ctx.JSONRedirect(issue.Link())
|
||||||
|
}
|
@ -369,6 +369,7 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
},
|
},
|
||||||
prepareIssueViewCommentsAndSidebarParticipants,
|
prepareIssueViewCommentsAndSidebarParticipants,
|
||||||
preparePullViewReviewAndMerge,
|
preparePullViewReviewAndMerge,
|
||||||
|
prepareIssueViewSidebarDevLinks,
|
||||||
prepareIssueViewSidebarWatch,
|
prepareIssueViewSidebarWatch,
|
||||||
prepareIssueViewSidebarTimeTracker,
|
prepareIssueViewSidebarTimeTracker,
|
||||||
prepareIssueViewSidebarDependency,
|
prepareIssueViewSidebarDependency,
|
||||||
@ -959,3 +960,50 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
|
|||||||
}
|
}
|
||||||
ctx.Data["Issue"] = issue
|
ctx.Data["Issue"] = issue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareIssueViewSidebarDevLinks(ctx *context.Context, issue *issues_model.Issue) {
|
||||||
|
if issue.IsPull {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devLinks, err := issue_service.FindIssueDevLinksByIssue(ctx, issue)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("FindIssueDevLinksByIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["DevLinks"] = devLinks
|
||||||
|
for _, link := range devLinks {
|
||||||
|
if link.LinkType == issues_model.IssueDevLinkTypePullRequest &&
|
||||||
|
!(link.PullRequest.Issue.IsClosed && !link.PullRequest.HasMerged) {
|
||||||
|
ctx.Data["MaybeFixed"] = link.PullRequest
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.IsSigned {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all possible repositories for creating branch model dropdown list
|
||||||
|
forkedRepos, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetForksByUserAndOrgs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
allowedRepos := make([]*repo_model.Repository, 0, len(forkedRepos)+1)
|
||||||
|
for _, repo := range append(forkedRepos, ctx.Repo.Repository) {
|
||||||
|
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if perm.CanWrite(unit.TypeCode) {
|
||||||
|
allowedRepos = append(allowedRepos, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["AllowedRepos"] = allowedRepos
|
||||||
|
ctx.Data["ShowCreateBranchLink"] = !ctx.Repo.Repository.IsEmpty &&
|
||||||
|
ctx.Repo.Repository.CanCreateBranch() &&
|
||||||
|
len(allowedRepos) > 0 && !issue.IsClosed
|
||||||
|
}
|
||||||
|
@ -1254,7 +1254,8 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
|
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
|
||||||
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
|
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
|
||||||
m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory)
|
m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory)
|
||||||
})
|
m.Post("/create_branch", web.Bind(forms.NewBranchForm{}), repo.CreateBranchFromIssue)
|
||||||
|
}, context.RepoMustNotBeArchived())
|
||||||
|
|
||||||
m.Post("/attachments", repo.UploadIssueAttachment)
|
m.Post("/attachments", repo.UploadIssueAttachment)
|
||||||
m.Post("/attachments/remove", repo.DeleteAttachment)
|
m.Post("/attachments/remove", repo.DeleteAttachment)
|
||||||
|
@ -14,9 +14,11 @@ import (
|
|||||||
|
|
||||||
// NewBranchForm form for creating a new branch
|
// NewBranchForm form for creating a new branch
|
||||||
type NewBranchForm struct {
|
type NewBranchForm struct {
|
||||||
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
|
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
|
||||||
CurrentPath string
|
RepoID int64
|
||||||
CreateTag bool
|
SourceBranchName string
|
||||||
|
CurrentPath string
|
||||||
|
CreateTag bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
85
services/issue/dev_link.go
Normal file
85
services/issue/dev_link.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindIssueDevLinksByIssue(ctx context.Context, issue *issues_model.Issue) (issues_model.IssueDevLinks, error) {
|
||||||
|
devLinks, err := issues_model.FindIssueDevLinksByIssueID(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(devLinks, func(i, j int) bool {
|
||||||
|
switch {
|
||||||
|
case devLinks[j].LinkType == issues_model.IssueDevLinkTypePullRequest:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
branchPRExists := make(container.Set[string])
|
||||||
|
|
||||||
|
for _, link := range devLinks {
|
||||||
|
link.Repo = issue.Repo
|
||||||
|
if link.LinkedRepoID == 0 {
|
||||||
|
link.LinkedRepoID = issue.RepoID
|
||||||
|
}
|
||||||
|
isSameRepo := issue.RepoID == link.LinkedRepoID
|
||||||
|
if isSameRepo {
|
||||||
|
link.LinkedRepo = issue.Repo
|
||||||
|
} else if link.LinkedRepoID > 0 {
|
||||||
|
repo, err := repo_model.GetRepositoryByID(ctx, link.LinkedRepoID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
link.LinkedRepo = repo
|
||||||
|
}
|
||||||
|
|
||||||
|
switch link.LinkType {
|
||||||
|
case issues_model.IssueDevLinkTypePullRequest:
|
||||||
|
pullID, err := strconv.ParseInt(link.LinkIndex, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pull, err := issues_model.GetPullRequestByID(ctx, pullID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pull.BaseRepo = issue.Repo
|
||||||
|
pull.HeadRepo = link.LinkedRepo
|
||||||
|
if err := pull.LoadIssue(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pull.Issue.Repo = issue.Repo
|
||||||
|
link.PullRequest = pull
|
||||||
|
branchPRExists.Add(fmt.Sprintf("%d-%s", link.LinkedRepoID, pull.HeadBranch))
|
||||||
|
case issues_model.IssueDevLinkTypeBranch:
|
||||||
|
branch, err := git_model.GetBranch(ctx, link.LinkedRepoID, link.LinkIndex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
link.Branch = branch
|
||||||
|
link.Branch.Repo = link.LinkedRepo
|
||||||
|
link.DisplayBranch = !branchPRExists.Contains(fmt.Sprintf("%d-%s", link.LinkedRepoID, link.LinkIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devLinks, nil
|
||||||
|
}
|
@ -284,6 +284,12 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issue.IsPull {
|
||||||
|
if err := issues_model.DeleteIssueDevLinkByPullRequestID(ctx, issue.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// find attachments related to this issue and remove them
|
// find attachments related to this issue and remove them
|
||||||
if err := issue.LoadAttributes(ctx); err != nil {
|
if err := issue.LoadAttributes(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -313,6 +319,7 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
|
|||||||
&issues_model.IssueDependency{DependencyID: issue.ID},
|
&issues_model.IssueDependency{DependencyID: issue.ID},
|
||||||
&issues_model.Comment{DependentIssueID: issue.ID},
|
&issues_model.Comment{DependentIssueID: issue.ID},
|
||||||
&issues_model.IssuePin{IssueID: issue.ID},
|
&issues_model.IssuePin{IssueID: issue.ID},
|
||||||
|
&issues_model.IssueDevLink{IssueID: issue.ID},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -142,6 +143,23 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pr.Flow == issues_model.PullRequestFlowGithub {
|
||||||
|
devLinks, err := issues_model.FindDevLinksByBranch(ctx, issue.RepoID, pr.HeadRepoID, pr.HeadBranch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, link := range devLinks {
|
||||||
|
if err := issues_model.CreateIssueDevLink(ctx, &issues_model.IssueDevLink{
|
||||||
|
IssueID: link.IssueID,
|
||||||
|
LinkType: issues_model.IssueDevLinkTypePullRequest,
|
||||||
|
LinkedRepoID: pr.HeadRepoID,
|
||||||
|
LinkIndex: strconv.FormatInt(pr.ID, 10),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
compareInfo, err := baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(),
|
compareInfo, err := baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(),
|
||||||
git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false)
|
git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -543,6 +543,9 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := issues_model.DeleteIssueDevLinkByBranchName(ctx, repo.ID, branchName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if pr != nil {
|
if pr != nil {
|
||||||
if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
|
if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
|
||||||
return fmt.Errorf("DeleteBranch: %v", err)
|
return fmt.Errorf("DeleteBranch: %v", err)
|
||||||
|
105
templates/repo/issue/sidebar/development.tmpl
Normal file
105
templates/repo/issue/sidebar/development.tmpl
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{{if not .Issue.IsPull}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.development"}}</strong></span>
|
||||||
|
<div class="ui devlinks list">
|
||||||
|
{{/* AllowedRepos not empty means login user can create new branch in some of the repositories */}}
|
||||||
|
{{if .ShowCreateBranchLink}}
|
||||||
|
<div class="tw-items-center">
|
||||||
|
<a class="tw-mt-1 fluid ui show-modal" data-modal="#create_branch">{{ctx.Locale.Tr "repo.branch.new_branch"}}</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{range .DevLinks}}
|
||||||
|
{{if .PullRequest}}
|
||||||
|
<div class="tw-flex tw-items-center tw-overflow-hidden tw-max-w-full tw-mt-2">
|
||||||
|
<span class="tw-mr-1">{{template "shared/issueicon" .PullRequest.Issue}}</span>
|
||||||
|
<a href="{{.PullRequest.Issue.Link}}" class="ref-issue item tw-overflow-hidden gt-ellipsis tw-whitespace-nowrap">
|
||||||
|
{{.PullRequest.Issue.Title}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ctx.Locale.Tr "repo.issues.link.created" (DateUtils.AbsoluteShort .PullRequest.Issue.CreatedUnix)}}
|
||||||
|
{{if .PullRequest.HasMerged}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.pr.completed"}}
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex tw-items-center tw-overflow-hidden tw-max-w-full">
|
||||||
|
{{svg "octicon-git-commit" 14 "tw-mr-1"}} <a href="{{.PullRequest.BaseRepo.Link}}/src/commit/{{.PullRequest.MergedCommitID}}" data-tooltip-content="{{.PullRequest.MergedCommitID}}" class="tw-overflow-hidden gt-ellipsis tw-whitespace-nowrap">{{.PullRequest.MergedCommitID | ShortSha}}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ctx.Locale.Tr "repo.issues.link.created" (DateUtils.AbsoluteShort .PullRequest.MergedUnix)}}
|
||||||
|
{{else if .PullRequest.ChangedProtectedFiles}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.pr.conflicted"}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else if and .Branch .DisplayBranch}}
|
||||||
|
<div class="tw-flex tw-justify-between tw-items-center tw-mt-2">
|
||||||
|
<div class="tw-flex tw-left tw-items-center tw-overflow-hidden tw-max-w-full">
|
||||||
|
{{svg "octicon-git-branch" 14 "tw-mr-1"}}
|
||||||
|
<a href="{{.Branch.Repo.Link}}/src/branch/{{.Branch.Name}}" data-tooltip-content="{{.BranchFullName}}" class="tw-overflow-hidden gt-ellipsis tw-whitespace-nowrap">
|
||||||
|
{{.BranchFullName}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="tw-right tw-items-center">
|
||||||
|
<a class="ui button mini compact basic icon" href="{{$.Issue.Repo.Link}}/compare/{{$.Issue.Repo.DefaultBranch}}...{{.Branch.Repo.FullName}}:{{.Branch.Name}}?ref_issue_index={{$.Issue.Index}}">
|
||||||
|
{{svg "octicon-git-pull-request"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>{{ctx.Locale.Tr "repo.issues.branch.latest" (DateUtils.AbsoluteShort .Branch.CommitTime)}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui tiny modal" id="create_branch">
|
||||||
|
<div class="header">
|
||||||
|
{{ctx.Locale.Tr "repo.branch.new_branch"}}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<form class="ui form form-fetch-action" action="{{.Issue.Link}}/create_branch"
|
||||||
|
method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="required field">
|
||||||
|
<label for="new_branch_name">{{ctx.Locale.Tr "form.NewBranchName"}}</label>
|
||||||
|
<input name="new_branch_name" type="text" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="required field">
|
||||||
|
<label for="source_repository">{{ctx.Locale.Tr "repo.issues.create_branch_from_repository"}}</label>
|
||||||
|
<div class="ui selection dropdown ellipsis-items-nowrap">
|
||||||
|
<input type="hidden" name="repo_id" value="{{.Issue.Repo.ID}}">
|
||||||
|
<div class="text">
|
||||||
|
<strong id="repo-branch-current">{{.Issue.Repo.FullName}}</strong>
|
||||||
|
</div>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
{{range .AllowedRepos}}
|
||||||
|
<div class="item" data-value="{{.ID}}" title="{{.FullName}}">{{.FullName}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="required field">
|
||||||
|
<label for="source_branch_name">{{ctx.Locale.Tr "repo.issues.base_branch"}}</label>
|
||||||
|
<div class="ui selection dropdown ellipsis-items-nowrap">
|
||||||
|
<input type="hidden" name="source_branch_name" value="{{.Issue.Repo.DefaultBranch}}">
|
||||||
|
<div class="text">
|
||||||
|
<strong id="repo-branch-current">{{.Issue.Repo.DefaultBranch}}</strong>
|
||||||
|
</div>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
{{range .Branches}}
|
||||||
|
<div class="item" data-value="{{.}}" title="{{.}}">{{.}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text right actions">
|
||||||
|
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "repo.branch.new_branch"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
@ -15,6 +15,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
|
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
|
||||||
|
|
||||||
|
{{template "repo/issue/sidebar/development" .}}
|
||||||
{{template "repo/issue/sidebar/participant_list" $}}
|
{{template "repo/issue/sidebar/participant_list" $}}
|
||||||
{{template "repo/issue/sidebar/watch_notification" $}}
|
{{template "repo/issue/sidebar/watch_notification" $}}
|
||||||
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}
|
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}
|
||||||
|
@ -133,6 +133,10 @@
|
|||||||
·
|
·
|
||||||
{{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}}
|
{{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}}
|
||||||
</span>
|
</span>
|
||||||
|
{{if .MaybeFixed}}
|
||||||
|
{{$fixedStr := HTMLFormat `<a href="%s" class="ref-issue">#%d</a>` .MaybeFixed.Issue.Link .MaybeFixed.Index}}
|
||||||
|
· <span>{{ctx.Locale.Tr "repo.issues.maybefixed" $fixedStr}}</span>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all .2s ease;
|
transition: all .2s ease;
|
||||||
z-index: 500;
|
z-index: 9999;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: 0 8px 24px var(--color-shadow);
|
box-shadow: 0 8px 24px var(--color-shadow);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user