diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 03e21d04b45e4..323374cdd9771 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -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 diff --git a/models/git/branch.go b/models/git/branch.go index 54351649cc5ec..43be70f073e77 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -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) diff --git a/models/issues/issue_dev_link.go b/models/issues/issue_dev_link.go new file mode 100644 index 0000000000000..f9bdabd028a63 --- /dev/null +++ b/models/issues/issue_dev_link.go @@ -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 +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1b1558f39d1ce..e164317fa9654 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 } diff --git a/models/migrations/v1_26/main_test.go b/models/migrations/v1_26/main_test.go new file mode 100644 index 0000000000000..5aa12d553c9f8 --- /dev/null +++ b/models/migrations/v1_26/main_test.go @@ -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) +} diff --git a/models/migrations/v1_26/v323.go b/models/migrations/v1_26/v323.go new file mode 100644 index 0000000000000..ed265f88e930b --- /dev/null +++ b/models/migrations/v1_26/v323.go @@ -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)) +} diff --git a/models/migrations/v1_26/v323_test.go b/models/migrations/v1_26/v323_test.go new file mode 100644 index 0000000000000..81c23a580d136 --- /dev/null +++ b/models/migrations/v1_26/v323_test.go @@ -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)) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 8237a429e508b..8007fb381fc06 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -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. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b3eb7b1f4a4fd..c67317b9c0b0a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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. diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index e8bef7d6c14bb..0174dde1d91b3 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -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) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 9262703078a6a..73b4276b1f707 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -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) } diff --git a/routers/web/repo/issue_dev.go b/routers/web/repo/issue_dev.go new file mode 100644 index 0000000000000..61918a760cba4 --- /dev/null +++ b/routers/web/repo/issue_dev.go @@ -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()) +} diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index d9f6c33e3fbc5..21e8fb7beb692 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -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 +} diff --git a/routers/web/web.go b/routers/web/web.go index 5ee211b576a0c..2578b06df239d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go index 42e6c85c371a8..16ebddf07d74e 100644 --- a/services/forms/repo_branch_form.go +++ b/services/forms/repo_branch_form.go @@ -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 diff --git a/services/issue/dev_link.go b/services/issue/dev_link.go new file mode 100644 index 0000000000000..492a2c524009b --- /dev/null +++ b/services/issue/dev_link.go @@ -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 +} diff --git a/services/issue/issue.go b/services/issue/issue.go index 62b330f8e20be..6ac8bdc95a949 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -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 } diff --git a/services/pull/pull.go b/services/pull/pull.go index 7bf13733b2727..4b997a3421650 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -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 { diff --git a/services/repository/branch.go b/services/repository/branch.go index 5d8178375e033..4e811fecd8ca4 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -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) diff --git a/services/repository/delete.go b/services/repository/delete.go index 040280c8a8d0e..1a8cd77e70841 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -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 diff --git a/templates/repo/issue/sidebar/development.tmpl b/templates/repo/issue/sidebar/development.tmpl new file mode 100644 index 0000000000000..522fd99a32916 --- /dev/null +++ b/templates/repo/issue/sidebar/development.tmpl @@ -0,0 +1,105 @@ +{{if not .Issue.IsPull}} +
+ + {{ctx.Locale.Tr "repo.issues.development"}} + + + +{{end}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 987a882be7be5..4dce72a625c34 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -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" $}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 103fa5de530a9..ff6cfacd3468e 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -133,6 +133,10 @@ · {{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}} + {{if .MaybeFixed}} + {{$fixedStr := HTMLFormat `#%d` .MaybeFixed.Issue.Link .MaybeFixed.Index}} + · {{ctx.Locale.Tr "repo.issues.maybefixed" $fixedStr}} + {{end}} {{end}} diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 12e2b18312a62..08886be87eea9 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -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, diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 72bf8ab4b032d..cce900a6aa516 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -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() }) }