Defer Last Commit Info (#16467)

One of the biggest reasons for slow repository browsing is that we wait
until last commit information has been generated for all files in the
repository.

This PR proposes deferring this generation to a new POST endpoint that
does the look up outside of the main page request.

Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
zeripath 2021-10-08 14:08:22 +01:00 committed by GitHub
parent 88fa9f3fb1
commit 001dbf100d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 321 additions and 151 deletions

View File

@ -44,20 +44,17 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
return nil, nil, err return nil, nil, err
} }
if len(unHitPaths) > 0 { if len(unHitPaths) > 0 {
revs2, err := GetLastCommitForPaths(ctx, c, treePath, unHitPaths) revs2, err := GetLastCommitForPaths(ctx, cache, c, treePath, unHitPaths)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for k, v := range revs2 { for k, v := range revs2 {
if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
return nil, nil, err
}
revs[k] = v revs[k] = v
} }
} }
} else { } else {
revs, err = GetLastCommitForPaths(ctx, c, treePath, entryPaths) revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths)
} }
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -70,25 +67,29 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
commitsInfo[i] = CommitInfo{ commitsInfo[i] = CommitInfo{
Entry: entry, Entry: entry,
} }
// Check if we have found a commit for this entry in time
if rev, ok := revs[entry.Name()]; ok { if rev, ok := revs[entry.Name()]; ok {
entryCommit := convertCommit(rev) entryCommit := convertCommit(rev)
commitsInfo[i].Commit = entryCommit commitsInfo[i].Commit = entryCommit
if entry.IsSubModule() { }
subModuleURL := ""
var fullPath string // If the entry if a submodule add a submodule file for this
if len(treePath) > 0 { if entry.IsSubModule() {
fullPath = treePath + "/" + entry.Name() subModuleURL := ""
} else { var fullPath string
fullPath = entry.Name() if len(treePath) > 0 {
} fullPath = treePath + "/" + entry.Name()
if subModule, err := commit.GetSubModule(fullPath); err != nil { } else {
return nil, nil, err fullPath = entry.Name()
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
commitsInfo[i].SubModuleFile = subModuleFile
} }
if subModule, err := commit.GetSubModule(fullPath); err != nil {
return nil, nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
commitsInfo[i].SubModuleFile = subModuleFile
} }
} }
@ -175,7 +176,9 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac
} }
// GetLastCommitForPaths returns last commit information // GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) { func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
refSha := c.ID().String()
// We do a tree traversal with nodes sorted by commit time // We do a tree traversal with nodes sorted by commit time
heap := binaryheap.NewWith(func(a, b interface{}) int { heap := binaryheap.NewWith(func(a, b interface{}) int {
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) { if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
@ -192,10 +195,13 @@ func GetLastCommitForPaths(ctx context.Context, c cgobject.CommitNode, treePath
// Start search from the root commit and with full set of paths // Start search from the root commit and with full set of paths
heap.Push(&commitAndPaths{c, paths, initialHashes}) heap.Push(&commitAndPaths{c, paths, initialHashes})
heaploop:
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
break heaploop
}
return nil, ctx.Err() return nil, ctx.Err()
default: default:
} }
@ -233,14 +239,14 @@ func GetLastCommitForPaths(ctx context.Context, c cgobject.CommitNode, treePath
} }
var remainingPaths []string var remainingPaths []string
for i, path := range current.paths { for i, pth := range current.paths {
// The results could already contain some newer change for the same path, // The results could already contain some newer change for the same path,
// so don't override that and bail out on the file early. // so don't override that and bail out on the file early.
if resultNodes[path] == nil { if resultNodes[pth] == nil {
if pathUnchanged[i] { if pathUnchanged[i] {
// The path existed with the same hash in at least one parent so it could // The path existed with the same hash in at least one parent so it could
// not have been changed in this commit directly. // not have been changed in this commit directly.
remainingPaths = append(remainingPaths, path) remainingPaths = append(remainingPaths, pth)
} else { } else {
// There are few possible cases how can we get here: // There are few possible cases how can we get here:
// - The path didn't exist in any parent, so it must have been created by // - The path didn't exist in any parent, so it must have been created by
@ -250,7 +256,10 @@ func GetLastCommitForPaths(ctx context.Context, c cgobject.CommitNode, treePath
// - We are looking at a merge commit and the hash of the file doesn't // - We are looking at a merge commit and the hash of the file doesn't
// match any of the hashes being merged. This is more common for directories, // match any of the hashes being merged. This is more common for directories,
// but it can also happen if a file is changed through conflict resolution. // but it can also happen if a file is changed through conflict resolution.
resultNodes[path] = current.commit resultNodes[pth] = current.commit
if err := cache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
return nil, err
}
} }
} }
} }

View File

@ -37,21 +37,18 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
} }
if len(unHitPaths) > 0 { if len(unHitPaths) > 0 {
sort.Strings(unHitPaths) sort.Strings(unHitPaths)
commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths) commits, err := GetLastCommitForPaths(ctx, cache, commit, treePath, unHitPaths)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for pth, found := range commits { for pth, found := range commits {
if err := cache.Put(commit.ID.String(), path.Join(treePath, pth), found.ID.String()); err != nil {
return nil, nil, err
}
revs[pth] = found revs[pth] = found
} }
} }
} else { } else {
sort.Strings(entryPaths) sort.Strings(entryPaths)
revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths) revs, err = GetLastCommitForPaths(ctx, nil, commit, treePath, entryPaths)
} }
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -62,27 +59,31 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
commitsInfo[i] = CommitInfo{ commitsInfo[i] = CommitInfo{
Entry: entry, Entry: entry,
} }
// Check if we have found a commit for this entry in time
if entryCommit, ok := revs[entry.Name()]; ok { if entryCommit, ok := revs[entry.Name()]; ok {
commitsInfo[i].Commit = entryCommit commitsInfo[i].Commit = entryCommit
if entry.IsSubModule() {
subModuleURL := ""
var fullPath string
if len(treePath) > 0 {
fullPath = treePath + "/" + entry.Name()
} else {
fullPath = entry.Name()
}
if subModule, err := commit.GetSubModule(fullPath); err != nil {
return nil, nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewSubModuleFile(entryCommit, subModuleURL, entry.ID.String())
commitsInfo[i].SubModuleFile = subModuleFile
}
} else { } else {
log.Debug("missing commit for %s", entry.Name()) log.Debug("missing commit for %s", entry.Name())
} }
// If the entry if a submodule add a submodule file for this
if entry.IsSubModule() {
subModuleURL := ""
var fullPath string
if len(treePath) > 0 {
fullPath = treePath + "/" + entry.Name()
} else {
fullPath = entry.Name()
}
if subModule, err := commit.GetSubModule(fullPath); err != nil {
return nil, nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
commitsInfo[i].SubModuleFile = subModuleFile
}
} }
// Retrieve the commit for the treePath itself (see above). We basically // Retrieve the commit for the treePath itself (see above). We basically
@ -121,9 +122,9 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
} }
// GetLastCommitForPaths returns last commit information // GetLastCommitForPaths returns last commit information
func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
// We read backwards from the commit to obtain all of the commits // We read backwards from the commit to obtain all of the commits
revs, err := WalkGitLog(ctx, commit.repo, commit, treePath, paths...) revs, err := WalkGitLog(ctx, cache, commit.repo, commit, treePath, paths...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -26,6 +26,9 @@ func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string {
// Put put the last commit id with commit and entry path // Put put the last commit id with commit and entry path
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
if c == nil || c.cache == nil {
return nil
}
log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl()) return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl())
} }

