Compare commits

...

80 Commits

Author SHA1 Message Date
Lunny Xiao
359af8f989
Merge c32d4aa9269edd28983bc13da70546946af70ef4 into b907b9fb1a1a792b9bc25112fa3eb3f8d2fb4397 2025-10-01 08:17:06 +02:00
Lunny Xiao
c32d4aa926 Merge branch 'main' into lunny/issue_dev 2025-09-30 20:40:00 -07:00
Lunny Xiao
1d9b7a1a61
Fix test 2025-08-30 17:34:57 -07:00
Lunny Xiao
1e4da5bc1a
Fix test 2025-08-30 11:11:54 -07:00
Lunny Xiao
c60d6fc5ab
Fix test 2025-08-30 10:46:28 -07:00
Lunny Xiao
27e0450aca
Fix test 2025-08-29 22:24:43 -07:00
Lunny Xiao
b580b59c65
Fix test 2025-08-29 21:13:24 -07:00
Lunny Xiao
43cdc71ed7 Merge branch 'main' into lunny/issue_dev 2025-08-29 20:13:38 -07:00
Lunny Xiao
793a9e6639
Fix lint 2025-08-29 20:12:05 -07:00
Lunny Xiao
7ea492cb90 Merge branch 'main' into lunny/issue_dev 2025-08-29 11:16:52 -07:00
Lunny Xiao
5d7388924f
Use branch id or pull request id as dev link id 2025-08-29 11:16:40 -07:00
Lunny Xiao
1787e028db Merge branch 'main' into lunny/issue_dev 2025-08-27 22:48:32 -07:00
Lunny Xiao
0735d3c496 Merge branch 'main' into lunny/issue_dev 2025-06-09 15:57:33 -07:00
Lunny Xiao
eaf998bb9b
Fix bug 2025-03-28 21:14:32 -07:00
Lunny Xiao
7b4c3e2a87
Fix bug 2025-03-28 20:46:41 -07:00
Lunny Xiao
097c649387 Merge branch 'main' into lunny/issue_dev 2025-03-28 20:19:36 -07:00
Lunny Xiao
260eee22f6 Merge branch 'main' into lunny/issue_dev 2025-03-27 17:32:20 -07:00
Lunny Xiao
b3e6d4bfe6 Merge branch 'main' into lunny/issue_dev 2025-02-17 12:43:05 -08:00
Lunny Xiao
479364a908 Merge branch 'main' into lunny/issue_dev 2025-02-07 19:52:34 -08:00
Lunny Xiao
a84034a838 Merge branch 'main' into lunny/issue_dev 2024-12-30 20:14:46 -08:00
Lunny Xiao
3addd3aa5a
Move migration to v1.24 2024-12-30 20:14:43 -08:00
Lunny Xiao
2d49aec197 Merge branch 'main' into lunny/issue_dev 2024-12-09 18:43:43 -08:00
Lunny Xiao
832b78eb69
Empty, mirror & archived repository will not allow create branch 2024-12-09 18:43:36 -08:00
Lunny Xiao
da7f700d28
Fix create branch permission 2024-12-07 21:13:49 -08:00
Lunny Xiao
a81c785918 Merge branch 'main' into lunny/issue_dev 2024-12-07 20:59:54 -08:00
Lunny Xiao
d869d3286f Merge branch 'main' into lunny/issue_dev 2024-12-05 10:17:10 -08:00
Lunny Xiao
478fbd5f49 Merge branch 'main' into lunny/issue_dev 2024-12-04 15:44:30 -08:00
Lunny Xiao
f2a28d0980
Add missing locale string 2024-12-04 15:44:24 -08:00
Lunny Xiao
051b42a4a8
Add ellipsis 2024-12-03 22:02:04 -08:00
Lunny Xiao
66dbadcf68 Merge branch 'main' into lunny/issue_dev 2024-12-03 21:55:18 -08:00
Lunny Xiao
029a444523
Fix dropdown list 2024-12-03 21:54:55 -08:00
Lunny Xiao
3556305d15 Merge branch 'main' into lunny/issue_dev 2024-12-03 17:09:10 -08:00
Lunny Xiao
35b7d32725
add margin top 2 for item 2024-12-03 17:09:07 -08:00
Lunny Xiao
f77d7e71e6 Merge branch 'main' into lunny/issue_dev 2024-12-02 23:50:02 -08:00
Lunny Xiao
f52a57dcd5
Fix bug 2024-12-02 23:49:57 -08:00
Lunny Xiao
17956ae24a
Make toast in front of modal 2024-11-28 21:31:30 -08:00
Lunny Xiao
267a2ec367
fill default branch name 2024-11-28 21:07:07 -08:00
Lunny Xiao
c0d1960efb
Fix lint 2024-11-28 17:25:33 -08:00
Lunny Xiao
e5b581ccfa
More ui improvements 2024-11-28 16:49:17 -08:00
Lunny Xiao
b3086c9e00
improve the popup branch create window 2024-11-28 16:39:50 -08:00
Lunny Xiao
79cb88901b
display pull request on float window 2024-11-28 16:21:03 -08:00
Lunny Xiao
ab6d2ed9d7
Display forked repository's branch 2024-11-28 16:13:40 -08:00
Lunny Xiao
703eebfa92
Adjust development sidebar 2024-11-28 15:54:27 -08:00
Lunny Xiao
121b823971
Follow template change 2024-11-28 12:35:51 -08:00
Lunny Xiao
3d8ed0e853 Merge branch 'main' into lunny/issue_dev 2024-11-28 12:14:58 -08:00
Lunny Xiao
72d06ee533
Remove issue.ref 2024-11-28 11:57:52 -08:00
Lunny Xiao
fcc2c577dc Merge branch 'main' into lunny/issue_dev 2024-11-06 14:26:34 -08:00
Lunny Xiao
d3f3fb136b Merge branch 'main' into lunny/issue_dev 2024-11-05 16:28:42 -08:00
Lunny Xiao
570f338490 Merge branch 'lunny/issue_dev' of github.com:lunny/gitea into lunny/issue_dev 2024-10-22 00:08:04 -07:00
Lunny Xiao
f17020c49c Merge branch 'main' into lunny/issue_dev 2024-10-22 00:06:03 -07:00
Lunny Xiao
c8a8fc6a1e
Don't display create branch link for closed issue 2024-10-22 00:06:00 -07:00
Lunny Xiao
f3c1634f4b
Merge branch 'main' into lunny/issue_dev 2024-10-14 02:51:32 +08:00
Lunny Xiao
66681c33ff
merge if conditions 2024-10-09 18:42:55 -07:00
Lunny Xiao
30d4010983
Some improvements 2024-10-09 18:34:08 -07:00
Lunny Xiao
7fd210b733
Add missed language content 2024-10-09 18:28:28 -07:00
Lunny Xiao
f11fc417c6
Update routers/web/repo/issue_dev.go
Co-authored-by: yp05327 <576951401@qq.com>
2024-10-10 09:25:06 +08:00
Lunny Xiao
86e4f29d20
Update routers/web/repo/issue_dev.go
Co-authored-by: yp05327 <576951401@qq.com>
2024-10-10 09:25:00 +08:00
Lunny Xiao
cbeed1168f Merge branch 'main' into lunny/issue_dev 2024-10-01 18:12:49 -07:00
Lunny Xiao
0e0527456e
Don't use SafeHTML 2024-09-30 00:21:39 -07:00
Lunny Xiao
e49445b17d Merge branch 'lunny/issue_dev' of github.com:lunny/gitea into lunny/issue_dev 2024-09-29 20:01:02 -07:00
Lunny Xiao
cb37b5925c Merge branch 'main' into lunny/issue_dev 2024-09-29 20:00:45 -07:00
Lunny Xiao
abe592c6e2
Fix repository list permissions 2024-09-29 20:00:35 -07:00
techknowlogick
8951291e6c
Merge branch 'main' into lunny/issue_dev 2024-09-24 12:30:27 -04:00
Lunny Xiao
837526aaf0 Merge branch 'main' into lunny/issue_dev 2024-09-08 22:20:00 -07:00
Lunny Xiao
003707ff97
Improve the name of branch creation dialog 2024-09-08 22:19:58 -07:00
Lunny Xiao
5fb581a885 Merge branch 'main' into lunny/issue_dev 2024-09-02 23:49:05 -07:00
Lunny Xiao
6e0bc0d3be
Fix test 2024-09-02 23:48:53 -07:00
Lunny Xiao
3abb72946c
Revert unnecessary change 2024-08-29 12:55:03 -07:00
Lunny Xiao
0208f5bc4c
Add ref issue when creating pull request from issue 2024-08-29 12:43:47 -07:00
Lunny Xiao
6b829f77a3
Delete dev links when repository/issue/pull/branch deleted 2024-08-29 12:13:49 -07:00
Lunny Xiao
e64f2322de
Avoid template lint bug 2024-08-29 11:53:41 -07:00
Lunny Xiao
bb988483bd
Allow multiple branches, pull requests 2024-08-29 11:50:03 -07:00
Lunny Xiao
6feb5a1e17 Merge branch 'main' into lunny/issue_dev 2024-08-29 10:00:03 -07:00
Lunny Xiao
e2d7980a84
Fix template 2024-08-29 09:59:51 -07:00
Lunny Xiao
bc1b2969ef
revert unnecessary change 2024-08-21 20:17:07 -07:00
Lunny Xiao
9ea33761a6
revert unnecessary change 2024-08-21 19:58:25 -07:00
Lunny Xiao
359e660f0f
some improvements 2024-08-21 19:49:09 -07:00
Lunny Xiao
b4eac75dba
Some improvements 2024-08-21 19:40:32 -07:00
Lunny Xiao
2361ec5a44
Improvements for creating branch model 2024-08-21 19:08:18 -07:00
Lunny Xiao
62fda252bd
Add a new section named development in issue view sidebar to interact with branch/pr 2024-08-21 15:34:03 -07:00
25 changed files with 622 additions and 16 deletions

