Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new section named development in issue view sidebar to interact with branch/pr #31899

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions models/issues/issue_dev_link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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"`

LinkedRepo *repo_model.Repository `xorm:"-"`
PullRequest *PullRequest `xorm:"-"`
Branch *git_model.Branch `xorm:"-"`
DisplayBranch bool `xorm:"-"`
}

func init() {
db.RegisterModel(new(IssueDevLink))
}

// 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
}
4 changes: 4 additions & 0 deletions models/issues/issue_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,10 @@ func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []
return nil, err
}

if _, err = sess.In("issue_id", issueIDs).Delete(&IssueDevLink{}); err != nil {
return nil, err
}

var attachments []*repo_model.Attachment
err = sess.In("issue_id", issueIDs).Find(&attachments)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,8 @@ var migrations = []Migration{
NewMigration("Add index for release sha1", v1_23.AddIndexForReleaseSha1),
// v305 -> v306
NewMigration("Add Repository Licenses", v1_23.AddRepositoryLicenses),
// v306 -> v307
NewMigration("Add table issue_dev_link", v1_23.CreateTableIssueDevLink),
}

// GetCurrentDBVersion returns the current db version
Expand Down
22 changes: 22 additions & 0 deletions models/migrations/v1_23/v306.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_23 //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))
}
9 changes: 9 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,15 @@ issues.label.filter_sort.alphabetically = Alphabetically
issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically
issues.label.filter_sort.by_size = Smallest 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.base_branch = Base Branch
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.num_participants = %d Participants
issues.attachment.open_tab = `Click to see "%s" in a new tab`
issues.attachment.download = `Click to download "%s"`
Expand Down
4 changes: 4 additions & 0 deletions routers/private/hook_post_receive.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
})
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 {
branchesToSync = append(branchesToSync, update)

Expand Down
15 changes: 15 additions & 0 deletions routers/web/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,21 @@ func CompareDiff(ctx *context.Context) {
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)
}

Expand Down
36 changes: 36 additions & 0 deletions routers/web/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,42 @@ func ViewIssue(ctx *context.Context) {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}

if ctx.IsSigned {
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

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 {
if !(link.PullRequest.Issue.IsClosed && !link.PullRequest.HasMerged) {
ctx.Data["MaybeFixed"] = link.PullRequest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be conflict with cross references? In some cases, the title may say "May be fixed by #1" and the comment list may say "User referenced a pull request that will close this issue (#2)".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In future, I will add a new record in this table when there is a xref.

break
}
}
}
}

ctx.HTML(http.StatusOK, tplIssueView)
}

Expand Down
109 changes: 109 additions & 0 deletions routers/web/repo/issue_dev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
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) {
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 form.RepoID > 0 {
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("GetRepositoryByID", err)
return
}
defer gitRepo.Close()
}

perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
}

canCreateBranch := perm.CanWrite(unit_model.TypeCode) && repo.CanCreateBranch()
if !canCreateBranch {
ctx.NotFound("CreateBranch", nil)
return
}

if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
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.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", ctx.Repo.BranchName))
ctx.JSONRedirect(issue.Link())
}
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ func registerRoutes(m *web.Router) {
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
m.Post("/create_branch", web.Bind(forms.NewBranchForm{}), repo.CreateBranchFromIssue)
}, context.RepoMustNotBeArchived())

m.Group("/{index}", func() {
Expand Down
8 changes: 5 additions & 3 deletions services/forms/repo_branch_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (

// NewBranchForm form for creating a new branch
type NewBranchForm struct {
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
CurrentPath string
CreateTag bool
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
RepoID int64
SourceBranchName string
CurrentPath string
CreateTag bool
}

// Validate validates the fields
Expand Down
Loading
Loading