forked from NYANDEV/forgejo
f174f35644
- The `<title>` element that lives inside the `<head>` element is an important element that gives browsers and search engine crawlers the title of the webpage, hence the element name. It's therefor important that this title is accurate. - Currently there are three issues with titles on repositories. It doesn't use the `FullName` and instead only uses the repository name, this doesn't distinguish which user or organisation the repository is on. It doesn't show the full treepath in the title when visiting an file inside a directory and instead only uses the latest path in treepath. It can show the repository name twice if the `.Title` variable also included the repository name such as on the repository homepage. - Use the repository's fullname (which include which user the repository is on) instead of just their name. - Display the repository's fullname if it isn't already in `.Title`. - Use the full treepath in the repository code view instead of just the last path. - Adds integration tests. - Adds a new repository (`repo59`) that has 3 depths for folders, which wasn't in any other fixture repository yet, so the full treepath for could be properly tested. - Resolves https://codeberg.org/forgejo/forgejo/issues/1276 (cherry picked from commit ff9a6a2cda34cf2b2e392cc47125ed0f619b287b) (cherry picked from commit 76dffc862103eb23d51445ef9d611296308c8413) (cherry picked from commit ff0615b9d0f3ea4bd86a28c4ac5b0c4740230c81) (cherry picked from commit 8712eaa394053a8c8f1f4cb17307e094c65c7059) (cherry picked from commit 0c11587582b8837778ee85f4e3b04241e5d71760) (cherry picked from commit 3cbd9fb7922177106b309f010dd34a68751873dc) Conflicts: tests/integration/repo_test.go https://codeberg.org/forgejo/forgejo/pulls/1512 (cherry picked from commit fbfdba8ae9e7cb9811452b30d5424fca41231a1f) Conflicts: models/fixtures/release.yml https://codeberg.org/forgejo/forgejo/pulls/1550 (cherry picked from commit 8b2bf0534ca6a2241c2a10cbecd7c96fb96558a6) (cherry picked from commit d706d9e222469c689eb069ec609968296657dfdc) (cherry picked from commit 6d46261a3f81d3642b313e76ad93c5f72fbd6bf8) (cherry picked from commit f864d18ad30760bd1e2fb1925b87b19e3208ad97) (cherry picked from commit 80f8620d0d746c7ce5e88eeef3ec62431c399ec8) [GITEA] Improve HTML title on repositories (squash) do not double escape (cherry picked from commit 22882fe25cde57837a31738a10c71c9478e16662) (cherry picked from commit 63e99df3d1ecb50da3b723848ca85d56b831a8d7) (cherry picked from commit b65d777bc78fabf7e3d1bf8c50aff4eb5395d783) (cherry picked from commit 2961f4f6320b4b38c33f33e7133e7f3d3f86bd0f) (cherry picked from commit f7f723628c76c5c2a0678139fbc4264feea352ea) (cherry picked from commit 9ed79158268160f62dc1b32183c9a487cd521ef7) (cherry picked from commit 8b9ead46085b8a7f1a9c63f561bce4795ccca31d) (cherry picked from commit 50eeaf1fbcf01d8616d8ea792a3b3cd736137f89) (cherry picked from commitee6f32820e
) (cherry picked from commitbf337bed35
) (cherry picked from commit 6be9501ec0c6eceda8faa48a4d1dc875da702880) (cherry picked from commit b39860570df95a860c151122a259becb6a221c0e) (cherry picked from commit 3f30f486d516cac043dbdcd780b2277b6a3278d7) (cherry picked from commit 5680ecdbe9b668ce69e5a55b2dd7fb7c0eb7087b) (cherry picked from commit da6a19ad16bd9014ac37e02f10095880baeac65c) (cherry picked from commit 5462493a77dc6f2bf8a0e07e6fbfbe9cce157bcd) (cherry picked from commit 530fe57ddea58aab0d4bfb3b8373a8f4e1632514)
1113 lines
34 KiB
Go
1113 lines
34 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"bytes"
|
|
gocontext "context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html/template"
|
|
"image"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "image/gif" // for processing gif images
|
|
_ "image/jpeg" // for processing jpeg images
|
|
_ "image/png" // for processing png images
|
|
|
|
activities_model "code.gitea.io/gitea/models/activities"
|
|
admin_model "code.gitea.io/gitea/models/admin"
|
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
|
"code.gitea.io/gitea/models/db"
|
|
git_model "code.gitea.io/gitea/models/git"
|
|
issue_model "code.gitea.io/gitea/models/issues"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
unit_model "code.gitea.io/gitea/models/unit"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/actions"
|
|
"code.gitea.io/gitea/modules/base"
|
|
"code.gitea.io/gitea/modules/charset"
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/highlight"
|
|
"code.gitea.io/gitea/modules/lfs"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/markup"
|
|
repo_module "code.gitea.io/gitea/modules/repository"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/typesniffer"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/routers/web/feed"
|
|
issue_service "code.gitea.io/gitea/services/issue"
|
|
|
|
"github.com/nektos/act/pkg/model"
|
|
|
|
_ "golang.org/x/image/bmp" // for processing bmp images
|
|
_ "golang.org/x/image/webp" // for processing webp images
|
|
)
|
|
|
|
const (
|
|
tplRepoEMPTY base.TplName = "repo/empty"
|
|
tplRepoHome base.TplName = "repo/home"
|
|
tplRepoViewList base.TplName = "repo/view_list"
|
|
tplWatchers base.TplName = "repo/watchers"
|
|
tplForks base.TplName = "repo/forks"
|
|
tplMigrating base.TplName = "repo/migrate/migrating"
|
|
)
|
|
|
|
// locate a README for a tree in one of the supported paths.
|
|
//
|
|
// entries is passed to reduce calls to ListEntries(), so
|
|
// this has precondition:
|
|
//
|
|
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
|
|
//
|
|
// FIXME: There has to be a more efficient way of doing this
|
|
func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
|
|
// Create a list of extensions in priority order
|
|
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
|
|
// 2. Txt files - e.g. README.txt
|
|
// 3. No extension - e.g. README
|
|
exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
|
|
extCount := len(exts)
|
|
readmeFiles := make([]*git.TreeEntry, extCount+1)
|
|
|
|
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
|
|
for _, entry := range entries {
|
|
if tryWellKnownDirs && entry.IsDir() {
|
|
// as a special case for the top-level repo introduction README,
|
|
// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
|
|
// (note that docsEntries is ignored unless we are at the root)
|
|
lowerName := strings.ToLower(entry.Name())
|
|
switch lowerName {
|
|
case "docs":
|
|
if entry.Name() == "docs" || docsEntries[0] == nil {
|
|
docsEntries[0] = entry
|
|
}
|
|
case ".gitea":
|
|
if entry.Name() == ".gitea" || docsEntries[1] == nil {
|
|
docsEntries[1] = entry
|
|
}
|
|
case ".github":
|
|
if entry.Name() == ".github" || docsEntries[2] == nil {
|
|
docsEntries[2] = entry
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
|
log.Debug("Potential readme file: %s", entry.Name())
|
|
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
|
|
if entry.IsLink() {
|
|
target, err := entry.FollowLinks()
|
|
if err != nil && !git.IsErrBadLink(err) {
|
|
return "", nil, err
|
|
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
|
readmeFiles[i] = entry
|
|
}
|
|
} else {
|
|
readmeFiles[i] = entry
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var readmeFile *git.TreeEntry
|
|
for _, f := range readmeFiles {
|
|
if f != nil {
|
|
readmeFile = f
|
|
break
|
|
}
|
|
}
|
|
|
|
if ctx.Repo.TreePath == "" && readmeFile == nil {
|
|
for _, subTreeEntry := range docsEntries {
|
|
if subTreeEntry == nil {
|
|
continue
|
|
}
|
|
subTree := subTreeEntry.Tree()
|
|
if subTree == nil {
|
|
// this should be impossible; if subTreeEntry exists so should this.
|
|
continue
|
|
}
|
|
var err error
|
|
childEntries, err := subTree.ListEntries()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
|
|
if err != nil && !git.IsErrNotExist(err) {
|
|
return "", nil, err
|
|
}
|
|
if readmeFile != nil {
|
|
return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", readmeFile, nil
|
|
}
|
|
|
|
func renderDirectory(ctx *context.Context) {
|
|
entries := renderDirectoryFiles(ctx, 1*time.Second)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if ctx.Repo.TreePath != "" {
|
|
ctx.Data["HideRepoInfo"] = true
|
|
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefName)
|
|
}
|
|
|
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
|
|
if err != nil {
|
|
ctx.ServerError("findReadmeFileInEntries", err)
|
|
return
|
|
}
|
|
|
|
renderReadmeFile(ctx, subfolder, readmeFile)
|
|
}
|
|
|
|
// localizedExtensions prepends the provided language code with and without a
|
|
// regional identifier to the provided extension.
|
|
// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
|
|
// Note: ext should be prefixed with a `.`
|
|
func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
|
if len(languageCode) < 1 {
|
|
return []string{ext}
|
|
}
|
|
|
|
lowerLangCode := "." + strings.ToLower(languageCode)
|
|
|
|
if strings.Contains(lowerLangCode, "-") {
|
|
underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
|
|
indexOfDash := strings.Index(lowerLangCode, "-")
|
|
// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
|
|
return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
|
|
}
|
|
|
|
// e.g. [.en.md, .md]
|
|
return []string{lowerLangCode + ext, ext}
|
|
}
|
|
|
|
type fileInfo struct {
|
|
isTextFile bool
|
|
isLFSFile bool
|
|
fileSize int64
|
|
lfsMeta *lfs.Pointer
|
|
st typesniffer.SniffedType
|
|
}
|
|
|
|
func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
|
|
dataRc, err := blob.DataAsync()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
buf := make([]byte, 1024)
|
|
n, _ := util.ReadAtMost(dataRc, buf)
|
|
buf = buf[:n]
|
|
|
|
st := typesniffer.DetectContentType(buf)
|
|
isTextFile := st.IsText()
|
|
|
|
// FIXME: what happens when README file is an image?
|
|
if !isTextFile || !setting.LFS.StartServer {
|
|
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
|
|
}
|
|
|
|
pointer, _ := lfs.ReadPointerFromBuffer(buf)
|
|
if !pointer.IsValid() { // fallback to plain file
|
|
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
|
|
}
|
|
|
|
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
|
|
if err != nil && err != git_model.ErrLFSObjectNotExist { // fallback to plain file
|
|
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
|
|
}
|
|
|
|
dataRc.Close()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
dataRc, err = lfs.ReadMetaObject(pointer)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
buf = make([]byte, 1024)
|
|
n, err = util.ReadAtMost(dataRc, buf)
|
|
if err != nil {
|
|
dataRc.Close()
|
|
return nil, nil, nil, err
|
|
}
|
|
buf = buf[:n]
|
|
|
|
st = typesniffer.DetectContentType(buf)
|
|
|
|
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
|
|
}
|
|
|
|
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
|
|
target := readmeFile
|
|
if readmeFile != nil && readmeFile.IsLink() {
|
|
target, _ = readmeFile.FollowLinks()
|
|
}
|
|
if target == nil {
|
|
// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
|
|
// simply skip rendering the README
|
|
return
|
|
}
|
|
|
|
ctx.Data["RawFileLink"] = ""
|
|
ctx.Data["ReadmeInList"] = true
|
|
ctx.Data["ReadmeExist"] = true
|
|
ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
|
|
|
|
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob())
|
|
if err != nil {
|
|
ctx.ServerError("getFileReader", err)
|
|
return
|
|
}
|
|
defer dataRc.Close()
|
|
|
|
ctx.Data["FileIsText"] = fInfo.isTextFile
|
|
ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
|
|
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
|
|
|
if fInfo.isLFSFile {
|
|
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
|
|
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
|
|
}
|
|
|
|
if !fInfo.isTextFile {
|
|
return
|
|
}
|
|
|
|
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
|
// Pretend that this is a normal text file to display 'This file is too large to be shown'
|
|
ctx.Data["IsFileTooLarge"] = true
|
|
ctx.Data["IsTextFile"] = true
|
|
ctx.Data["FileSize"] = fInfo.fileSize
|
|
return
|
|
}
|
|
|
|
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
|
|
|
|
if markupType := markup.Type(readmeFile.Name()); markupType != "" {
|
|
ctx.Data["IsMarkup"] = true
|
|
ctx.Data["MarkupType"] = markupType
|
|
|
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
|
Ctx: ctx,
|
|
RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
|
Links: markup.Links{
|
|
Base: ctx.Repo.RepoLink,
|
|
BranchPath: ctx.Repo.BranchNameSubURL(),
|
|
TreePath: path.Join(ctx.Repo.TreePath, subfolder),
|
|
},
|
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
|
GitRepo: ctx.Repo.GitRepo,
|
|
}, rd)
|
|
if err != nil {
|
|
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
|
delete(ctx.Data, "IsMarkup")
|
|
}
|
|
}
|
|
|
|
if ctx.Data["IsMarkup"] != true {
|
|
ctx.Data["IsPlainText"] = true
|
|
content, err := io.ReadAll(rd)
|
|
if err != nil {
|
|
log.Error("Read readme content failed: %v", err)
|
|
}
|
|
contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content))
|
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
|
|
}
|
|
|
|
if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
|
ctx.Data["CanEditReadmeFile"] = true
|
|
}
|
|
}
|
|
|
|
func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|
ctx.Data["IsViewFile"] = true
|
|
ctx.Data["HideRepoInfo"] = true
|
|
blob := entry.Blob()
|
|
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
|
|
if err != nil {
|
|
ctx.ServerError("getFileReader", err)
|
|
return
|
|
}
|
|
defer dataRc.Close()
|
|
|
|
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefName)
|
|
ctx.Data["FileIsSymlink"] = entry.IsLink()
|
|
ctx.Data["FileName"] = blob.Name()
|
|
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
|
|
if ctx.Repo.TreePath == ".editorconfig" {
|
|
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
|
if editorconfigWarning != nil {
|
|
ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error())
|
|
}
|
|
if editorconfigErr != nil {
|
|
ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
|
|
}
|
|
} else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) {
|
|
_, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit)
|
|
if issueConfigErr != nil {
|
|
ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
|
|
}
|
|
} else if actions.IsWorkflow(ctx.Repo.TreePath) {
|
|
content, err := actions.GetContentFromEntry(entry)
|
|
if err != nil {
|
|
log.Error("actions.GetContentFromEntry: %v", err)
|
|
}
|
|
_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
|
|
if workFlowErr != nil {
|
|
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
|
|
}
|
|
} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
|
|
if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
|
|
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
|
|
if len(warnings) > 0 {
|
|
ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
isDisplayingSource := ctx.FormString("display") == "source"
|
|
isDisplayingRendered := !isDisplayingSource
|
|
|
|
if fInfo.isLFSFile {
|
|
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
}
|
|
|
|
isRepresentableAsText := fInfo.st.IsRepresentableAsText()
|
|
if !isRepresentableAsText {
|
|
// If we can't show plain text, always try to render.
|
|
isDisplayingSource = false
|
|
isDisplayingRendered = true
|
|
}
|
|
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
|
ctx.Data["FileSize"] = fInfo.fileSize
|
|
ctx.Data["IsTextFile"] = fInfo.isTextFile
|
|
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
|
|
ctx.Data["IsDisplayingSource"] = isDisplayingSource
|
|
ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
|
|
ctx.Data["IsExecutable"] = entry.IsExecutable()
|
|
|
|
isTextSource := fInfo.isTextFile || isDisplayingSource
|
|
ctx.Data["IsTextSource"] = isTextSource
|
|
if isTextSource {
|
|
ctx.Data["CanCopyContent"] = true
|
|
}
|
|
|
|
// Check LFS Lock
|
|
lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
|
|
ctx.Data["LFSLock"] = lfsLock
|
|
if err != nil {
|
|
ctx.ServerError("GetTreePathLock", err)
|
|
return
|
|
}
|
|
if lfsLock != nil {
|
|
u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
|
|
if err != nil {
|
|
ctx.ServerError("GetTreePathLock", err)
|
|
return
|
|
}
|
|
ctx.Data["LFSLockOwner"] = u.Name
|
|
ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
|
|
ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
|
|
}
|
|
|
|
// Assume file is not editable first.
|
|
if fInfo.isLFSFile {
|
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
|
|
} else if !isRepresentableAsText {
|
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
|
|
}
|
|
|
|
switch {
|
|
case isRepresentableAsText:
|
|
if fInfo.st.IsSvgImage() {
|
|
ctx.Data["IsImageFile"] = true
|
|
ctx.Data["CanCopyContent"] = true
|
|
ctx.Data["HasSourceRenderedToggle"] = true
|
|
}
|
|
|
|
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
|
ctx.Data["IsFileTooLarge"] = true
|
|
break
|
|
}
|
|
|
|
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
|
|
|
|
shouldRenderSource := ctx.FormString("display") == "source"
|
|
readmeExist := util.IsReadmeFileName(blob.Name())
|
|
ctx.Data["ReadmeExist"] = readmeExist
|
|
|
|
markupType := markup.Type(blob.Name())
|
|
// If the markup is detected by custom markup renderer it should not be reset later on
|
|
// to not pass it down to the render context.
|
|
detected := false
|
|
if markupType == "" {
|
|
detected = true
|
|
markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
|
|
}
|
|
if markupType != "" {
|
|
ctx.Data["HasSourceRenderedToggle"] = true
|
|
}
|
|
|
|
if markupType != "" && !shouldRenderSource {
|
|
ctx.Data["IsMarkup"] = true
|
|
ctx.Data["MarkupType"] = markupType
|
|
if !detected {
|
|
markupType = ""
|
|
}
|
|
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
|
|
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
|
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
|
Ctx: ctx,
|
|
Type: markupType,
|
|
RelativePath: ctx.Repo.TreePath,
|
|
Links: markup.Links{
|
|
Base: ctx.Repo.RepoLink,
|
|
BranchPath: ctx.Repo.BranchNameSubURL(),
|
|
TreePath: path.Dir(ctx.Repo.TreePath),
|
|
},
|
|
Metas: metas,
|
|
GitRepo: ctx.Repo.GitRepo,
|
|
}, rd)
|
|
if err != nil {
|
|
ctx.ServerError("Render", err)
|
|
return
|
|
}
|
|
// to prevent iframe load third-party url
|
|
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
|
|
} else {
|
|
buf, _ := io.ReadAll(rd)
|
|
|
|
// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
|
|
// empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
|
|
// Gitea uses the definition (like most modern editors):
|
|
// empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
|
|
// When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
|
|
// To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
|
|
// This NumLines is only used for the display on the UI: "xxx lines"
|
|
if len(buf) == 0 {
|
|
ctx.Data["NumLines"] = 0
|
|
} else {
|
|
ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
|
|
}
|
|
ctx.Data["NumLinesSet"] = true
|
|
|
|
language := ""
|
|
|
|
indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
|
|
if err == nil {
|
|
defer deleteTemporaryFile()
|
|
|
|
filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
|
|
CachedOnly: true,
|
|
Attributes: []string{"linguist-language", "gitlab-language"},
|
|
Filenames: []string{ctx.Repo.TreePath},
|
|
IndexFile: indexFilename,
|
|
WorkTree: worktree,
|
|
})
|
|
if err != nil {
|
|
log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
|
|
}
|
|
|
|
language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
|
|
if language == "" || language == "unspecified" {
|
|
language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
|
|
}
|
|
if language == "unspecified" {
|
|
language = ""
|
|
}
|
|
}
|
|
fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
|
|
ctx.Data["LexerName"] = lexerName
|
|
if err != nil {
|
|
log.Error("highlight.File failed, fallback to plain text: %v", err)
|
|
fileContent = highlight.PlainText(buf)
|
|
}
|
|
status := &charset.EscapeStatus{}
|
|
statuses := make([]*charset.EscapeStatus, len(fileContent))
|
|
for i, line := range fileContent {
|
|
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
|
|
status = status.Or(statuses[i])
|
|
}
|
|
ctx.Data["EscapeStatus"] = status
|
|
ctx.Data["FileContent"] = fileContent
|
|
ctx.Data["LineEscapeStatus"] = statuses
|
|
}
|
|
if !fInfo.isLFSFile {
|
|
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
|
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
|
|
ctx.Data["CanEditFile"] = false
|
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
|
|
} else {
|
|
ctx.Data["CanEditFile"] = true
|
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
|
|
}
|
|
} else if !ctx.Repo.IsViewBranch {
|
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
|
|
} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
|
|
}
|
|
}
|
|
|
|
case fInfo.st.IsPDF():
|
|
ctx.Data["IsPDFFile"] = true
|
|
case fInfo.st.IsVideo():
|
|
ctx.Data["IsVideoFile"] = true
|
|
case fInfo.st.IsAudio():
|
|
ctx.Data["IsAudioFile"] = true
|
|
case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
|
|
ctx.Data["IsImageFile"] = true
|
|
ctx.Data["CanCopyContent"] = true
|
|
default:
|
|
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
|
ctx.Data["IsFileTooLarge"] = true
|
|
break
|
|
}
|
|
|
|
if markupType := markup.Type(blob.Name()); markupType != "" {
|
|
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
|
|
ctx.Data["IsMarkup"] = true
|
|
ctx.Data["MarkupType"] = markupType
|
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
|
Ctx: ctx,
|
|
RelativePath: ctx.Repo.TreePath,
|
|
Links: markup.Links{
|
|
Base: ctx.Repo.RepoLink,
|
|
BranchPath: ctx.Repo.BranchNameSubURL(),
|
|
TreePath: path.Dir(ctx.Repo.TreePath),
|
|
},
|
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
|
GitRepo: ctx.Repo.GitRepo,
|
|
}, rd)
|
|
if err != nil {
|
|
ctx.ServerError("Render", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
|
|
img, _, err := image.DecodeConfig(bytes.NewReader(buf))
|
|
if err == nil {
|
|
// There are Image formats go can't decode
|
|
// Instead of throwing an error in that case, we show the size only when we can decode
|
|
ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
|
|
}
|
|
}
|
|
|
|
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
|
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
|
|
ctx.Data["CanDeleteFile"] = false
|
|
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
|
|
} else {
|
|
ctx.Data["CanDeleteFile"] = true
|
|
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
|
|
}
|
|
} else if !ctx.Repo.IsViewBranch {
|
|
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
|
|
} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
|
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
|
}
|
|
}
|
|
|
|
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
|
|
markupRd, markupWr := io.Pipe()
|
|
defer markupWr.Close()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
sb := &strings.Builder{}
|
|
// We allow NBSP here this is rendered
|
|
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
|
|
output = template.HTML(sb.String())
|
|
close(done)
|
|
}()
|
|
err = markup.Render(renderCtx, input, markupWr)
|
|
_ = markupWr.CloseWithError(err)
|
|
<-done
|
|
return escaped, output, err
|
|
}
|
|
|
|
func checkHomeCodeViewable(ctx *context.Context) {
|
|
if len(ctx.Repo.Units) > 0 {
|
|
if ctx.Repo.Repository.IsBeingCreated() {
|
|
task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
|
|
if err != nil {
|
|
if admin_model.IsErrTaskDoesNotExist(err) {
|
|
ctx.Data["Repo"] = ctx.Repo
|
|
ctx.Data["CloneAddr"] = ""
|
|
ctx.Data["Failed"] = true
|
|
ctx.HTML(http.StatusOK, tplMigrating)
|
|
return
|
|
}
|
|
ctx.ServerError("models.GetMigratingTask", err)
|
|
return
|
|
}
|
|
cfg, err := task.MigrateConfig()
|
|
if err != nil {
|
|
ctx.ServerError("task.MigrateConfig", err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["Repo"] = ctx.Repo
|
|
ctx.Data["MigrateTask"] = task
|
|
ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr)
|
|
ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed
|
|
ctx.HTML(http.StatusOK, tplMigrating)
|
|
return
|
|
}
|
|
|
|
if ctx.IsSigned {
|
|
// Set repo notification-status read if unread
|
|
if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
|
|
ctx.ServerError("ReadBy", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
var firstUnit *unit_model.Unit
|
|
for _, repoUnit := range ctx.Repo.Units {
|
|
if repoUnit.Type == unit_model.TypeCode {
|
|
return
|
|
}
|
|
|
|
unit, ok := unit_model.Units[repoUnit.Type]
|
|
if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
|
|
firstUnit = &unit
|
|
}
|
|
}
|
|
|
|
if firstUnit != nil {
|
|
ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI))
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
|
|
}
|
|
|
|
func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
|
|
if entry.Name() != "" {
|
|
return
|
|
}
|
|
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
|
|
if err != nil {
|
|
HandleGitError(ctx, "Repo.Commit.SubTree", err)
|
|
return
|
|
}
|
|
allEntries, err := tree.ListEntries()
|
|
if err != nil {
|
|
ctx.ServerError("ListEntries", err)
|
|
return
|
|
}
|
|
for _, entry := range allEntries {
|
|
if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
|
|
// Read Citation file contents
|
|
if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
|
log.Error("checkCitationFile: GetBlobContent: %v", err)
|
|
} else {
|
|
ctx.Data["CitiationExist"] = true
|
|
ctx.PageData["citationFileContent"] = content
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Home render repository home page
|
|
func Home(ctx *context.Context) {
|
|
if setting.Other.EnableFeed {
|
|
isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
|
|
if isFeed {
|
|
switch {
|
|
case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType):
|
|
feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
|
|
case ctx.Repo.TreePath == "":
|
|
feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
|
|
case ctx.Repo.TreePath != "":
|
|
feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
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 {
|
|
HandleGitError(ctx, "Repo.Commit.SubTree", err)
|
|
return nil
|
|
}
|
|
|
|
ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
|
|
// Get current entry user currently looking at.
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
|
if err != nil {
|
|
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
|
return nil
|
|
}
|
|
|
|
if !entry.IsDir() {
|
|
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", 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()
|
|
}
|
|
|
|
selected := make(container.Set[string])
|
|
selected.AddMultiple(ctx.FormStrings("f[]")...)
|
|
|
|
entries := allEntries
|
|
if len(selected) > 0 {
|
|
entries = make(git.Entries, 0, len(selected))
|
|
for _, entry := range allEntries {
|
|
if selected.Contains(entry.Name()) {
|
|
entries = append(entries, entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
var latestCommit *git.Commit
|
|
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
|
|
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 := asymkey_model.ParseCommitWithSignature(ctx, latestCommit)
|
|
|
|
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
|
|
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
|
|
}, nil); err != nil {
|
|
ctx.ServerError("CalculateTrustStatus", err)
|
|
return nil
|
|
}
|
|
ctx.Data["LatestCommitVerification"] = verification
|
|
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
|
|
|
|
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true})
|
|
if err != nil {
|
|
log.Error("GetLatestCommitStatus: %v", err)
|
|
}
|
|
|
|
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
|
|
ctx.Data["LatestCommitStatuses"] = statuses
|
|
}
|
|
|
|
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
treeLink := branchLink
|
|
|
|
if len(ctx.Repo.TreePath) > 0 {
|
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
}
|
|
|
|
ctx.Data["TreeLink"] = treeLink
|
|
ctx.Data["SSHDomain"] = setting.SSH.Domain
|
|
|
|
return allEntries
|
|
}
|
|
|
|
func renderLanguageStats(ctx *context.Context) {
|
|
langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5)
|
|
if err != nil {
|
|
ctx.ServerError("Repo.GetTopLanguageStats", err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["LanguageStats"] = langs
|
|
}
|
|
|
|
func renderRepoTopics(ctx *context.Context) {
|
|
topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("models.FindTopics", err)
|
|
return
|
|
}
|
|
ctx.Data["Topics"] = topics
|
|
}
|
|
|
|
func renderCode(ctx *context.Context) {
|
|
ctx.Data["PageIsViewCode"] = true
|
|
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
|
|
|
|
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
|
showEmpty := true
|
|
var err error
|
|
if ctx.Repo.GitRepo != nil {
|
|
showEmpty, err = ctx.Repo.GitRepo.IsEmpty()
|
|
if err != nil {
|
|
log.Error("GitRepo.IsEmpty: %v", err)
|
|
ctx.Repo.Repository.Status = repo_model.RepositoryBroken
|
|
showEmpty = true
|
|
ctx.Flash.Error(ctx.Tr("error.occurred"), true)
|
|
}
|
|
}
|
|
if showEmpty {
|
|
ctx.HTML(http.StatusOK, tplRepoEMPTY)
|
|
return
|
|
}
|
|
|
|
// the repo is not really empty, so we should update the modal in database
|
|
// such problem may be caused by:
|
|
// 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually
|
|
// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos.
|
|
// it's possible for a repository to be non-empty by that flag but still 500
|
|
// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
|
|
ctx.Repo.Repository.IsEmpty = false
|
|
if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil {
|
|
ctx.ServerError("UpdateRepositoryCols", err)
|
|
return
|
|
}
|
|
if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
|
|
ctx.ServerError("UpdateRepoSize", err)
|
|
return
|
|
}
|
|
|
|
// the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values
|
|
link := ctx.Link
|
|
if ctx.Req.URL.RawQuery != "" {
|
|
link += "?" + ctx.Req.URL.RawQuery
|
|
}
|
|
ctx.Redirect(link)
|
|
return
|
|
}
|
|
|
|
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
|
if len(ctx.Repo.Repository.Description) > 0 {
|
|
title += ": " + ctx.Repo.Repository.Description
|
|
}
|
|
ctx.Data["Title"] = title
|
|
|
|
// Get Topics of this repo
|
|
renderRepoTopics(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
// Get current entry user currently looking at.
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
|
if err != nil {
|
|
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
|
return
|
|
}
|
|
|
|
checkCitationFile(ctx, entry)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
renderLanguageStats(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if entry.IsDir() {
|
|
renderDirectory(ctx)
|
|
} else {
|
|
renderFile(ctx, entry)
|
|
}
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if ctx.Doer != nil {
|
|
if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
|
|
ctx.ServerError("GetBaseRepo", err)
|
|
return
|
|
}
|
|
|
|
showRecentlyPushedNewBranches := true
|
|
if ctx.Repo.Repository.IsMirror ||
|
|
!ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
|
|
showRecentlyPushedNewBranches = false
|
|
}
|
|
if showRecentlyPushedNewBranches {
|
|
ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch)
|
|
if err != nil {
|
|
ctx.ServerError("GetRecentlyPushedBranches", err)
|
|
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]
|
|
}
|
|
}
|
|
|
|
ctx.Data["Paths"] = paths
|
|
|
|
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
treeLink := branchLink
|
|
if len(ctx.Repo.TreePath) > 0 {
|
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
|
}
|
|
ctx.Data["TreeLink"] = treeLink
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["BranchLink"] = branchLink
|
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
|
}
|
|
|
|
// RenderUserCards render a page show users according the input template
|
|
func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
|
|
page := ctx.FormInt("page")
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pager := context.NewPagination(total, setting.ItemsPerPage, page, 5)
|
|
ctx.Data["Page"] = pager
|
|
|
|
items, err := getter(db.ListOptions{
|
|
Page: pager.Paginater.Current(),
|
|
PageSize: setting.ItemsPerPage,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("getter", err)
|
|
return
|
|
}
|
|
ctx.Data["Cards"] = items
|
|
|
|
ctx.HTML(http.StatusOK, tpl)
|
|
}
|
|
|
|
// Watchers render repository's watch users
|
|
func Watchers(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("repo.watchers")
|
|
ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
|
|
ctx.Data["PageIsWatchers"] = true
|
|
|
|
RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
|
|
return repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, opts)
|
|
}, tplWatchers)
|
|
}
|
|
|
|
// Stars render repository's starred users
|
|
func Stars(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("repo.stargazers")
|
|
ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
|
|
ctx.Data["PageIsStargazers"] = true
|
|
RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
|
|
return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
|
|
}, tplWatchers)
|
|
}
|
|
|
|
// Forks render repository's forked users
|
|
func Forks(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("repo.forks")
|
|
|
|
page := ctx.FormInt("page")
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
|
|
pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
|
|
ctx.Data["Page"] = pager
|
|
|
|
forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
|
|
Page: pager.Paginater.Current(),
|
|
PageSize: setting.ItemsPerPage,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("GetForks", err)
|
|
return
|
|
}
|
|
|
|
for _, fork := range forks {
|
|
if err = fork.LoadOwner(ctx); err != nil {
|
|
ctx.ServerError("LoadOwner", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.Data["Forks"] = forks
|
|
|
|
ctx.HTML(http.StatusOK, tplForks)
|
|
}
|