View File

@ -213,3 +213,27 @@
is_deleted: false
deleted_by_id: 0
deleted_unix: 0
-
id: 26
repo_id: 10
name: 'develop'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'Initial commit'
commit_time: 1489927679
pusher_id: 2
is_deleted: false
deleted_by_id: 0
deleted_unix: 0
-
id: 27
repo_id: 1
name: 'pr-to-update'
commit_id: '62fb502a7172d4453f0322a2cc85bddffa57f07a'
commit_message: 'add WoW File'
commit_time: 1579204295
pusher_id: 2
is_deleted: false
deleted_by_id: 0
deleted_unix: 0

View File

@ -156,6 +156,19 @@ func init() {
db.RegisterModel(new(RenamedBranch))
}
func GetBranchByID(ctx context.Context, branchID int64) (*Branch, error) {
var branch Branch
has, err := db.GetEngine(ctx).ID(branchID).Get(&branch)
if err != nil {
return nil, err
} else if !has {
return nil, ErrBranchNotExist{
RepoID: branch.RepoID,
}
}
return &branch, nil
}
func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, error) {
var branch Branch
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)

View File

@ -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
}

View File

@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_24"
"code.gitea.io/gitea/models/migrations/v1_25"
"code.gitea.io/gitea/models/migrations/v1_26"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -394,6 +395,9 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.24.0 ends at database version 321
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
// Gitea 1.25.0 ends at database version 323
newMigration(323, "Add table issue_dev_link", v1_26.CreateTableIssueDevLink),
}
return preparedMigrations
}

