From daf2a4c047c88083d8820bdee9074357d5c5d7b7 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Wed, 22 May 2024 02:00:35 +0900
Subject: [PATCH] Fix wrong display of recently pushed notification (#25812)

There's a bug in #25715:
If user pushed a commit into another repo with same branch name, the
no-related repo will display the recently pushed notification
incorrectly.
It is simple to fix this, we should match the repo id in the sql query.


![image](https://github.com/go-gitea/gitea/assets/18380374/9411a926-16f1-419e-a1b5-e953af38bab1)
The latest commit is 2 weeks ago.

![image](https://github.com/go-gitea/gitea/assets/18380374/52f9ab22-4999-43ac-a86f-6d36fb1e0411)

The notification comes from another repo with same branch name:

![image](https://github.com/go-gitea/gitea/assets/18380374/a26bc335-8e5b-4b9c-a965-c3dc3fa6f252)


After:
In forked repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/ce6ffc35-deb7-4be7-8b09-184207392f32)
New PR Link will redirect to the original repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/7b98e76f-0c75-494c-9462-80cf9f98e786)
In the original repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/5f6a821b-e51a-4bbd-9980-d9eb94a3c847)
New PR Link:

![image](https://github.com/go-gitea/gitea/assets/18380374/1ce8c879-9f11-4312-8c32-695d7d9af0df)

In the same repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/64b56073-4d0e-40c4-b8a0-80be7a775f69)
New PR Link:

![image](https://github.com/go-gitea/gitea/assets/18380374/96e1b6a3-fb98-40ee-b2ee-648039fb0dcf)

08/15 Update:
Follow #26257, added permission check and logic fix mentioned in
https://github.com/go-gitea/gitea/pull/26257#discussion_r1294085203


2024/04/25 Update:
Fix #30611

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/fixtures/branch.yml                    |  36 +++++
 models/fixtures/issue_index.yml               |   8 +
 models/fixtures/org_user.yml                  |  12 ++
 models/fixtures/repository.yml                |   2 +-
 models/fixtures/team.yml                      |  22 +++
 models/fixtures/team_unit.yml                 |  18 +++
 models/fixtures/team_user.yml                 |  12 ++
 models/fixtures/user.yml                      |   8 +-
 models/git/branch.go                          | 142 ++++++++++++++---
 models/git/branch_list.go                     |  19 +++
 models/organization/org_user_test.go          |   6 +-
 models/repo/repo_list.go                      |   6 +
 routers/web/repo/view.go                      |  26 ++-
 .../code/recently_pushed_new_branches.tmpl    |   4 +-
 tests/integration/api_user_orgs_test.go       |  26 +++
 tests/integration/compare_test.go             |   2 +-
 tests/integration/empty_repo_test.go          |  13 ++
 tests/integration/integration_test.go         |   9 ++
 tests/integration/pull_compare_test.go        |   2 +-
 tests/integration/pull_create_test.go         |   6 +-
 tests/integration/pull_merge_test.go          |  30 ++--
 tests/integration/pull_review_test.go         |   2 +-
 tests/integration/pull_status_test.go         |   6 +-
 tests/integration/repo_activity_test.go       |   2 +-
 tests/integration/repo_branch_test.go         | 148 +++++++++++++++++-
 tests/integration/repo_fork_test.go           |  13 +-
 26 files changed, 508 insertions(+), 72 deletions(-)

diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml
index 93003049c6..c7bdff7733 100644
--- a/models/fixtures/branch.yml
+++ b/models/fixtures/branch.yml
@@ -45,3 +45,39 @@
   is_deleted: false
   deleted_by_id: 0
   deleted_unix: 0
+
+-
+  id: 5
+  repo_id: 10
+  name: 'master'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489927679
+  pusher_id: 12
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
+
+-
+  id: 6
+  repo_id: 10
+  name: 'outdated-new-branch'
+  commit_id: 'cb24c347e328d83c1e0c3c908a6b2c0a2fcb8a3d'
+  commit_message: 'add'
+  commit_time: 1489927679
+  pusher_id: 12
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
+
+-
+  id: 14
+  repo_id: 11
+  name: 'master'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489927679
+  pusher_id: 13
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
diff --git a/models/fixtures/issue_index.yml b/models/fixtures/issue_index.yml
index de6e955804..5aabc08e38 100644
--- a/models/fixtures/issue_index.yml
+++ b/models/fixtures/issue_index.yml
@@ -1,27 +1,35 @@
 -
   group_id: 1
   max_index: 5
+
 -
   group_id: 2
   max_index: 2
+
 -
   group_id: 3
   max_index: 2
+
 -
   group_id: 10
   max_index: 1
+
 -
   group_id: 32
   max_index: 2
+
 -
   group_id: 48
   max_index: 1
+
 -
   group_id: 42
   max_index: 1
+
 -
   group_id: 50
   max_index: 1
+
 -
   group_id: 51
   max_index: 1
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index a7fbcb2c5a..cf21b84aa9 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -117,3 +117,15 @@
   uid: 40
   org_id: 41
   is_public: true
+
+-
+  id: 21
+  uid: 12
+  org_id: 25
+  is_public: true
+
+-
+  id: 22
+  uid: 2
+  org_id: 35
+  is_public: true
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index e5c6224c96..e1f1dd7367 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -327,7 +327,7 @@
   is_archived: false
   is_mirror: false
   status: 0
-  is_fork: false
+  is_fork: true
   fork_id: 10
   is_template: false
   template_id: 0
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index 149fe90888..b549d0589b 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -239,3 +239,25 @@
   num_members: 2
   includes_all_repositories: false
   can_create_org_repo: false
+
+-
+  id: 23
+  org_id: 25
+  lower_name: owners
+  name: Owners
+  authorize: 4 # owner
+  num_repos: 0
+  num_members: 1
+  includes_all_repositories: false
+  can_create_org_repo: true
+
+-
+  id: 24
+  org_id: 35
+  lower_name: team24
+  name: team24
+  authorize: 2 # write
+  num_repos: 0
+  num_members: 1
+  includes_all_repositories: true
+  can_create_org_repo: false
diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml
index de0e8d738b..110019eee3 100644
--- a/models/fixtures/team_unit.yml
+++ b/models/fixtures/team_unit.yml
@@ -322,3 +322,21 @@
   team_id: 22
   type: 3
   access_mode: 1
+
+-
+  id: 55
+  team_id: 18
+  type: 1 # code
+  access_mode: 4
+
+-
+  id: 56
+  team_id: 23
+  type: 1 # code
+  access_mode: 4
+
+-
+  id: 57
+  team_id: 24
+  type: 1 # code
+  access_mode: 2
diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml
index 02d57ae644..6b2d153278 100644
--- a/models/fixtures/team_user.yml
+++ b/models/fixtures/team_user.yml
@@ -147,3 +147,15 @@
   org_id: 41
   team_id: 22
   uid: 39
+
+-
+  id: 26
+  org_id: 25
+  team_id: 23
+  uid: 12
+
+-
+  id: 27
+  org_id: 35
+  team_id: 24
+  uid: 2
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index a3de535508..8504d88ce5 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -918,8 +918,8 @@
   num_following: 0
   num_stars: 0
   num_repos: 0
-  num_teams: 1
-  num_members: 1
+  num_teams: 2
+  num_members: 2
   visibility: 0
   repo_admin_change_team_access: false
   theme: ""
@@ -1289,8 +1289,8 @@
   num_following: 0
   num_stars: 0
   num_repos: 0
-  num_teams: 1
-  num_members: 1
+  num_teams: 2
+  num_members: 2
   visibility: 2
   repo_admin_change_team_access: false
   theme: ""
diff --git a/models/git/branch.go b/models/git/branch.go
index 2979dff3d2..c315d921ff 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -10,9 +10,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -102,8 +104,9 @@ func (err ErrBranchesEqual) Unwrap() error {
 // for pagination, keyword search and filtering
 type Branch struct {
 	ID            int64
-	RepoID        int64  `xorm:"UNIQUE(s)"`
-	Name          string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
+	RepoID        int64                  `xorm:"UNIQUE(s)"`
+	Repo          *repo_model.Repository `xorm:"-"`
+	Name          string                 `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
 	CommitID      string
 	CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line)
 	PusherID      int64
@@ -139,6 +142,14 @@ func (b *Branch) LoadPusher(ctx context.Context) (err error) {
 	return err
 }
 
+func (b *Branch) LoadRepo(ctx context.Context) (err error) {
+	if b.Repo != nil || b.RepoID == 0 {
+		return nil
+	}
+	b.Repo, err = repo_model.GetRepositoryByID(ctx, b.RepoID)
+	return err
+}
+
 func init() {
 	db.RegisterModel(new(Branch))
 	db.RegisterModel(new(RenamedBranch))
@@ -400,24 +411,111 @@ 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
-// except the indicate branch
-func FindRecentlyPushedNewBranches(ctx context.Context, repoID, userID int64, excludeBranchName string) (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("name <> ?", excludeBranchName).
-		And("repo_id = ?", repoID).
-		And("commit_time >= ?", time.Now().Add(-time.Hour*6).Unix()).
-		NotIn("name", subQuery).
-		OrderBy("branch.commit_time DESC").
-		Limit(2).
-		Find(&branches)
-	return branches, err
+type FindRecentlyPushedNewBranchesOptions struct {
+	Repo            *repo_model.Repository
+	BaseRepo        *repo_model.Repository
+	CommitAfterUnix int64
+	MaxCount        int
+}
+
+type RecentlyPushedNewBranch struct {
+	BranchDisplayName string
+	BranchLink        string
+	BranchCompareURL  string
+	CommitTime        timeutil.TimeStamp
+}
+
+// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
+// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
+// if opts.ListOptions is not set, we will only display top 2 latest branch
+func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
+	if doer == nil {
+		return []*RecentlyPushedNewBranch{}, nil
+	}
+
+	// find all related repo ids
+	repoOpts := repo_model.SearchRepoOptions{
+		Actor:      doer,
+		Private:    true,
+		AllPublic:  false, // Include also all public repositories of users and public organisations
+		AllLimited: false, // Include also all public repositories of limited organisations
+		Fork:       optional.Some(true),
+		ForkFrom:   opts.BaseRepo.ID,
+		Archived:   optional.Some(false),
+	}
+	repoCond := repo_model.SearchRepositoryCondition(&repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode))
+	if opts.Repo.ID == opts.BaseRepo.ID {
+		// should also include the base repo's branches
+		repoCond = repoCond.Or(builder.Eq{"id": opts.BaseRepo.ID})
+	} else {
+		// in fork repo, we only detect the fork repo's branch
+		repoCond = repoCond.And(builder.Eq{"id": opts.Repo.ID})
+	}
+	repoIDs := builder.Select("id").From("repository").Where(repoCond)
+
+	if opts.CommitAfterUnix == 0 {
+		opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix()
+	}
+
+	baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	// find all related branches, these branches may already created PRs, we will check later
+	var branches []*Branch
+	if err := db.GetEngine(ctx).
+		Where(builder.And(
+			builder.Eq{
+				"pusher_id":  doer.ID,
+				"is_deleted": false,
+			},
+			builder.Gte{"commit_time": opts.CommitAfterUnix},
+			builder.In("repo_id", repoIDs),
+			// newly created branch have no changes, so skip them
+			builder.Neq{"commit_id": baseBranch.CommitID},
+		)).
+		OrderBy(db.SearchOrderByRecentUpdated.String()).
+		Find(&branches); err != nil {
+		return nil, err
+	}
+
+	newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches))
+	if opts.MaxCount == 0 {
+		// by default we display 2 recently pushed new branch
+		opts.MaxCount = 2
+	}
+	for _, branch := range branches {
+		// whether branch have already created PR
+		count, err := db.GetEngine(ctx).Table("pull_request").
+			// we should not only use branch name here, because if there are branches with same name in other repos,
+			// we can not detect them correctly
+			Where(builder.Eq{"head_repo_id": branch.RepoID, "head_branch": branch.Name}).Count()
+		if err != nil {
+			return nil, err
+		}
+
+		// if no PR, we add to the result
+		if count == 0 {
+			if err := branch.LoadRepo(ctx); err != nil {
+				return nil, err
+			}
+
+			branchDisplayName := branch.Name
+			if branch.Repo.ID != opts.BaseRepo.ID && branch.Repo.ID != opts.Repo.ID {
+				branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
+			}
+			newBranches = append(newBranches, &RecentlyPushedNewBranch{
+				BranchDisplayName: branchDisplayName,
+				BranchLink:        fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
+				BranchCompareURL:  branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
+				CommitTime:        branch.CommitTime,
+			})
+		}
+		if len(newBranches) == opts.MaxCount {
+			break
+		}
+	}
+
+	return newBranches, nil
 }
diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 980bd7b4c9..5c887461d5 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -7,6 +7,7 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/optional"
@@ -59,6 +60,24 @@ func (branches BranchList) LoadPusher(ctx context.Context) error {
 	return nil
 }
 
+func (branches BranchList) LoadRepo(ctx context.Context) error {
+	ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+		return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil
+	})
+
+	reposMap := make(map[int64]*repo_model.Repository, len(ids))
+	if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil {
+		return err
+	}
+	for _, branch := range branches {
+		if branch.RepoID <= 0 || branch.Repo != nil {
+			continue
+		}
+		branch.Repo = reposMap[branch.RepoID]
+	}
+	return nil
+}
+
 type FindBranchOptions struct {
 	db.ListOptions
 	RepoID             int64
diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go
index 7924517f31..cf7acdf83b 100644
--- a/models/organization/org_user_test.go
+++ b/models/organization/org_user_test.go
@@ -81,7 +81,7 @@ func TestUserListIsPublicMember(t *testing.T) {
 		{3, map[int64]bool{2: true, 4: false, 28: true}},
 		{6, map[int64]bool{5: true, 28: true}},
 		{7, map[int64]bool{5: false}},
-		{25, map[int64]bool{24: true}},
+		{25, map[int64]bool{12: true, 24: true}},
 		{22, map[int64]bool{}},
 	}
 	for _, v := range tt {
@@ -108,8 +108,8 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
 		{3, map[int64]bool{2: true, 4: false, 28: false}},
 		{6, map[int64]bool{5: true, 28: false}},
 		{7, map[int64]bool{5: true}},
-		{25, map[int64]bool{24: false}}, // ErrTeamNotExist
-		{22, map[int64]bool{}},          // No member
+		{25, map[int64]bool{12: true, 24: false}}, // ErrTeamNotExist
+		{22, map[int64]bool{}},                    // No member
 	}
 	for _, v := range tt {
 		t.Run(fmt.Sprintf("IsUserOrgOwnerOfOrgId%d", v.orgid), func(t *testing.T) {
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 987c7df9b0..eacc98e222 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -175,6 +175,8 @@ type SearchRepoOptions struct {
 	// True -> include just forks
 	// False -> include just non-forks
 	Fork optional.Option[bool]
+	// If Fork option is True, you can use this option to limit the forks of a special repo by repo id.
+	ForkFrom int64
 	// None -> include templates AND non-templates
 	// True -> include just templates
 	// False -> include just non-templates
@@ -514,6 +516,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 			cond = cond.And(builder.Eq{"is_fork": false})
 		} else {
 			cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
+
+			if opts.ForkFrom > 0 && opts.Fork.Value() {
+				cond = cond.And(builder.Eq{"fork_id": opts.ForkFrom})
+			}
 		}
 	}
 
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index e4e6201c24..e1498c0d58 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -29,6 +29,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	issue_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"
 	user_model "code.gitea.io/gitea/models/user"
@@ -1027,15 +1028,26 @@ func renderHomeCode(ctx *context.Context) {
 			return
 		}
 
-		showRecentlyPushedNewBranches := true
-		if ctx.Repo.Repository.IsMirror ||
-			!ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
-			showRecentlyPushedNewBranches = false
+		opts := &git_model.FindRecentlyPushedNewBranchesOptions{
+			Repo:     ctx.Repo.Repository,
+			BaseRepo: ctx.Repo.Repository,
 		}
-		if showRecentlyPushedNewBranches {
-			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch)
+		if ctx.Repo.Repository.IsFork {
+			opts.BaseRepo = ctx.Repo.Repository.BaseRepo
+		}
+
+		baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
+		if err != nil {
+			ctx.ServerError("GetUserRepoPermission", err)
+			return
+		}
+
+		if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
+			opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
+			baseRepoPerm.CanRead(unit_model.TypePullRequests) {
+			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
 			if err != nil {
-				ctx.ServerError("GetRecentlyPushedBranches", err)
+				ctx.ServerError("FindRecentlyPushedNewBranches", err)
 				return
 			}
 		}
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index b808f413d3..7f613fcba7 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -2,10 +2,10 @@
 	<div class="ui positive message tw-flex tw-items-center">
 		<div class="tw-flex-1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
-			{{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}}
+			{{$branchLink := HTMLFormat `<a href="%s">%s</a>` .BranchLink .BranchDisplayName}}
 			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
 		</div>
-		<a role="button" class="ui compact green button tw-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
+		<a role="button" class="ui compact green button tw-m-0" href="{{.BranchCompareURL}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
 		</a>
 	</div>
diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go
index b6b4b6f2b2..c656ded5ae 100644
--- a/tests/integration/api_user_orgs_test.go
+++ b/tests/integration/api_user_orgs_test.go
@@ -29,6 +29,7 @@ func TestUserOrgs(t *testing.T) {
 
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
 	org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
+	org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"})
 
 	assert.Equal(t, []*api.Organization{
 		{
@@ -55,6 +56,18 @@ func TestUserOrgs(t *testing.T) {
 			Location:    "",
 			Visibility:  "public",
 		},
+		{
+			ID:          35,
+			Name:        org35.Name,
+			UserName:    org35.Name,
+			FullName:    org35.FullName,
+			Email:       org35.Email,
+			AvatarURL:   org35.AvatarLink(db.DefaultContext),
+			Description: "",
+			Website:     "",
+			Location:    "",
+			Visibility:  "private",
+		},
 	}, orgs)
 
 	// user itself should get it's org's he is a member of
@@ -102,6 +115,7 @@ func TestMyOrgs(t *testing.T) {
 	DecodeJSON(t, resp, &orgs)
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
 	org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
+	org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"})
 
 	assert.Equal(t, []*api.Organization{
 		{
@@ -128,5 +142,17 @@ func TestMyOrgs(t *testing.T) {
 			Location:    "",
 			Visibility:  "public",
 		},
+		{
+			ID:          35,
+			Name:        org35.Name,
+			UserName:    org35.Name,
+			FullName:    org35.FullName,
+			Email:       org35.Email,
+			AvatarURL:   org35.AvatarLink(db.DefaultContext),
+			Description: "",
+			Website:     "",
+			Location:    "",
+			Visibility:  "private",
+		},
 	}, orgs)
 }
diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go
index 7fb8dbc332..9f73ac80e2 100644
--- a/tests/integration/compare_test.go
+++ b/tests/integration/compare_test.go
@@ -140,7 +140,7 @@ func TestCompareCodeExpand(t *testing.T) {
 
 		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 		session = loginUser(t, user2.Name)
-		testRepoFork(t, session, user1.Name, repo.Name, user2.Name, "test_blob_excerpt-fork")
+		testRepoFork(t, session, user1.Name, repo.Name, user2.Name, "test_blob_excerpt-fork", "")
 		testCreateBranch(t, session, user2.Name, "test_blob_excerpt-fork", "branch/main", "forked-branch", http.StatusSeeOther)
 		testEditFile(t, session, user2.Name, "test_blob_excerpt-fork", "forked-branch", "README.md", strings.Repeat("a\n", 15)+"CHANGED\n"+strings.Repeat("a\n", 15))
 
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index ea393a6061..002aa5600e 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -6,9 +6,11 @@ package integration
 import (
 	"bytes"
 	"encoding/base64"
+	"fmt"
 	"io"
 	"mime/multipart"
 	"net/http"
+	"net/http/httptest"
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
@@ -24,6 +26,17 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
+	url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
+	req := NewRequestWithValues(t, "POST", url, map[string]string{
+		"_csrf":         GetCSRF(t, session, "/user/settings"),
+		"commit_choice": "direct",
+		"tree_path":     treePath,
+		"content":       content,
+	})
+	return session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
 func TestEmptyRepo(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	subPaths := []string{
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index f9bd352b62..18f415083c 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -485,6 +485,7 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile
 	assert.True(t, result.Valid())
 }
 
+// GetCSRF returns CSRF token from body
 func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
 	t.Helper()
 	req := NewRequest(t, "GET", urlStr)
@@ -492,3 +493,11 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
 	doc := NewHTMLParser(t, resp.Body)
 	return doc.GetCSRF()
 }
+
+// GetCSRFFrom returns CSRF token from body
+func GetCSRFFromCookie(t testing.TB, session *TestSession, urlStr string) string {
+	t.Helper()
+	req := NewRequest(t, "GET", urlStr)
+	session.MakeRequest(t, req, http.StatusOK)
+	return session.GetCookie("_csrf").Value
+}
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index 39d9103dfd..aed699fd20 100644
--- a/tests/integration/pull_compare_test.go
+++ b/tests/integration/pull_compare_test.go
@@ -45,7 +45,7 @@ func TestPullCompare(t *testing.T) {
 		defer tests.PrepareTestEnv(t)()
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
 		testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
 		testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go
index 7add8e1db6..5a06a7817f 100644
--- a/tests/integration/pull_create_test.go
+++ b/tests/integration/pull_create_test.go
@@ -85,7 +85,7 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b
 func TestPullCreate(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
@@ -113,7 +113,7 @@ func TestPullCreate(t *testing.T) {
 func TestPullCreate_TitleEscape(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "<i>XSS PR</i>")
 
@@ -177,7 +177,7 @@ func TestPullBranchDelete(t *testing.T) {
 		defer tests.PrepareTestEnv(t)()
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
 		testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index 979c408388..3e7054c7e8 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -95,7 +95,7 @@ func TestPullMerge(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
@@ -117,7 +117,7 @@ func TestPullRebase(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
@@ -139,7 +139,7 @@ func TestPullRebaseMerge(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
@@ -161,7 +161,7 @@ func TestPullSquash(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
 
@@ -180,7 +180,7 @@ func TestPullSquash(t *testing.T) {
 func TestPullCleanUpAfterMerge(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
@@ -215,7 +215,7 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
 func TestCantMergeWorkInProgress(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
@@ -234,7 +234,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
 func TestCantMergeConflict(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
 
@@ -280,7 +280,7 @@ func TestCantMergeConflict(t *testing.T) {
 func TestCantMergeUnrelated(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
 
 		// Now we want to create a commit on a branch that is totally unrelated to our current head
@@ -375,7 +375,7 @@ func TestCantMergeUnrelated(t *testing.T) {
 func TestFastForwardOnlyMerge(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n")
 
 		// Use API to create a pr from update to master
@@ -416,7 +416,7 @@ func TestFastForwardOnlyMerge(t *testing.T) {
 func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n")
 
@@ -539,7 +539,7 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
 		testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
 
 		respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
@@ -568,7 +568,7 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) {
 func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
 
@@ -599,7 +599,7 @@ func TestPullMergeIndexerNotifier(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		// create a pull request
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull")
 
@@ -676,7 +676,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
 		session := loginUser(t, "user1")
 		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 		forkedName := "repo1-1"
-		testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
+		testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
 		defer func() {
 			testDeleteRepository(t, session, "user1", forkedName)
 		}()
@@ -759,7 +759,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) {
 		session := loginUser(t, "user1")
 		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 		forkedName := "repo1-2"
-		testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
+		testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
 		defer func() {
 			testDeleteRepository(t, session, "user1", forkedName)
 		}()
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index df5d7b38ea..5ecf3ef469 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -186,7 +186,7 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
 		user2Session := loginUser(t, "user2")
 
 		// Have user1 create a fork of repo1.
-		testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1", "")
 
 		t.Run("Submit approve/reject review on merged PR", func(t *testing.T) {
 			// Create a merged PR (made by user1) in the upstream repo1.
diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go
index 80eea34513..26e1baeb11 100644
--- a/tests/integration/pull_status_test.go
+++ b/tests/integration/pull_status_test.go
@@ -23,7 +23,7 @@ import (
 func TestPullCreate_CommitStatus(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
 
 		url := path.Join("user1", "repo1", "compare", "master...status1")
@@ -122,7 +122,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
 	// so we need to have this meta commit also in develop branch.
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1")
 
@@ -147,7 +147,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
 func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
 		url := path.Join("user1", "repo1", "compare", "master...status1")
 		req := NewRequestWithValues(t, "POST", url,
diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go
index 792554db4b..b04560379d 100644
--- a/tests/integration/repo_activity_test.go
+++ b/tests/integration/repo_activity_test.go
@@ -20,7 +20,7 @@ func TestRepoActivity(t *testing.T) {
 		session := loginUser(t, "user1")
 
 		// Create PRs (1 merged & 2 proposed)
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 		elem := strings.Split(test.RedirectURL(resp), "/")
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index baa8da4b75..d1bc9198c3 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -4,26 +4,37 @@
 package integration
 
 import (
+	"fmt"
 	"net/http"
 	"net/url"
 	"path"
 	"strings"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
+	org_model "code.gitea.io/gitea/models/organization"
+	"code.gitea.io/gitea/models/perm"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/PuerkitoBio/goquery"
 	"github.com/stretchr/testify/assert"
 )
 
 func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string {
 	var csrf string
 	if expectedStatus == http.StatusNotFound {
-		csrf = GetCSRF(t, session, path.Join(user, repo, "src/branch/master"))
+		// src/branch/branch_name may not container "_csrf" input,
+		// so we need to get it from cookies not from body
+		csrf = GetCSRFFromCookie(t, session, path.Join(user, repo, "src/branch/master"))
 	} else {
-		csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefSubURL))
+		csrf = GetCSRFFromCookie(t, session, path.Join(user, repo, "src", oldRefSubURL))
 	}
 	req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{
 		"_csrf":           csrf,
@@ -145,3 +156,136 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
 		strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
 	)
 }
+
+func prepareBranch(t *testing.T, session *TestSession, repo *repo_model.Repository) {
+	baseRefSubURL := fmt.Sprintf("branch/%s", repo.DefaultBranch)
+
+	// create branch with no new commit
+	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "no-commit", http.StatusSeeOther)
+
+	// create branch with commit
+	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "new-commit", http.StatusSeeOther)
+	testAPINewFile(t, session, repo.OwnerName, repo.Name, "new-commit", "new-commit.txt", "new-commit")
+
+	// create deleted branch
+	testCreateBranch(t, session, repo.OwnerName, repo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther)
+	testUIDeleteBranch(t, session, repo.OwnerName, repo.Name, "deleted-branch")
+}
+
+func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository, headBranch, title string) string {
+	srcRef := headBranch
+	if baseRepo.ID != headRepo.ID {
+		srcRef = fmt.Sprintf("%s/%s:%s", headRepo.OwnerName, headRepo.Name, headBranch)
+	}
+	resp := testPullCreate(t, session, baseRepo.OwnerName, baseRepo.Name, false, baseRepo.DefaultBranch, srcRef, title)
+	elem := strings.Split(test.RedirectURL(resp), "/")
+	// return pull request ID
+	return elem[4]
+}
+
+func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	// create opening PR
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "opening-pr", http.StatusSeeOther)
+	testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "opening-pr", "opening pr")
+
+	// create closed PR
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "closed-pr", http.StatusSeeOther)
+	prID := testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "closed-pr", "closed pr")
+	testIssueClose(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID)
+
+	// create closed PR with deleted branch
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "closed-pr-deleted", http.StatusSeeOther)
+	prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "closed-pr-deleted", "closed pr with deleted branch")
+	testIssueClose(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID)
+	testUIDeleteBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "closed-pr-deleted")
+
+	// create merged PR
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "merged-pr", http.StatusSeeOther)
+	prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "merged-pr", "merged pr")
+	testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "merged-pr", fmt.Sprintf("new-commit-%s.txt", headRepo.Name), "new-commit")
+	testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, repo_model.MergeStyleRebaseMerge, false)
+
+	// create merged PR with deleted branch
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "merged-pr-deleted", http.StatusSeeOther)
+	prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "merged-pr-deleted", "merged pr with deleted branch")
+	testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "merged-pr-deleted", fmt.Sprintf("new-commit-%s-2.txt", headRepo.Name), "new-commit")
+	testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, repo_model.MergeStyleRebaseMerge, true)
+}
+
+func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath string, expected []string) {
+	branches := make([]string, 0, 2)
+	req := NewRequest(t, "GET", repoPath)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	doc := NewHTMLParser(t, resp.Body)
+	doc.doc.Find(".ui.positive.message div a").Each(func(index int, branch *goquery.Selection) {
+		branches = append(branches, branch.Text())
+	})
+	assert.Equal(t, expected, branches)
+}
+
+func TestRecentlyPushedNewBranches(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user1Session := loginUser(t, "user1")
+		user2Session := loginUser(t, "user2")
+		user12Session := loginUser(t, "user12")
+		user13Session := loginUser(t, "user13")
+
+		// prepare branch and PRs in original repo
+		repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+		prepareBranch(t, user12Session, repo10)
+		prepareRepoPR(t, user12Session, user12Session, repo10, repo10)
+
+		// outdated new branch should not be displayed
+		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"new-commit"})
+
+		// create a fork repo in public org
+		testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", "new-commit")
+		orgPublicForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 25, Name: "org25_fork_repo10"})
+		prepareRepoPR(t, user12Session, user12Session, repo10, orgPublicForkRepo)
+
+		// user12 is the owner of the repo10 and the organization org25
+		// in repo10, user12 has opening/closed/merged pr and closed/merged pr with deleted branch
+		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"org25/org25_fork_repo10:new-commit", "new-commit"})
+
+		userForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
+		testCtx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository)
+		t.Run("AddUser13AsCollaborator", doAPIAddCollaborator(testCtx, "user13", perm.AccessModeWrite))
+		prepareBranch(t, user13Session, userForkRepo)
+		prepareRepoPR(t, user13Session, user13Session, repo10, userForkRepo)
+
+		// create branch with same name in different repo by user13
+		testCreateBranch(t, user13Session, repo10.OwnerName, repo10.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
+		testCreateBranch(t, user13Session, userForkRepo.OwnerName, userForkRepo.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
+		testCreatePullToDefaultBranch(t, user13Session, repo10, userForkRepo, "same-name-branch", "same name branch pr")
+
+		// user13 pushed 2 branches with the same name in repo10 and repo11
+		// and repo11's branch has a pr, but repo10's branch doesn't
+		// in this case, we should get repo10's branch but not repo11's branch
+		checkRecentlyPushedNewBranches(t, user13Session, "user12/repo10", []string{"same-name-branch", "user13/repo11:new-commit"})
+
+		// create a fork repo in private org
+		testRepoFork(t, user1Session, repo10.OwnerName, repo10.Name, "private_org35", "org35_fork_repo10", "new-commit")
+		orgPrivateForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 35, Name: "org35_fork_repo10"})
+		prepareRepoPR(t, user1Session, user1Session, repo10, orgPrivateForkRepo)
+
+		// user1 is the owner of private_org35 and no write permission to repo10
+		// so user1 can only see the branch in org35_fork_repo10
+		checkRecentlyPushedNewBranches(t, user1Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:new-commit"})
+
+		// user2 push a branch in private_org35
+		testCreateBranch(t, user2Session, orgPrivateForkRepo.OwnerName, orgPrivateForkRepo.Name, "branch/new-commit", "user-read-permission", http.StatusSeeOther)
+		// convert write permission to read permission for code unit
+		token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization)
+		req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", 24), &api.EditTeamOption{
+			Name:     "team24",
+			UnitsMap: map[string]string{"repo.code": "read"},
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusOK)
+		teamUnit := unittest.AssertExistsAndLoadBean(t, &org_model.TeamUnit{TeamID: 24, Type: unit.TypeCode})
+		assert.Equal(t, perm.AccessModeRead, teamUnit.AccessMode)
+		// user2 can see the branch as it is created by user2
+		checkRecentlyPushedNewBranches(t, user2Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:user-read-permission"})
+	})
+}
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index ca5d61ecc2..feebebf062 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -16,7 +16,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName string) *httptest.ResponseRecorder {
+func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName, forkBranch string) *httptest.ResponseRecorder {
 	forkOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: forkOwnerName})
 
 	// Step0: check the existence of the to-fork repo
@@ -41,9 +41,10 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 	_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value")
 	assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName))
 	req = NewRequestWithValues(t, "POST", link, map[string]string{
-		"_csrf":     htmlDoc.GetCSRF(),
-		"uid":       fmt.Sprintf("%d", forkOwner.ID),
-		"repo_name": forkRepoName,
+		"_csrf":              htmlDoc.GetCSRF(),
+		"uid":                fmt.Sprintf("%d", forkOwner.ID),
+		"repo_name":          forkRepoName,
+		"fork_single_branch": forkBranch,
 	})
 	session.MakeRequest(t, req, http.StatusSeeOther)
 
@@ -57,13 +58,13 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 func TestRepoFork(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user1")
-	testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+	testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 }
 
 func TestRepoForkToOrg(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user2")
-	testRepoFork(t, session, "user2", "repo1", "org3", "repo1")
+	testRepoFork(t, session, "user2", "repo1", "org3", "repo1", "")
 
 	// Check that no more forking is allowed as user2 owns repository
 	//  and org3 organization that owner user2 is also now has forked this repository