Skip to content

Commit 62fda25

Browse files
committed
Add a new section named development in issue view sidebar to interact with branch/pr
1 parent 40036b6 commit 62fda25

File tree

14 files changed

+415
-52
lines changed

14 files changed

+415
-52
lines changed

models/issues/issue_dev_link.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package issues
5+
6+
import (
7+
"context"
8+
"strconv"
9+
10+
"code.gitea.io/gitea/models/db"
11+
git_model "code.gitea.io/gitea/models/git"
12+
repo_model "code.gitea.io/gitea/models/repo"
13+
"code.gitea.io/gitea/modules/timeutil"
14+
)
15+
16+
type IssueDevLinkType int
17+
18+
const (
19+
IssueDevLinkTypeBranch IssueDevLinkType = iota + 1
20+
IssueDevLinkTypePullRequest
21+
)
22+
23+
type IssueDevLink struct {
24+
ID int64 `xorm:"pk autoincr"`
25+
IssueID int64 `xorm:"INDEX"`
26+
LinkType IssueDevLinkType
27+
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
28+
LinkIndex string // branch name, pull request number or commit sha
29+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
30+
31+
LinkedRepo *repo_model.Repository `xorm:"-"`
32+
PullRequest *PullRequest `xorm:"-"`
33+
Branch *git_model.Branch `xorm:"-"`
34+
}
35+
36+
func init() {
37+
db.RegisterModel(new(IssueDevLink))
38+
}
39+
40+
// IssueDevLinks represents a list of issue development links
41+
type IssueDevLinks []*IssueDevLink
42+
43+
// FindIssueDevLinksByIssueID returns a list of issue development links by issue ID
44+
func FindIssueDevLinksByIssueID(ctx context.Context, issueID int64) (IssueDevLinks, error) {
45+
links := make(IssueDevLinks, 0, 5)
46+
return links, db.GetEngine(ctx).Where("issue_id = ?", issueID).Find(&links)
47+
}
48+
49+
func FindDevLinksByBranch(ctx context.Context, repoID, linkedRepoID int64, branchName string) (IssueDevLinks, error) {
50+
links := make(IssueDevLinks, 0, 5)
51+
return links, db.GetEngine(ctx).
52+
Join("INNER", "issue", "issue_dev_link.issue_id = issue.id").
53+
Where("link_type = ? AND link_index = ? AND linked_repo_id = ?",
54+
IssueDevLinkTypeBranch, branchName, linkedRepoID).
55+
And("issue.repo_id=?", repoID).
56+
Find(&links)
57+
}
58+
59+
func CreateIssueDevLink(ctx context.Context, link *IssueDevLink) error {
60+
_, err := db.GetEngine(ctx).Insert(link)
61+
return err
62+
}
63+
64+
func DeleteIssueDevLinkByBranchName(ctx context.Context, repoID int64, branchName string) error {
65+
_, err := db.GetEngine(ctx).
66+
Where("link_type = ? AND link_index = ? AND linked_repo_id = ?",
67+
IssueDevLinkTypeBranch, branchName, repoID).
68+
Delete(new(IssueDevLink))
69+
return err
70+
}
71+
72+
func DeleteIssueDevLinkByPullRequestID(ctx context.Context, pullID int64) error {
73+
pullIDStr := strconv.FormatInt(pullID, 10)
74+
_, err := db.GetEngine(ctx).Where("link_type = ? AND link_index = ?", IssueDevLinkTypePullRequest, pullIDStr).
75+
Delete(new(IssueDevLink))
76+
return err
77+
}

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,8 @@ var migrations = []Migration{
601601
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
602602
// v304 -> v305
603603
NewMigration("Add index for release sha1", v1_23.AddIndexForReleaseSha1),
604+
// v305 -> v306
605+
NewMigration("Add table issue_dev_link", v1_23.CreateTableIssueDevLink),
604606
}
605607

606608
// GetCurrentDBVersion returns the current db version

