mirror of https://github.com/go-gitea/gitea.git
Merge 811c67640c into ed698d1a61
commit
b97b038bd1
@ -0,0 +1,59 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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
|
||||
LinkID int64 // branch id in branch table or the pull request id(not issue if of the pull request)
|
||||
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 CreateIssueDevLink(ctx context.Context, link *IssueDevLink) error {
|
||||
_, err := db.GetEngine(ctx).Insert(link)
|
||||
return err
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
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
|
||||
LinkID int64 // branch id in branch table or pull request id
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
return x.Sync(new(IssueDevLink))
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_CreateTableIssueDevLink(t *testing.T) {
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0)
|
||||
defer deferable()
|
||||
|
||||
assert.NoError(t, CreateTableIssueDevLink(x))
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
// 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/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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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, 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
|
||||
}
|
||||
|
||||
branch, err := git_model.GetBranch(ctx, repo.ID, form.NewBranchName)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBranch", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issues_model.CreateIssueDevLink(ctx, &issues_model.IssueDevLink{
|
||||
IssueID: issue.ID,
|
||||
LinkType: issues_model.IssueDevLinkTypeBranch,
|
||||
LinkedRepoID: repo.ID,
|
||||
LinkID: branch.ID,
|
||||
}); 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())
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
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 {
|
||||
return devLinks[j].LinkType != issues_model.IssueDevLinkTypePullRequest
|
||||
})
|
||||
|
||||
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:
|
||||
pull, err := issues_model.GetPullRequestByID(ctx, link.LinkID)
|
||||
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-%d-%s", link.LinkedRepoID, link.LinkType, pull.HeadBranch))
|
||||
case issues_model.IssueDevLinkTypeBranch:
|
||||
branch, err := git_model.GetBranchByID(ctx, link.LinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link.Branch = branch
|
||||
link.Branch.Repo = link.LinkedRepo
|
||||
link.DisplayBranch = !branchPRExists.Contains(fmt.Sprintf("%d-%d-%d", link.LinkedRepoID, link.LinkType, link.LinkID))
|
||||
}
|
||||
}
|
||||
|
||||
return devLinks, nil
|
||||
}
|
||||
@ -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}}
|
||||
Loading…
Reference in New Issue