aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-12-07 05:10:35 +0800
committerGitHub <noreply@github.com>2024-12-06 21:10:35 +0000
commit513da407f406b161aca078c8e1158f3394e71ca1 (patch)
treedcec1cb252744c3c85cc0273e025cb7f9079a2ce
parent5a75160c92190eb038d9b0fc71846310e76a5481 (diff)
downloadgitea-513da407f406b161aca078c8e1158f3394e71ca1.tar.gz
gitea-513da407f406b161aca078c8e1158f3394e71ca1.zip
Support "merge upstream branch" (Sync fork) (#32741)
Add basic "sync fork" support (GitHub-like) <details> ![image](https://github.com/user-attachments/assets/e71473f4-4518-48c7-b9e2-fedfcd564fc3) </details>
-rw-r--r--modules/git/repo.go2
-rw-r--r--options/locale/locale_en-US.ini4
-rw-r--r--routers/web/repo/branch.go17
-rw-r--r--routers/web/repo/view_file.go2
-rw-r--r--routers/web/repo/view_home.go293
-rw-r--r--routers/web/web.go1
-rw-r--r--services/pull/update.go4
-rw-r--r--services/repository/merge_upstream.go115
-rw-r--r--templates/repo/code/upstream_diverging_info.tmpl18
-rw-r--r--templates/repo/home.tmpl3
10 files changed, 323 insertions, 136 deletions
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 <strong>%[1]s</strong> %[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)
- 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() {
+func prepareUpstreamDivergingInfo(ctx *context.Context) {
+ if !ctx.Repo.Repository.IsFork || !ctx.Repo.IsViewBranch || ctx.Repo.TreePath != "" {
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,53 +221,60 @@ func renderHomeCode(ctx *context.Context) {
}
}
}
+}
- var treeNames, paths []string
- branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
- treeLink := branchLink
- if ctx.Repo.TreePath != "" {
- treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
- treeNames = strings.Split(ctx.Repo.TreePath, "/")
- for i := range treeNames {
- paths = append(paths, strings.Join(treeNames[:i+1], "/"))
- }
- ctx.Data["HasParentPath"] = true
- if len(paths)-2 >= 0 {
- ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
+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
+ }
- isTreePathRoot := ctx.Repo.TreePath == ""
- if isTreePathRoot {
- prepareHomeSidebarLicenses(ctx)
- if ctx.Written() {
- return
- }
- prepareHomeSidebarCitationFile(ctx, entry)
- if ctx.Written() {
- 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
+ }
- prepareHomeSidebarLanguageStats(ctx)
- if ctx.Written() {
- 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)
+}
- prepareHomeSidebarLatestRelease(ctx)
- if ctx.Written() {
- return
+func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
+ return func(ctx *context.Context) {
+ if entry.IsDir() {
+ prepareToRenderDirectory(ctx)
+ } else {
+ prepareToRenderFile(ctx, entry)
}
}
-
- 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) {
+func handleRepoHomeFeed(ctx *context.Context) bool {
if setting.Other.EnableFeed {
isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req)
if isFeed {
@@ -338,14 +286,93 @@ func Home(ctx *context.Context) {
case ctx.Repo.TreePath != "":
feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
}
- return
+ 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
}
- renderHomeCode(ctx)
+ 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
+ if ctx.Repo.TreePath != "" {
+ treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+ treeNames = strings.Split(ctx.Repo.TreePath, "/")
+ for i := range treeNames {
+ paths = append(paths, strings.Join(treeNames[:i+1], "/"))
+ }
+ ctx.Data["HasParentPath"] = true
+ if len(paths)-2 >= 0 {
+ ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
+ }
+ }
+ ctx.Data["Paths"] = paths
+ ctx.Data["TreeLink"] = treeLink
+ ctx.Data["TreeNames"] = treeNames
+ ctx.Data["BranchLink"] = branchLink
+
+ // 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
+ }
+ }
+
+ 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)}}
+<div class="ui message flex-text-block">
+ <div class="tw-flex-1">
+ {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.BranchName|PathEscapeSegments)}}
+ {{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $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}}
+ </div>
+ {{if .CanWriteCode}}
+ <button class="ui compact green button tw-m-0 link-action" data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
+ {{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
+ </button>
+ {{end}}
+</div>
+{{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}}
</div>