View File

@ -9,7 +9,6 @@ package git
import ( import (
"context" "context"
"path"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -93,15 +92,12 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, index cgobject.Com
entryMap[entry.Name()] = entry entryMap[entry.Name()] = entry
} }
commits, err := GetLastCommitForPaths(ctx, index, treePath, entryPaths) commits, err := GetLastCommitForPaths(ctx, c, index, treePath, entryPaths)
if err != nil { if err != nil {
return err return err
} }
for entry, cm := range commits { for entry := range commits {
if err := c.Put(index.ID().String(), path.Join(treePath, entry), cm.ID().String()); err != nil {
return err
}
if entryMap[entry].IsDir() { if entryMap[entry].IsDir() {
subTree, err := tree.SubTree(entry) subTree, err := tree.SubTree(entry)
if err != nil { if err != nil {

View File

@ -10,7 +10,6 @@ package git
import ( import (
"bufio" "bufio"
"context" "context"
"path"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
) )
@ -80,28 +79,23 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tr
} }
entryPaths := make([]string, len(entries)) entryPaths := make([]string, len(entries))
entryMap := make(map[string]*TreeEntry)
for i, entry := range entries { for i, entry := range entries {
entryPaths[i] = entry.Name() entryPaths[i] = entry.Name()
entryMap[entry.Name()] = entry
} }
commits, err := GetLastCommitForPaths(ctx, commit, treePath, entryPaths) _, err = WalkGitLog(ctx, c, commit.repo, commit, treePath, entryPaths...)
if err != nil { if err != nil {
return err return err
} }
for entry, entryCommit := range commits { for _, treeEntry := range entries {
if err := c.Put(commit.ID.String(), path.Join(treePath, entry), entryCommit.ID.String()); err != nil {
return err
}
// entryMap won't contain "" therefore skip this. // entryMap won't contain "" therefore skip this.
if treeEntry := entryMap[entry]; treeEntry != nil && treeEntry.IsDir() { if treeEntry.IsDir() {
subTree, err := tree.SubTree(entry) subTree, err := tree.SubTree(treeEntry.Name())
if err != nil { if err != nil {
return err return err
} }
if err := c.recursiveCache(ctx, commit, subTree, entry, level-1); err != nil { if err := c.recursiveCache(ctx, commit, subTree, treeEntry.Name(), level-1); err != nil {
return err return err
} }
} }

View File

@ -275,7 +275,9 @@ func (g *LogNameStatusRepoParser) Close() {
} }
// WalkGitLog walks the git log --name-status for the head commit in the provided treepath and files // WalkGitLog walks the git log --name-status for the head commit in the provided treepath and files
func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) { func WalkGitLog(ctx context.Context, cache *LastCommitCache, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) {
headRef := head.ID.String()
tree, err := head.SubTree(treepath) tree, err := head.SubTree(treepath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -339,6 +341,9 @@ heaploop:
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
break heaploop
}
g.Close() g.Close()
return nil, ctx.Err() return nil, ctx.Err()
default: default:
@ -360,10 +365,16 @@ heaploop:
changed[i] = false changed[i] = false
if results[i] == "" { if results[i] == "" {
results[i] = current.CommitID results[i] = current.CommitID
if err := cache.Put(headRef, path.Join(treepath, paths[i]), current.CommitID); err != nil {
return nil, err
}
delete(path2idx, paths[i]) delete(path2idx, paths[i])
remaining-- remaining--
if results[0] == "" { if results[0] == "" {
results[0] = current.CommitID results[0] = current.CommitID
if err := cache.Put(headRef, treepath, current.CommitID); err != nil {
return nil, err
}
delete(path2idx, "") delete(path2idx, "")
remaining-- remaining--
} }

View File

@ -17,6 +17,7 @@ import (
) )
// GetNote retrieves the git-notes data for a given commit. // GetNote retrieves the git-notes data for a given commit.
// FIXME: Add LastCommitCache support
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
notes, err := repo.GetCommit(NotesRef) notes, err := repo.GetCommit(NotesRef)
@ -75,7 +76,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
return err return err
} }
lastCommits, err := GetLastCommitForPaths(ctx, commitNode, "", []string{path}) lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path})
if err != nil { if err != nil {
log.Error("Unable to get the commit for the path %q. Error: %v", path, err) log.Error("Unable to get the commit for the path %q. Error: %v", path, err)
return err return err

View File

@ -16,6 +16,7 @@ import (
) )
// GetNote retrieves the git-notes data for a given commit. // GetNote retrieves the git-notes data for a given commit.
// FIXME: Add LastCommitCache support
func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error {
log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path)
notes, err := repo.GetCommit(NotesRef) notes, err := repo.GetCommit(NotesRef)
@ -75,7 +76,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
path = path[idx+1:] path = path[idx+1:]
} }
lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path}) lastCommits, err := GetLastCommitForPaths(ctx, nil, notes, treePath, []string{path})
if err != nil { if err != nil {
log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err) log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err)
return err return err