View File

@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -642,7 +642,7 @@ func (repo *Repository) IsOwnedBy(userID int64) bool {
// CanCreateBranch returns true if repository meets the requirements for creating new branches.
func (repo *Repository) CanCreateBranch() bool {
return !repo.IsMirror
return !repo.IsMirror && !repo.IsArchived
}
// CanEnablePulls returns true if repository meets the requirements of accepting pulls.

View File

@ -1694,6 +1694,17 @@ 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.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.attachment.open_tab = `Click to see "%s" in a new tab`
issues.attachment.download = `Click to download "%s"`
@ -2759,6 +2770,7 @@ branch.create_from = from "%s"
branch.create_success = Branch "%s" has been created.
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_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.deleted_by = Deleted by %s
branch.restore_success = Branch "%s" has been restored.

View File

@ -113,6 +113,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
})
return
}
if err := repo_service.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)

View File

@ -862,6 +862,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)
}

View File

@ -0,0 +1,120 @@
// 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
}
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())
}

View File

@ -384,6 +384,7 @@ func ViewIssue(ctx *context.Context) {
prepareFuncs := []func(*context.Context, *issues_model.Issue){
prepareIssueViewContent,
prepareIssueViewCommentsAndSidebarParticipants,
prepareIssueViewSidebarDevLinks,
prepareIssueViewSidebarWatch,
prepareIssueViewSidebarTimeTracker,
prepareIssueViewSidebarDependency,
@ -1007,3 +1008,50 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
return
}
}
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
}

View File

@ -1275,7 +1275,8 @@ func registerWebRoutes(m *web.Router) {
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
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/remove", repo.DeleteAttachment)

View File

@ -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

View File

@ -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
}

View File

@ -286,6 +286,16 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, erro
issue.MilestoneID, err)
}
if issue.IsPull {
if err := issue.LoadPullRequest(ctx); err != nil {
return nil, err
}
if _, err := db.GetEngine(ctx).Where("link_type = ? AND link_id = ?", issues_model.IssueDevLinkTypePullRequest, issue.PullRequest.ID).
Delete(new(issues_model.IssueDevLink)); err != nil {
return nil, err
}
}
if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil {
return nil, err
}
@ -320,6 +330,7 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, erro
&issues_model.IssueDependency{DependencyID: issue.ID},
&issues_model.Comment{DependentIssueID: issue.ID},
&issues_model.IssuePin{IssueID: issue.ID},
&issues_model.IssueDevLink{IssueID: issue.ID},
); err != nil {
return nil, err
}

