diff --git a/modules/git/repo.go b/modules/git/repo.go index 1c223018ad..fc6e6e7acc 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -223,7 +223,7 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { if err != nil { if strings.Contains(stderr, "non-fast-forward") { return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err} - } else if strings.Contains(stderr, "! [remote rejected]") { + } else if strings.Contains(stderr, "! [remote rejected]") || strings.Contains(stderr, "! [rejected]") { err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err} err.GenerateMessage() return err diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e4b8beeeff..1c56dce822 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1946,6 +1946,10 @@ pulls.delete.title = Delete this pull request? pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) pulls.recently_pushed_new_branches = You pushed on branch %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1 = This branch is %d commit behind %s +pulls.upstream_diverging_prompt_behind_n = This branch is %d commits behind %s +pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes +pulls.upstream_diverging_merge = Sync fork pull.deleted_branch = (deleted):%s pull.agit_documentation = Review documentation about AGit diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index dc170742b9..c918cd7a72 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -259,3 +259,20 @@ func CreateBranch(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName) + "/" + util.PathEscapeSegments(form.CurrentPath)) } + +func MergeUpstream(ctx *context.Context) { + branchName := ctx.FormString("branch") + _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Tr("error.not_found")) + return + } else if models.IsErrMergeConflicts(err) { + ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict")) + return + } + ctx.ServerError("MergeUpstream", err) + return + } + ctx.JSONRedirect("") +} diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 03f394d7d8..17c2821824 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -31,7 +31,7 @@ import ( "github.com/nektos/act/pkg/model" ) -func renderFile(ctx *context.Context, entry *git.TreeEntry) { +func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsViewFile"] = true ctx.Data["HideRepoInfo"] = true blob := entry.Blob() diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index d1a50800c1..e0539f53b0 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "html/template" "net/http" @@ -86,29 +87,31 @@ func prepareOpenWithEditorApps(ctx *context.Context) { ctx.Data["OpenWithEditorApps"] = tmplApps } -func prepareHomeSidebarCitationFile(ctx *context.Context, entry *git.TreeEntry) { - if entry.Name() != "" { - return - } - tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) - if err != nil { - HandleGitError(ctx, "Repo.Commit.SubTree", err) - return - } - allEntries, err := tree.ListEntries() - if err != nil { - ctx.ServerError("ListEntries", err) - return - } - for _, entry := range allEntries { - if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { - // Read Citation file contents - if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("checkCitationFile: GetBlobContent: %v", err) - } else { - ctx.Data["CitiationExist"] = true - ctx.PageData["citationFileContent"] = content - break +func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) { + return func(ctx *context.Context) { + if entry.Name() != "" { + return + } + tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "Repo.Commit.SubTree", err) + return + } + allEntries, err := tree.ListEntries() + if err != nil { + ctx.ServerError("ListEntries", err) + return + } + for _, entry := range allEntries { + if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { + // Read Citation file contents + if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("checkCitationFile: GetBlobContent: %v", err) + } else { + ctx.Data["CitiationExist"] = true + ctx.PageData["citationFileContent"] = content + break + } } } } @@ -174,83 +177,21 @@ func prepareHomeSidebarLatestRelease(ctx *context.Context) { } } -func renderHomeCode(ctx *context.Context) { - ctx.Data["PageIsViewCode"] = true - ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled - prepareOpenWithEditorApps(ctx) - - if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { - showEmpty := true - var err error - if ctx.Repo.GitRepo != nil { - showEmpty, err = ctx.Repo.GitRepo.IsEmpty() - if err != nil { - log.Error("GitRepo.IsEmpty: %v", err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - showEmpty = true - ctx.Flash.Error(ctx.Tr("error.occurred"), true) - } - } - if showEmpty { - ctx.HTML(http.StatusOK, tplRepoEMPTY) - return - } - - // the repo is not really empty, so we should update the modal in database - // such problem may be caused by: - // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually - // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. - // it's possible for a repository to be non-empty by that flag but still 500 - // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. - ctx.Repo.Repository.IsEmpty = false - if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { - ctx.ServerError("UpdateRepositoryCols", err) - return - } - if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { - ctx.ServerError("UpdateRepoSize", err) - return - } - - // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values - link := ctx.Link - if ctx.Req.URL.RawQuery != "" { - link += "?" + ctx.Req.URL.RawQuery - } - ctx.Redirect(link) +func prepareUpstreamDivergingInfo(ctx *context.Context) { + if !ctx.Repo.Repository.IsFork || !ctx.Repo.IsViewBranch || ctx.Repo.TreePath != "" { return } - - title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name - if len(ctx.Repo.Repository.Description) > 0 { - title += ": " + ctx.Repo.Repository.Description - } - ctx.Data["Title"] = title - - // Get Topics of this repo - prepareHomeSidebarRepoTopics(ctx) - if ctx.Written() { - return - } - - // Get current entry user currently looking at. - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + upstreamDivergingInfo, err := repo_service.GetUpstreamDivergingInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName) if err != nil { - HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) - return - } - - checkOutdatedBranch(ctx) - - if entry.IsDir() { - prepareToRenderDirectory(ctx) - } else { - renderFile(ctx, entry) - } - if ctx.Written() { + if !errors.Is(err, util.ErrNotExist) && !errors.Is(err, util.ErrInvalidArgument) { + log.Error("GetUpstreamDivergingInfo: %v", err) + } return } + ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo +} +func prepareRecentlyPushedNewBranches(ctx *context.Context) { if ctx.Doer != nil { if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { ctx.ServerError("GetBaseRepo", err) @@ -280,7 +221,112 @@ func renderHomeCode(ctx *context.Context) { } } } +} +func handleRepoEmptyOrBroken(ctx *context.Context) { + showEmpty := true + var err error + if ctx.Repo.GitRepo != nil { + showEmpty, err = ctx.Repo.GitRepo.IsEmpty() + if err != nil { + log.Error("GitRepo.IsEmpty: %v", err) + ctx.Repo.Repository.Status = repo_model.RepositoryBroken + showEmpty = true + ctx.Flash.Error(ctx.Tr("error.occurred"), true) + } + } + if showEmpty { + ctx.HTML(http.StatusOK, tplRepoEMPTY) + return + } + + // the repo is not really empty, so we should update the modal in database + // such problem may be caused by: + // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually + // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. + // it's possible for a repository to be non-empty by that flag but still 500 + // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. + ctx.Repo.Repository.IsEmpty = false + if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { + ctx.ServerError("UpdateRepositoryCols", err) + return + } + if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + ctx.ServerError("UpdateRepoSize", err) + return + } + + // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values + link := ctx.Link + if ctx.Req.URL.RawQuery != "" { + link += "?" + ctx.Req.URL.RawQuery + } + ctx.Redirect(link) +} + +func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { + return func(ctx *context.Context) { + if entry.IsDir() { + prepareToRenderDirectory(ctx) + } else { + prepareToRenderFile(ctx, entry) + } + } +} + +func handleRepoHomeFeed(ctx *context.Context) bool { + if setting.Other.EnableFeed { + isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) + if isFeed { + switch { + case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): + feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) + case ctx.Repo.TreePath == "": + feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) + case ctx.Repo.TreePath != "": + feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) + } + return true + } + } + return false +} + +// Home render repository home page +func Home(ctx *context.Context) { + if handleRepoHomeFeed(ctx) { + return + } + + // Check whether the repo is viewable: not in migration, and the code unit should be enabled + // Ideally the "feed" logic should be after this, but old code did so, so keep it as-is. + checkHomeCodeViewable(ctx) + if ctx.Written() { + return + } + + title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name + if len(ctx.Repo.Repository.Description) > 0 { + title += ": " + ctx.Repo.Repository.Description + } + ctx.Data["Title"] = title + ctx.Data["PageIsViewCode"] = true + ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons + + if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { + // empty or broken repositories need to be handled differently + handleRepoEmptyOrBroken(ctx) + return + } + + // get the current git entry which doer user is currently looking at. + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) + return + } + + // prepare the tree path var treeNames, paths []string branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() treeLink := branchLink @@ -295,57 +341,38 @@ func renderHomeCode(ctx *context.Context) { ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] } } - - isTreePathRoot := ctx.Repo.TreePath == "" - if isTreePathRoot { - prepareHomeSidebarLicenses(ctx) - if ctx.Written() { - return - } - prepareHomeSidebarCitationFile(ctx, entry) - if ctx.Written() { - return - } - - prepareHomeSidebarLanguageStats(ctx) - if ctx.Written() { - return - } - - prepareHomeSidebarLatestRelease(ctx) - if ctx.Written() { - return - } - } - ctx.Data["Paths"] = paths ctx.Data["TreeLink"] = treeLink ctx.Data["TreeNames"] = treeNames ctx.Data["BranchLink"] = branchLink - ctx.HTML(http.StatusOK, tplRepoHome) -} -// Home render repository home page -func Home(ctx *context.Context) { - if setting.Other.EnableFeed { - isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) - if isFeed { - switch { - case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): - feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath == "": - feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath != "": - feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) - } + // some UI components are only shown when the tree path is root + isTreePathRoot := ctx.Repo.TreePath == "" + + prepareFuncs := []func(*context.Context){ + prepareOpenWithEditorApps, + prepareHomeSidebarRepoTopics, + checkOutdatedBranch, + prepareToRenderDirOrFile(entry), + prepareRecentlyPushedNewBranches, + } + + if isTreePathRoot { + prepareFuncs = append(prepareFuncs, + prepareUpstreamDivergingInfo, + prepareHomeSidebarLicenses, + prepareHomeSidebarCitationFile(entry), + prepareHomeSidebarLanguageStats, + prepareHomeSidebarLatestRelease, + ) + } + + for _, prepare := range prepareFuncs { + prepare(ctx) + if ctx.Written() { return } } - checkHomeCodeViewable(ctx) - if ctx.Written() { - return - } - - renderHomeCode(ctx) + ctx.HTML(http.StatusOK, tplRepoHome) } diff --git a/routers/web/web.go b/routers/web/web.go index 85e0fdc41e..c87c01ea0f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1320,6 +1320,7 @@ func registerRoutes(m *web.Router) { m.Post("/delete", repo.DeleteBranchPost) m.Post("/restore", repo.RestoreBranchPost) m.Post("/rename", web.Bind(forms.RenameBranchForm{}), repo_setting.RenameBranchPost) + m.Post("/merge-upstream", repo.MergeUpstream) }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) diff --git a/services/pull/update.go b/services/pull/update.go index 311ffc2442..abf7ad4509 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -65,7 +65,9 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err) } - // use merge functions but switch repos and branches + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment + // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest + // now use a fake reverse PR to switch head&base repos/branches reversePR := &issues_model.PullRequest{ ID: pr.ID, diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go new file mode 100644 index 0000000000..85ca8f7e31 --- /dev/null +++ b/services/repository/merge_upstream.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "fmt" + + git_model "code.gitea.io/gitea/models/git" + issue_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/pull" +) + +type UpstreamDivergingInfo struct { + BaseIsNewer bool + CommitsBehind int + CommitsAhead int +} + +func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { + if err = repo.MustNotBeArchived(); err != nil { + return "", err + } + if err = repo.GetBaseRepo(ctx); err != nil { + return "", err + } + err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{ + Remote: repo.RepoPath(), + Branch: fmt.Sprintf("%s:%s", branch, branch), + Env: repo_module.PushingEnvironment(doer, repo), + }) + if err == nil { + return "fast-forward", nil + } + if !git.IsErrPushOutOfDate(err) && !git.IsErrPushRejected(err) { + return "", err + } + + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment + // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest + fakeIssue := &issue_model.Issue{ + ID: -1, + RepoID: repo.ID, + Repo: repo, + Index: -1, + PosterID: doer.ID, + Poster: doer, + IsPull: true, + } + fakePR := &issue_model.PullRequest{ + ID: -1, + Status: issue_model.PullRequestStatusMergeable, + IssueID: -1, + Issue: fakeIssue, + Index: -1, + HeadRepoID: repo.ID, + HeadRepo: repo, + BaseRepoID: repo.BaseRepo.ID, + BaseRepo: repo.BaseRepo, + HeadBranch: branch, // maybe HeadCommitID is not needed + BaseBranch: branch, + } + fakeIssue.PullRequest = fakePR + err = pull.Update(ctx, fakePR, doer, "merge upstream", false) + if err != nil { + return "", err + } + return "merge", nil +} + +func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { + if !repo.IsFork { + return nil, util.NewInvalidArgumentErrorf("repo is not a fork") + } + + if repo.IsArchived { + return nil, util.NewInvalidArgumentErrorf("repo is archived") + } + + if err := repo.GetBaseRepo(ctx); err != nil { + return nil, err + } + + forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch) + if err != nil { + return nil, err + } + + baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch) + if err != nil { + return nil, err + } + + info := &UpstreamDivergingInfo{} + if forkBranch.CommitID == baseBranch.CommitID { + return info, nil + } + + // TODO: if the fork repo has new commits, this call will fail: + // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb + // so at the moment, we are not able to handle this case, should be improved in the future + diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) + if err != nil { + info.BaseIsNewer = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix + return info, nil + } + info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead + return info, nil +} diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl new file mode 100644 index 0000000000..299ba63e9e --- /dev/null +++ b/templates/repo/code/upstream_diverging_info.tmpl @@ -0,0 +1,18 @@ +{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseIsNewer .UpstreamDivergingInfo.CommitsBehind)}} +
+
+ {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.BranchName|PathEscapeSegments)}} + {{$upstreamHtml := HTMLFormat `%s:%s` $upstreamLink .Repository.BaseRepo.FullName .BranchName}} + {{if .UpstreamDivergingInfo.CommitsBehind}} + {{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}} + {{else}} + {{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}} + {{end}} +
+ {{if .CanWriteCode}} + + {{end}} +
+{{end}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 63bf3eef0f..343425134b 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -136,6 +136,9 @@ {{else if .IsBlame}} {{template "repo/blame" .}} {{else}}{{/* IsViewDirectory */}} + {{if $isTreePathRoot}} + {{template "repo/code/upstream_diverging_info" .}} + {{end}} {{template "repo/view_list" .}} {{end}}