View File

@ -7,6 +7,7 @@ package repo
import ( import (
"bytes" "bytes"
gocontext "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
gotemplate "html/template" gotemplate "html/template"
@ -16,6 +17,7 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -34,11 +36,12 @@ import (
) )
const ( const (
tplRepoEMPTY base.TplName = "repo/empty" tplRepoEMPTY base.TplName = "repo/empty"
tplRepoHome base.TplName = "repo/home" tplRepoHome base.TplName = "repo/home"
tplWatchers base.TplName = "repo/watchers" tplRepoViewList base.TplName = "repo/view_list"
tplForks base.TplName = "repo/forks" tplWatchers base.TplName = "repo/watchers"
tplMigrating base.TplName = "repo/migrate/migrating" tplForks base.TplName = "repo/forks"
tplMigrating base.TplName = "repo/migrate/migrating"
) )
type namedBlob struct { type namedBlob struct {
@ -128,28 +131,8 @@ func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, err
} }
func renderDirectory(ctx *context.Context, treeLink string) { func renderDirectory(ctx *context.Context, treeLink string) {
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) entries := renderDirectoryFiles(ctx, 1*time.Second)
if err != nil { if ctx.Written() {
ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
return
}
entries, err := tree.ListEntries()
if err != nil {
ctx.ServerError("ListEntries", err)
return
}
entries.CustomSort(base.NaturalSortLess)
var c *git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
}
var latestCommit *git.Commit
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return return
} }
@ -186,6 +169,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
isSymlink := entry.IsLink() isSymlink := entry.IsLink()
target := entry target := entry
if isSymlink { if isSymlink {
var err error
target, err = entry.FollowLinks() target, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) { if err != nil && !git.IsErrBadLink(err) {
ctx.ServerError("FollowLinks", err) ctx.ServerError("FollowLinks", err)
@ -207,6 +191,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
name := entry.Name() name := entry.Name()
isSymlink := entry.IsLink() isSymlink := entry.IsLink()
if isSymlink { if isSymlink {
var err error
entry, err = entry.FollowLinks() entry, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) { if err != nil && !git.IsErrBadLink(err) {
ctx.ServerError("FollowLinks", err) ctx.ServerError("FollowLinks", err)
@ -237,6 +222,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
if entry == nil { if entry == nil {
continue continue
} }
var err error
readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName()) readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName())
if err != nil { if err != nil {
ctx.ServerError("getReadmeFileFromPath", err) ctx.ServerError("getReadmeFileFromPath", err)
@ -365,34 +351,12 @@ func renderDirectory(ctx *context.Context, treeLink string) {
} }
} }
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
verification := models.ParseCommitWithSignature(latestCommit)
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), db.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses
// Check permission to add or upload new file. // Check permission to add or upload new file.
if ctx.Repo.CanWrite(models.UnitTypeCode) && ctx.Repo.IsViewBranch { if ctx.Repo.CanWrite(models.UnitTypeCode) && ctx.Repo.IsViewBranch {
ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived
ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
} }
ctx.Data["SSHDomain"] = setting.SSH.Domain
} }
func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) {
@ -614,8 +578,7 @@ func safeURL(address string) string {
return u.String() return u.String()
} }
// Home render repository home page func checkHomeCodeViewable(ctx *context.Context) {
func Home(ctx *context.Context) {
if len(ctx.Repo.Units) > 0 { if len(ctx.Repo.Units) > 0 {
if ctx.Repo.Repository.IsBeingCreated() { if ctx.Repo.Repository.IsBeingCreated() {
task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) task, err := models.GetMigratingTask(ctx.Repo.Repository.ID)
@ -648,7 +611,6 @@ func Home(ctx *context.Context) {
var firstUnit *models.Unit var firstUnit *models.Unit
for _, repoUnit := range ctx.Repo.Units { for _, repoUnit := range ctx.Repo.Units {
if repoUnit.Type == models.UnitTypeCode { if repoUnit.Type == models.UnitTypeCode {
renderCode(ctx)
return return
} }
@ -667,6 +629,145 @@ func Home(ctx *context.Context) {
ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo"))) ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
} }
// Home render repository home page
func Home(ctx *context.Context) {
checkHomeCodeViewable(ctx)
if ctx.Written() {
return
}
renderCode(ctx)
}
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
func LastCommit(ctx *context.Context) {
checkHomeCodeViewable(ctx)
if ctx.Written() {
return
}
renderDirectoryFiles(ctx, 0)
if ctx.Written() {
return
}
var treeNames []string
paths := make([]string, 0, 5)
if len(ctx.Repo.TreePath) > 0 {
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]
}
}
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
ctx.Data["BranchLink"] = branchLink
ctx.HTML(http.StatusOK, tplRepoViewList)
}
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
return nil
}
ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + ctx.Repo.CommitID + "/" + ctx.Repo.TreePath
// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
return nil
}
if !entry.IsDir() {
ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
return nil
}
allEntries, err := tree.ListEntries()
if err != nil {
ctx.ServerError("ListEntries", err)
return nil
}
allEntries.CustomSort(base.NaturalSortLess)
commitInfoCtx := gocontext.Context(ctx)
if timeout > 0 {
var cancel gocontext.CancelFunc
commitInfoCtx, cancel = gocontext.WithTimeout(ctx, timeout)
defer cancel()
}
var c *git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
}
selected := map[string]bool{}
for _, pth := range ctx.FormStrings("f[]") {
selected[pth] = true
}
entries := allEntries
if len(selected) > 0 {
entries = make(git.Entries, 0, len(selected))
for _, entry := range allEntries {
if selected[entry.Name()] {
entries = append(entries, entry)
}
}
}
var latestCommit *git.Commit
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return nil
}
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
if latestCommit != nil {
verification := models.ParseCommitWithSignature(latestCommit)
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return nil
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
}
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), db.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + ctx.Repo.TreePath
}
ctx.Data["TreeLink"] = treeLink
ctx.Data["SSHDomain"] = setting.SSH.Domain
return allEntries
}
func renderLanguageStats(ctx *context.Context) { func renderLanguageStats(ctx *context.Context) {
langs, err := ctx.Repo.Repository.GetTopLanguageStats(5) langs, err := ctx.Repo.Repository.GetTopLanguageStats(5)
if err != nil { if err != nil {

View File

@ -995,6 +995,9 @@ func RegisterRoutes(m *web.Route) {
m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}", m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}",
repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
}, ignSignIn, context.RepoAssignment, context.UnitTypes()) }, ignSignIn, context.RepoAssignment, context.UnitTypes())
m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit)
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Get("/stars", repo.Stars) m.Get("/stars", repo.Stars)
m.Get("/watchers", repo.Watchers) m.Get("/watchers", repo.Watchers)

