From 6375419468edc95fdfac94aac3b0e10b23743557 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 8 Jul 2023 11:19:00 +0800
Subject: [PATCH] Newly pushed branches hints on repository home page (#25715)

This PR will display a pull request creation hint on the repository home
page when there are newly created branches with no pull request. Only
the recent 6 hours and 2 updated branches will be displayed.

Inspired by #14003
Replace #14003
Resolves #311
Resolves #13196
Resolves #23743

co-authored by @kolaente
---
 models/git/branch.go                          | 21 +++++++++++++++++++
 models/repo/repo.go                           | 12 +++++++++++
 options/locale/locale_en-US.ini               |  2 ++
 routers/web/repo/view.go                      | 12 +++++++++++
 .../code/recently_pushed_new_branches.tmpl    | 11 ++++++++++
 templates/repo/home.tmpl                      |  1 +
 tests/integration/repo_branch_test.go         |  4 ++--
 7 files changed, 61 insertions(+), 2 deletions(-)
 create mode 100644 templates/repo/code/recently_pushed_new_branches.tmpl

diff --git a/models/git/branch.go b/models/git/branch.go
index 5e995449586..97891f01ebb 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -15,6 +15,8 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
 )
 
 // ErrBranchNotExist represents an error that branch with such name does not exist.
@@ -378,3 +380,22 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 
 	return committer.Commit()
 }
+
+// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 6 hours which has no opened PRs created
+func FindRecentlyPushedNewBranches(ctx context.Context, repoID, userID int64) (BranchList, error) {
+	branches := make(BranchList, 0, 2)
+	subQuery := builder.Select("head_branch").From("pull_request").
+		InnerJoin("issue", "issue.id = pull_request.issue_id").
+		Where(builder.Eq{
+			"pull_request.head_repo_id": repoID,
+			"issue.is_closed":           false,
+		})
+	err := db.GetEngine(ctx).
+		Where("pusher_id=? AND is_deleted=?", userID, false).
+		And("updated_unix >= ?", time.Now().Add(-time.Hour*6).Unix()).
+		NotIn("name", subQuery).
+		OrderBy("branch.updated_unix DESC").
+		Limit(2).
+		Find(&branches)
+	return branches, err
+}
diff --git a/models/repo/repo.go b/models/repo/repo.go
index b7c02057c27..3d1f2dcfa8a 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -528,6 +528,18 @@ func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) strin
 	return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID))
 }
 
+func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string {
+	if baseRepo == nil {
+		baseRepo = repo
+	}
+	var cmpBranchEscaped string
+	if repo.ID != baseRepo.ID {
+		cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
+	}
+	cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName))
+	return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped)
+}
+
 // IsOwnedBy returns true when user owns this repository
 func (repo *Repository) IsOwnedBy(userID int64) bool {
 	return repo.OwnerID == userID
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a0901451a25..58fd84308d3 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1767,6 +1767,8 @@ pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull re
 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
+
 milestones.new = New Milestone
 milestones.closed = Closed %s
 milestones.update_ago = Updated %s
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index ad87bae9b8f..acea08d6297 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -977,6 +977,18 @@ func renderCode(ctx *context.Context) {
 		return
 	}
 
+	if ctx.Doer != nil {
+		if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
+			ctx.ServerError("GetBaseRepo", err)
+			return
+		}
+		ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID)
+		if err != nil {
+			ctx.ServerError("GetRecentlyPushedBranches", err)
+			return
+		}
+	}
+
 	var treeNames []string
 	paths := make([]string, 0, 5)
 	if len(ctx.Repo.TreePath) > 0 {
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
new file mode 100644
index 00000000000..e936fa4bb46
--- /dev/null
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -0,0 +1,11 @@
+{{range .RecentlyPushedNewBranches}}
+	<div class="ui positive message gt-df gt-ac">
+		<div class="gt-f1">
+			{{$timeSince := TimeSince .UpdatedUnix.AsTime $.locale}}
+			{{$.locale.Tr "repo.pulls.recently_pushed_new_branches" (PathEscapeSegments .Name) $timeSince | Safe}}
+		</div>
+		<a aria-role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo (PathEscapeSegments .Name)}}">
+			{{$.locale.Tr "repo.pulls.compare_changes"}}
+		</a>
+	</div>
+{{end}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 01ea5d9122f..386908e4246 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -3,6 +3,7 @@
 	{{template "repo/header" .}}
 	<div class="ui container {{if .IsBlame}}fluid padded{{end}}">
 		{{template "base/alert" .}}
+		{{template "repo/code/recently_pushed_new_branches" .}}
 		{{if and (not .HideRepoInfo) (not .IsBlame)}}
 		<div class="ui repo-description">
 			<div id="repo-desc">
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index c56aa43c514..91674ddc82d 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -120,9 +120,9 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
 			req := NewRequest(t, "GET", redirectURL)
 			resp := session.MakeRequest(t, req, http.StatusOK)
 			htmlDoc := NewHTMLParser(t, resp.Body)
-			assert.Equal(t,
-				test.FlashMessage,
+			assert.Contains(t,
 				strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
+				test.FlashMessage,
 			)
 		}
 	}