models/migrations/v1_23/v305.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_23 //nolint
5+
6+
import (
7+
"code.gitea.io/gitea/modules/timeutil"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func CreateTableIssueDevLink(x *xorm.Engine) error {
13+
type IssueDevLink struct {
14+
ID int64 `xorm:"pk autoincr"`
15+
IssueID int64 `xorm:"INDEX"`
16+
LinkType int
17+
LinkIndex string // branch name, pull request number or commit sha
18+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
19+
}
20+
return x.Sync(new(IssueDevLink))
21+
}

models/organization/org.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -508,16 +508,20 @@ func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model.
508508
return false
509509
}
510510

511+
func orgAllowedCreatedRepoSubQuery(userID int64) *builder.Builder {
512+
return builder.Select("`user`.id").From("`user`").
513+
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
514+
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
515+
Where(builder.Eq{"`team_user`.uid": userID}).
516+
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true}))
517+
}
518+
511519
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
512520
// are allowed to create repos.
513521
func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) {
514522
orgs := make([]*Organization, 0, 10)
515523

516-
return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`").
517-
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
518-
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
519-
Where(builder.Eq{"`team_user`.uid": userID}).
520-
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))).
524+
return orgs, db.GetEngine(ctx).Where(builder.In("id", orgAllowedCreatedRepoSubQuery(userID))).
521525
Asc("`user`.name").
522526
Find(&orgs)
523527
}

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,7 @@ issues.label.filter_sort.alphabetically = Alphabetically
16221622
issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically
16231623
issues.label.filter_sort.by_size = Smallest size
16241624
issues.label.filter_sort.reverse_by_size = Largest size
1625+
issues.development = Development
16251626
issues.num_participants = %d Participants
16261627
issues.attachment.open_tab = `Click to see "%s" in a new tab`
16271628
issues.attachment.download = `Click to download "%s"`

routers/web/repo/branch.go

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,54 @@ func redirect(ctx *context.Context) {
176176
ctx.JSONRedirect(ctx.Repo.RepoLink + "/branches?page=" + url.QueryEscape(ctx.FormString("page")))
177177
}
178178

179+
func handleCreateBranchError(ctx *context.Context, err error, form *forms.NewBranchForm) {
180+
if models.IsErrProtectedTagName(err) {
181+
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
182+
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
183+
return
184+
}
185+
186+
if models.IsErrTagAlreadyExists(err) {
187+
e := err.(models.ErrTagAlreadyExists)
188+
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
189+
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
190+
return
191+
}
192+
if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
193+
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
194+
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
195+
return
196+
}
197+
if git_model.IsErrBranchNameConflict(err) {
198+
e := err.(git_model.ErrBranchNameConflict)
199+
ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
200+
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
201+
return
202+
}
203+
if git.IsErrPushRejected(err) {
204+
e := err.(*git.ErrPushRejected)
205+
if len(e.Message) == 0 {
206+
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
207+
} else {
208+
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
209+
"Message": ctx.Tr("repo.editor.push_rejected"),
210+
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
211+
"Details": utils.SanitizeFlashErrorString(e.Message),
212+
})
213+
if err != nil {
214+
ctx.ServerError("UpdatePullRequest.HTMLString", err)
215+
return
216+
}
217+
ctx.Flash.Error(flashError)
218+
}
219+
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
220+
return
221+
}
222+
223+
ctx.ServerError("CreateNewBranch", err)
224+
return
225+
}
226+
179227
// CreateBranch creates new branch in repository
180228
func CreateBranch(ctx *context.Context) {
181229
form := web.GetForm(ctx).(*forms.NewBranchForm)
@@ -204,50 +252,7 @@ func CreateBranch(ctx *context.Context) {
204252
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName)
205253
}
206254
if err != nil {
207-
if models.IsErrProtectedTagName(err) {
208-
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
209-
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
210-
return
211-
}
212-
213-
if models.IsErrTagAlreadyExists(err) {
214-
e := err.(models.ErrTagAlreadyExists)
215-
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
216-
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
217-
return
218-
}
219-
if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
220-
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
221-
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
222-
return
223-
}
224-
if git_model.IsErrBranchNameConflict(err) {
225-
e := err.(git_model.ErrBranchNameConflict)
226-
ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
227-
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
228-
return
229-
}
230-
if git.IsErrPushRejected(err) {
231-
e := err.(*git.ErrPushRejected)
232-
if len(e.Message) == 0 {
233-
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
234-
} else {
235-
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
236-
"Message": ctx.Tr("repo.editor.push_rejected"),
237-
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
238-
"Details": utils.SanitizeFlashErrorString(e.Message),
239-
})
240-
if err != nil {
241-
ctx.ServerError("UpdatePullRequest.HTMLString", err)
242-
return
243-
}
244-
ctx.Flash.Error(flashError)
245-
}
246-
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
247-
return
248-
}
249-
250-
ctx.ServerError("CreateNewBranch", err)
255+
handleCreateBranchError(ctx, err, form)
251256
return
252257
}
253258

routers/web/repo/issue.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,6 +2075,21 @@ func ViewIssue(ctx *context.Context) {
20752075
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
20762076
}
20772077

2078+
forkedRepos, err := repo_model.FindUserOrgForks(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID)
2079+
if err != nil {
2080+
ctx.ServerError("FindUserOrgForks", err)
2081+
return
2082+
}
2083+
2084+
ctx.Data["AllowedRepos"] = append(forkedRepos, ctx.Repo.Repository)
2085+
2086+
devLinks, err := issue_service.FindIssueDevLinksByIssue(ctx, issue)
2087+
if err != nil {
2088+
ctx.ServerError("FindIssueDevLinksByIssueID", err)
2089+
return
2090+
}
2091+
ctx.Data["DevLinks"] = devLinks
2092+
20782093
ctx.HTML(http.StatusOK, tplIssueView)
20792094
}
20802095

routers/web/repo/issue_dev.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"net/http"
8+
9+
issues_model "code.gitea.io/gitea/models/issues"
10+
"code.gitea.io/gitea/modules/web"
11+
"code.gitea.io/gitea/services/context"
12+
"code.gitea.io/gitea/services/forms"
13+
repo_service "code.gitea.io/gitea/services/repository"
14+
)
15+
16+
func CreateBranchFromIssue(ctx *context.Context) {
17+
issue := GetActionIssue(ctx)
18+
if ctx.Written() {
19+
return
20+
}
21+
22+
if issue.IsPull {
23+
ctx.Flash.Error(ctx.Tr("repo.issues.create_branch_from_issue_error_is_pull"))
24+
ctx.Redirect(issue.Link(), http.StatusSeeOther)
25+
return
26+
}
27+
28+
form := web.GetForm(ctx).(*forms.NewBranchForm)
29+
if !ctx.Repo.CanCreateBranch() {
30+
ctx.NotFound("CreateBranch", nil)
31+
return
32+
}
33+
34+
if ctx.HasError() {
35+
ctx.Flash.Error(ctx.GetErrMsg())
36+
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
37+
return
38+
}
39+
40+
if err := repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, form.SourceBranchName, form.NewBranchName); err != nil {
41+
handleCreateBranchError(ctx, err, form)
42+
return
43+
}
44+
45+
if err := issues_model.CreateIssueDevLink(ctx, &issues_model.IssueDevLink{
46+
IssueID: issue.ID,
47+
LinkType: issues_model.IssueDevLinkTypeBranch,
48+
LinkIndex: form.NewBranchName,
49+
}); err != nil {
50+
ctx.ServerError("CreateIssueDevLink", err)
51+
return
52+
}
53+
54+
ctx.Flash.Success(ctx.Tr("repo.issues.create_branch_from_issue_success", ctx.Repo.BranchName))
55+
ctx.Redirect(issue.Link())
56+
}

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,7 @@ func registerRoutes(m *web.Router) {
12161216
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
12171217
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
12181218
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
1219+
m.Post("/create_branch", web.Bind(forms.NewBranchForm{}), repo.CreateBranchFromIssue)
12191220
}, context.RepoMustNotBeArchived())
12201221

12211222
m.Group("/{index}", func() {

services/forms/repo_branch_form.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import (
1414

1515
// NewBranchForm form for creating a new branch
1616
type NewBranchForm struct {
17-
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
18-
CurrentPath string
19-
CreateTag bool
17+
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
18+
SourceBranchName string
19+
CurrentPath string
20+
CreateTag bool
2021
}
2122

2223
// Validate validates the fields

0 commit comments

Comments
 (0)