View File

@ -1,36 +1,40 @@
<table id="repo-files-table" class="ui single line table"> <table id="repo-files-table" class="ui single line table" data-last-commit-loader-url="{{.LastCommitLoaderURL}}">
<thead> <thead>
<tr class="commit-list"> <tr class="commit-list">
<th colspan="2"> <th colspan="2" {{if not .LatestCommit}}class="notready"{{end}}>
{{if .LatestCommitUser}} {{if not .LatestCommit}}
{{avatar .LatestCommitUser 24}} <div class="ui active tiny slow centered inline">…</div>
{{if .LatestCommitUser.FullName}}
<a href="{{AppSubUrl}}/{{.LatestCommitUser.Name}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
{{else}}
<a href="{{AppSubUrl}}/{{.LatestCommitUser.Name}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
{{end}}
{{else}} {{else}}
{{if .LatestCommit.Author}} {{if .LatestCommitUser}}
{{avatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24}} {{avatar .LatestCommitUser 24}}
<strong>{{.LatestCommit.Author.Name}}</strong> {{if .LatestCommitUser.FullName}}
<a href="{{AppSubUrl}}/{{.LatestCommitUser.Name}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
{{else}}
<a href="{{AppSubUrl}}/{{.LatestCommitUser.Name}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
{{end}}
{{else}}
{{if .LatestCommit.Author}}
{{avatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24}}
<strong>{{.LatestCommit.Author.Name}}</strong>
{{end}}
{{end}} {{end}}
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified }} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{.LatestCommit.ID}}">
<span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span>
{{if .LatestCommit.Signature}}
{{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}}
{{end}}
</a>
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses "root" $}}
{{ $commitLink:= printf "%s/commit/%s" .RepoLink .LatestCommit.ID }}
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="basic compact mini ui icon button commit-button"><i class="ellipsis horizontal icon"></i></button>
<pre class="commit-body" style="display: none;">{{RenderCommitBody .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}}
</span>
{{end}} {{end}}
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified }} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{.LatestCommit.ID}}">
<span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span>
{{if .LatestCommit.Signature}}
{{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}}
{{end}}
</a>
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses "root" $}}
{{ $commitLink:= printf "%s/commit/%s" .RepoLink .LatestCommit.ID }}
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="basic compact mini ui icon button commit-button"><i class="ellipsis horizontal icon"></i></button>
<pre class="commit-body" style="display: none;">{{RenderCommitBody .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}}
</span>
</th> </th>
<th class="text grey right age">{{if .LatestCommit.Author}}{{TimeSince .LatestCommit.Author.When $.Lang}}{{end}}</th> <th class="text grey right age">{{if .LatestCommit}}{{if .LatestCommit.Author}}{{TimeSince .LatestCommit.Author.When $.Lang}}{{end}}{{end}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -43,7 +47,7 @@
{{$entry := $item.Entry}} {{$entry := $item.Entry}}
{{$commit := $item.Commit}} {{$commit := $item.Commit}}
{{$subModuleFile := $item.SubModuleFile}} {{$subModuleFile := $item.SubModuleFile}}
<tr> <tr data-entryname="{{$entry.Name}}" data-ready="{{if $commit}}true{{else}}false{{end}}" class="{{if not $commit}}not{{end}}ready entry">
<td class="name four wide"> <td class="name four wide">
<span class="truncate"> <span class="truncate">
{{if $entry.IsSubModule}} {{if $entry.IsSubModule}}
@ -75,10 +79,14 @@
</td> </td>
<td class="message nine wide"> <td class="message nine wide">
<span class="truncate"> <span class="truncate">
<a href="{{$.RepoLink}}/commit/{{$commit.ID}}" title="{{$commit.Summary}}">{{$commit.Summary | RenderEmoji}}</a> {{if $commit}}
<a href="{{$.RepoLink}}/commit/{{$commit.ID}}" title="{{$commit.Summary}}">{{$commit.Summary | RenderEmoji}}</a>
{{else}}
<div class="ui active tiny slow centered inline">…</div>
{{end}}
</span> </span>
</td> </td>
<td class="text right age three wide">{{TimeSince $commit.Committer.When $.Lang}}</td> <td class="text right age three wide">{{if $commit}}{{TimeSince $commit.Committer.When $.Lang}}{{end}}</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>

View File

@ -0,0 +1,40 @@
const {csrf} = window.config;
export async function initLastCommitLoader() {
const entryMap = {};
const entries = $('table#repo-files-table tr.notready')
.map((_, v) => {
entryMap[$(v).attr('data-entryname')] = $(v);
return $(v).attr('data-entryname');
})
.get();
if (entries.length === 0) {
return;
}
const lastCommitLoaderURL = $('table#repo-files-table').data('lastCommitLoaderUrl');
if (entries.length > 200) {
$.post(lastCommitLoaderURL, {
_csrf: csrf,
}, (data) => {
$('table#repo-files-table').replaceWith(data);
});
return;
}
$.post(lastCommitLoaderURL, {
_csrf: csrf,
'f': entries,
}, (data) => {
$(data).find('tr').each((_, row) => {
if (row.className === 'commit-list') {
$('table#repo-files-table .commit-list').replaceWith(row);
return;
}
entryMap[$(row).attr('data-entryname')].replaceWith(row);
});
});
}

View File

@ -20,6 +20,7 @@ import initTableSort from './features/tablesort.js';
import {createCodeEditor, createMonaco} from './features/codeeditor.js'; import {createCodeEditor, createMonaco} from './features/codeeditor.js';
import {initMarkupAnchors} from './markup/anchors.js'; import {initMarkupAnchors} from './markup/anchors.js';
import {initNotificationsTable, initNotificationCount} from './features/notification.js'; import {initNotificationsTable, initNotificationCount} from './features/notification.js';
import {initLastCommitLoader} from './features/lastcommitloader.js';
import {initStopwatch} from './features/stopwatch.js'; import {initStopwatch} from './features/stopwatch.js';
import {showLineButton} from './code/linebutton.js'; import {showLineButton} from './code/linebutton.js';
import {initMarkupContent, initCommentContent} from './markup/content.js'; import {initMarkupContent, initCommentContent} from './markup/content.js';
@ -2864,6 +2865,7 @@ $(document).ready(async () => {
initContextPopups(); initContextPopups();
initTableSort(); initTableSort();
initNotificationsTable(); initNotificationsTable();
initLastCommitLoader();
initPullRequestMergeInstruction(); initPullRequestMergeInstruction();
initFileViewToggle(); initFileViewToggle();
initReleaseEditor(); initReleaseEditor();