View File

@ -127,6 +127,35 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
return err
}
// add dev links
if pr.Flow == issues_model.PullRequestFlowGithub {
branch, err := git_model.GetBranch(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
return err
}
devLinks := make(issues_model.IssueDevLinks, 0, 5)
if err := db.GetEngine(ctx).
Join("INNER", "issue", "issue_dev_link.issue_id = issue.id").
Where("link_type = ? AND link_id = ? AND linked_repo_id = ?",
issues_model.IssueDevLinkTypeBranch, branch.ID, issue.RepoID).
And("issue.repo_id=?", pr.HeadRepoID).
Find(&devLinks); 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,
LinkID: pr.ID,
}); err != nil {
return err
}
}
}
// Update Commit Divergence
err = syncCommitDivergence(ctx, pr)
if err != nil {

View File

@ -512,6 +512,22 @@ func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchNam
return nil
}
func DeleteIssueDevLinkByBranchName(ctx context.Context, repoID int64, branchName string) error {
awBranch, err := git_model.GetBranch(ctx, repoID, branchName)
if err != nil && !git_model.IsErrBranchNotExist(err) {
return fmt.Errorf("GetBranch: %vc", err)
}
if awBranch == nil {
return nil
}
_, err = db.GetEngine(ctx).
Where("linked_repo_id = ? AND link_type = ? AND link_id = ?",
repoID, issues_model.IssueDevLinkTypeBranch, awBranch.ID).
Delete(new(issues_model.IssueDevLink))
return err
}
// DeleteBranch delete branch
func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string, pr *issues_model.PullRequest) error {
err := repo.MustNotBeArchived()
@ -543,6 +559,9 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
}
}
if err := DeleteIssueDevLinkByBranchName(ctx, repo.ID, branchName); err != nil {
return err
}
if pr != nil {
if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
return fmt.Errorf("DeleteBranch: %v", err)

View File

@ -188,17 +188,18 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
return err
}
// Delete Pulls and related objects
if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil {
return err
}
// Delete Issues and related objects
var attachmentPaths []string
if attachmentPaths, err = issue_service.DeleteIssuesByRepoID(ctx, repoID); err != nil {
return err
}
// Delete Pulls and related objects
// Notice: we should delete issue first because issue may load pull request
if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil {
return err
}
// Delete issue index
if err := db.DeleteResourceIndex(ctx, "issue_index", repoID); err != nil {
return err

View 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}}

View File

@ -15,6 +15,7 @@
{{end}}
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/development" .}}
{{template "repo/issue/sidebar/participant_list" $}}
{{template "repo/issue/sidebar/watch_notification" $}}
{{template "repo/issue/sidebar/stopwatch_timetracker" $}}

View File

@ -133,6 +133,10 @@
·
{{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}}
</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}}
</div>
</div>

View File

@ -303,7 +303,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 6)
assert.Len(t, branches, 7)
// make a broke repository with no branch on database
_, err = db.DeleteByBean(t.Context(), git_model.Branch{RepoID: 1})
@ -320,7 +320,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 7)
assert.Len(t, branches, 8)
branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
RepoID: 1,

View File

@ -342,9 +342,15 @@ func TestCantMergeUnrelated(t *testing.T) {
assert.NoError(t, err)
commitSha := strings.TrimSpace(stdout.String())
gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1)
assert.NoError(t, err)
defer gitRepo1.Close()
_, _, err = gitcmd.NewCommand("branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: path})
assert.NoError(t, err)
assert.NoError(t, repo_service.CreateNewBranchFromCommit(t.Context(), user1, repo1, gitRepo1, commitSha, "unrelated"))
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
// Use API to create a conflicting pr
@ -357,8 +363,6 @@ func TestCantMergeUnrelated(t *testing.T) {
session.MakeRequest(t, req, http.StatusCreated)
// Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo1)
assert.NoError(t, err)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
HeadRepoID: repo1.ID,
BaseRepoID: repo1.ID,
@ -366,10 +370,9 @@ func TestCantMergeUnrelated(t *testing.T) {
BaseBranch: "base",
})
err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED", false)
err = pull_service.Merge(t.Context(), pr, user1, gitRepo1, repo_model.MergeStyleMerge, "", "UNRELATED", false)
assert.Error(t, err, "Merge should return an error due to unrelated")
assert.True(t, pull_service.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
gitRepo.Close()
})
}