From 0c0445c97a9219f7a9f7f2afd618b31bf958e14f Mon Sep 17 00:00:00 2001
From: Chester Liu <skyline75489@outlook.com>
Date: Tue, 19 Jan 2021 12:07:38 +0800
Subject: [PATCH] Add pager to the branches page (#14202)

* Add pager to the branches page

* override pageSize if bigger than max

* Make branches commit range configurable

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: silverwind <me@silverwind.io>
---
 custom/conf/app.example.ini                   |   4 +
 .../doc/advanced/config-cheat-sheet.en-us.md  |   2 +
 modules/git/repo_commit.go                    |   3 +
 modules/setting/git.go                        |   7 +
 routers/repo/branch.go                        | 231 +++++++++++-------
 templates/repo/branch/list.tmpl               |   1 +
 6 files changed, 160 insertions(+), 88 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 3920f5112c..8921e3a5de 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1098,6 +1098,10 @@ MAX_GIT_DIFF_LINES = 1000
 MAX_GIT_DIFF_LINE_CHARACTERS = 5000
 ; Max number of files shown in diff view
 MAX_GIT_DIFF_FILES = 100
+; Set the default commits range size
+COMMITS_RANGE_SIZE = 50
+; Set the default branches range size
+BRANCHES_RANGE_SIZE = 20
 ; Arguments for command 'git gc', e.g. "--aggressive --auto"
 ; see more on http://git-scm.com/docs/git-gc/
 GC_ARGS =
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 5b86cadd44..50d6a07227 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -752,6 +752,8 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false`
 - `MAX_GIT_DIFF_LINES`: **100**: Max number of lines allowed of a single file in diff view.
 - `MAX_GIT_DIFF_LINE_CHARACTERS`: **5000**: Max character count per line highlighted in diff view.
 - `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
+- `COMMITS_RANGE_SIZE`: **50**: Set the default commits range size
+- `BRANCHES_RANGE_SIZE`: **20**: Set the default branches range size
 - `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`. See more on http://git-scm.com/docs/git-gc/
 - `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1
 - `PULL_REQUEST_PUSH_MESSAGE`: **true**: Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index c31f416628..f02fdde5a8 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -110,6 +110,9 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
 // CommitsRangeSize the default commits range size
 var CommitsRangeSize = 50
 
+// BranchesRangeSize the default branches range size
+var BranchesRangeSize = 20
+
 func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) (*list.List, error) {
 	stdout, err := NewCommand("log", id.String(), "--skip="+strconv.Itoa((page-1)*pageSize),
 		"--max-count="+strconv.Itoa(pageSize), prettyLogFormat).RunInDirBytes(repo.Path)
diff --git a/modules/setting/git.go b/modules/setting/git.go
index 80170e37bf..308d94894b 100644
--- a/modules/setting/git.go
+++ b/modules/setting/git.go
@@ -19,6 +19,8 @@ var (
 		MaxGitDiffLines           int
 		MaxGitDiffLineCharacters  int
 		MaxGitDiffFiles           int
+		CommitsRangeSize          int
+		BranchesRangeSize         int
 		VerbosePush               bool
 		VerbosePushDelay          time.Duration
 		GCArgs                    []string `ini:"GC_ARGS" delim:" "`
@@ -37,6 +39,8 @@ var (
 		MaxGitDiffLines:           1000,
 		MaxGitDiffLineCharacters:  5000,
 		MaxGitDiffFiles:           100,
+		CommitsRangeSize:          50,
+		BranchesRangeSize:         20,
 		VerbosePush:               true,
 		VerbosePushDelay:          5 * time.Second,
 		GCArgs:                    []string{},
@@ -91,5 +95,8 @@ func newGit() {
 		args = append(args, "Version 2") // for focus color
 	}
 
+	git.CommitsRangeSize = Git.CommitsRangeSize
+	git.BranchesRangeSize = Git.BranchesRangeSize
+
 	log.Info(format, args...)
 }
diff --git a/routers/repo/branch.go b/routers/repo/branch.go
index 81e8916af2..bf9f2e6a36 100644
--- a/routers/repo/branch.go
+++ b/routers/repo/branch.go
@@ -52,7 +52,25 @@ func Branches(ctx *context.Context) {
 	ctx.Data["PageIsViewCode"] = true
 	ctx.Data["PageIsBranches"] = true
 
-	ctx.Data["Branches"] = loadBranches(ctx)
+	page := ctx.QueryInt("page")
+	if page <= 1 {
+		page = 1
+	}
+
+	pageSize := ctx.QueryInt("limit")
+	if pageSize <= 0 || pageSize > git.BranchesRangeSize {
+		pageSize = git.BranchesRangeSize
+	}
+
+	branches, branchesCount := loadBranches(ctx, page, pageSize)
+	if ctx.Written() {
+		return
+	}
+	ctx.Data["Branches"] = branches
+	pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5)
+	pager.SetDefaultParams(ctx)
+	ctx.Data["Page"] = pager
+
 	ctx.HTML(200, tplBranch)
 }
 
@@ -176,17 +194,25 @@ func deleteBranch(ctx *context.Context, branchName string) error {
 	return nil
 }
 
-func loadBranches(ctx *context.Context) []*Branch {
+// loadBranches loads branches from the repository limited by page & pageSize.
+// NOTE: May write to context on error. page & pageSize must be > 0
+func loadBranches(ctx *context.Context, page, pageSize int) ([]*Branch, int) {
+	defaultBranch, err := repo_module.GetBranch(ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
+	if err != nil {
+		ctx.ServerError("GetDefaultBranch", err)
+		return nil, 0
+	}
+
 	rawBranches, err := repo_module.GetBranches(ctx.Repo.Repository)
 	if err != nil {
 		ctx.ServerError("GetBranches", err)
-		return nil
+		return nil, 0
 	}
 
 	protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
 	if err != nil {
 		ctx.ServerError("GetProtectedBranches", err)
-		return nil
+		return nil, 0
 	}
 
 	repoIDToRepo := map[int64]*models.Repository{}
@@ -195,100 +221,129 @@ func loadBranches(ctx *context.Context) []*Branch {
 	repoIDToGitRepo := map[int64]*git.Repository{}
 	repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo
 
-	branches := make([]*Branch, len(rawBranches))
-	for i := range rawBranches {
-		commit, err := rawBranches[i].GetCommit()
-		if err != nil {
-			ctx.ServerError("GetCommit", err)
-			return nil
-		}
-
-		var isProtected bool
-		branchName := rawBranches[i].Name
-		for _, b := range protectedBranches {
-			if b.BranchName == branchName {
-				isProtected = true
-				break
-			}
-		}
-
-		divergence, divergenceError := repofiles.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName)
-		if divergenceError != nil {
-			ctx.ServerError("CountDivergingCommits", divergenceError)
-			return nil
-		}
-
-		pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
-		if err != nil {
-			ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
-			return nil
-		}
-		headCommit := commit.ID.String()
-
-		mergeMovedOn := false
-		if pr != nil {
-			pr.HeadRepo = ctx.Repo.Repository
-			if err := pr.LoadIssue(); err != nil {
-				ctx.ServerError("pr.LoadIssue", err)
-				return nil
-			}
-			if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
-				pr.BaseRepo = repo
-			} else if err := pr.LoadBaseRepo(); err != nil {
-				ctx.ServerError("pr.LoadBaseRepo", err)
-				return nil
-			} else {
-				repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
-			}
-			pr.Issue.Repo = pr.BaseRepo
-
-			if pr.HasMerged {
-				baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID]
-				if !ok {
-					baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath())
-					if err != nil {
-						ctx.ServerError("OpenRepository", err)
-						return nil
-					}
-					defer baseGitRepo.Close()
-					repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo
-				}
-				pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
-				if err != nil && !git.IsErrNotExist(err) {
-					ctx.ServerError("GetBranchCommitID", err)
-					return nil
-				}
-				if err == nil && headCommit != pullCommit {
-					// the head has moved on from the merge - we shouldn't delete
-					mergeMovedOn = true
-				}
-			}
-		}
-
-		isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName
-
-		branches[i] = &Branch{
-			Name:              branchName,
-			Commit:            commit,
-			IsProtected:       isProtected,
-			IsIncluded:        isIncluded,
-			CommitsAhead:      divergence.Ahead,
-			CommitsBehind:     divergence.Behind,
-			LatestPullRequest: pr,
-			MergeMovedOn:      mergeMovedOn,
-		}
+	var totalNumOfBranches = len(rawBranches)
+	var startIndex = (page - 1) * pageSize
+	if startIndex > totalNumOfBranches {
+		startIndex = totalNumOfBranches - 1
 	}
+	var endIndex = startIndex + pageSize
+	if endIndex > totalNumOfBranches {
+		endIndex = totalNumOfBranches - 1
+	}
+
+	var branches []*Branch
+	for i := startIndex; i < endIndex; i++ {
+		var branch = loadOneBranch(ctx, rawBranches[i], protectedBranches, repoIDToRepo, repoIDToGitRepo)
+		if branch == nil {
+			return nil, 0
+		}
+
+		if branch.Name == ctx.Repo.Repository.DefaultBranch {
+			// Skip default branch
+			continue
+		}
+
+		branches = append(branches, branch)
+	}
+
+	// Always add the default branch
+	branches = append(branches, loadOneBranch(ctx, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo))
 
 	if ctx.Repo.CanWrite(models.UnitTypeCode) {
 		deletedBranches, err := getDeletedBranches(ctx)
 		if err != nil {
 			ctx.ServerError("getDeletedBranches", err)
-			return nil
+			return nil, 0
 		}
 		branches = append(branches, deletedBranches...)
 	}
 
-	return branches
+	return branches, len(rawBranches) - 1
+}
+
+func loadOneBranch(ctx *context.Context, rawBranch *git.Branch, protectedBranches []*models.ProtectedBranch,
+	repoIDToRepo map[int64]*models.Repository,
+	repoIDToGitRepo map[int64]*git.Repository) *Branch {
+
+	commit, err := rawBranch.GetCommit()
+	if err != nil {
+		ctx.ServerError("GetCommit", err)
+		return nil
+	}
+
+	branchName := rawBranch.Name
+	var isProtected bool
+	for _, b := range protectedBranches {
+		if b.BranchName == branchName {
+			isProtected = true
+			break
+		}
+	}
+
+	divergence, divergenceError := repofiles.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName)
+	if divergenceError != nil {
+		ctx.ServerError("CountDivergingCommits", divergenceError)
+		return nil
+	}
+
+	pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
+	if err != nil {
+		ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
+		return nil
+	}
+	headCommit := commit.ID.String()
+
+	mergeMovedOn := false
+	if pr != nil {
+		pr.HeadRepo = ctx.Repo.Repository
+		if err := pr.LoadIssue(); err != nil {
+			ctx.ServerError("pr.LoadIssue", err)
+			return nil
+		}
+		if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
+			pr.BaseRepo = repo
+		} else if err := pr.LoadBaseRepo(); err != nil {
+			ctx.ServerError("pr.LoadBaseRepo", err)
+			return nil
+		} else {
+			repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
+		}
+		pr.Issue.Repo = pr.BaseRepo
+
+		if pr.HasMerged {
+			baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID]
+			if !ok {
+				baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath())
+				if err != nil {
+					ctx.ServerError("OpenRepository", err)
+					return nil
+				}
+				defer baseGitRepo.Close()
+				repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo
+			}
+			pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+			if err != nil && !git.IsErrNotExist(err) {
+				ctx.ServerError("GetBranchCommitID", err)
+				return nil
+			}
+			if err == nil && headCommit != pullCommit {
+				// the head has moved on from the merge - we shouldn't delete
+				mergeMovedOn = true
+			}
+		}
+	}
+
+	isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName
+	return &Branch{
+		Name:              branchName,
+		Commit:            commit,
+		IsProtected:       isProtected,
+		IsIncluded:        isIncluded,
+		CommitsAhead:      divergence.Ahead,
+		CommitsBehind:     divergence.Behind,
+		LatestPullRequest: pr,
+		MergeMovedOn:      mergeMovedOn,
+	}
 }
 
 func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index cf7c224629..8176738ff6 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -127,6 +127,7 @@
 					</tbody>
 				</table>
 			</div>
+			{{template "base/paginate" .}}
 		{{end}}
 	</div>
 </div>