mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 16:44:26 +00:00
GitHub like repo home page (#32213)
Move some components (description, license, release, language stats) to sidebar --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
3c4a06273f
commit
faf5705d29
@ -145,6 +145,7 @@ confirm_delete_selected = Confirm to delete all selected items?
|
|||||||
|
|
||||||
name = Name
|
name = Name
|
||||||
value = Value
|
value = Value
|
||||||
|
readme = Readme
|
||||||
|
|
||||||
filter = Filter
|
filter = Filter
|
||||||
filter.clear = Clear Filter
|
filter.clear = Clear Filter
|
||||||
@ -1045,7 +1046,8 @@ generate_repo = Generate Repository
|
|||||||
generate_from = Generate From
|
generate_from = Generate From
|
||||||
repo_desc = Description
|
repo_desc = Description
|
||||||
repo_desc_helper = Enter short description (optional)
|
repo_desc_helper = Enter short description (optional)
|
||||||
repo_lang = Language
|
repo_no_desc = No description provided
|
||||||
|
repo_lang = Languages
|
||||||
repo_gitignore_helper = Select .gitignore templates.
|
repo_gitignore_helper = Select .gitignore templates.
|
||||||
repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default.
|
repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default.
|
||||||
issue_labels = Issue Labels
|
issue_labels = Issue Labels
|
||||||
|
@ -114,12 +114,6 @@ func RefBlame(ctx *context.Context) {
|
|||||||
ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs
|
ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs
|
||||||
ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile
|
ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile
|
||||||
|
|
||||||
// Get Topics of this repo
|
|
||||||
renderRepoTopics(ctx)
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
commitNames := processBlameParts(ctx, result.Parts)
|
commitNames := processBlameParts(ctx, result.Parts)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
|
@ -89,7 +89,6 @@ func Branches(ctx *context.Context) {
|
|||||||
pager := context.NewPagination(int(branchesCount), pageSize, page, 5)
|
pager := context.NewPagination(int(branchesCount), pageSize, page, 5)
|
||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
ctx.HTML(http.StatusOK, tplBranch)
|
ctx.HTML(http.StatusOK, tplBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,6 @@ func Commits(ctx *context.Context) {
|
|||||||
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
ctx.HTML(http.StatusOK, tplCommits)
|
ctx.HTML(http.StatusOK, tplCommits)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,8 +218,6 @@ func SearchCommits(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||||
ctx.Data["RefName"] = ctx.Repo.RefName
|
|
||||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
ctx.HTML(http.StatusOK, tplCommits)
|
ctx.HTML(http.StatusOK, tplCommits)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +263,6 @@ func FileHistory(ctx *context.Context) {
|
|||||||
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
ctx.HTML(http.StatusOK, tplCommits)
|
ctx.HTML(http.StatusOK, tplCommits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ import (
|
|||||||
"code.gitea.io/gitea/services/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
releaseservice "code.gitea.io/gitea/services/release"
|
releaseservice "code.gitea.io/gitea/services/release"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -153,9 +152,6 @@ func Releases(ctx *context.Context) {
|
|||||||
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
|
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
|
||||||
ctx.Data["IsViewBranch"] = false
|
ctx.Data["IsViewBranch"] = false
|
||||||
ctx.Data["IsViewTag"] = true
|
ctx.Data["IsViewTag"] = true
|
||||||
// Disable the showCreateNewBranch form in the dropdown on this page.
|
|
||||||
ctx.Data["CanCreateBranch"] = false
|
|
||||||
ctx.Data["HideBranchesInDropdown"] = true
|
|
||||||
|
|
||||||
listOptions := db.ListOptions{
|
listOptions := db.ListOptions{
|
||||||
Page: ctx.FormInt("page"),
|
Page: ctx.FormInt("page"),
|
||||||
@ -193,9 +189,6 @@ func Releases(ctx *context.Context) {
|
|||||||
pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
|
pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
|
||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplReleasesList)
|
ctx.HTML(http.StatusOK, tplReleasesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,9 +198,6 @@ func TagsList(ctx *context.Context) {
|
|||||||
ctx.Data["Title"] = ctx.Tr("repo.release.tags")
|
ctx.Data["Title"] = ctx.Tr("repo.release.tags")
|
||||||
ctx.Data["IsViewBranch"] = false
|
ctx.Data["IsViewBranch"] = false
|
||||||
ctx.Data["IsViewTag"] = true
|
ctx.Data["IsViewTag"] = true
|
||||||
// Disable the showCreateNewBranch form in the dropdown on this page.
|
|
||||||
ctx.Data["CanCreateBranch"] = false
|
|
||||||
ctx.Data["HideBranchesInDropdown"] = true
|
|
||||||
ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived
|
ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived
|
||||||
|
|
||||||
namePattern := ctx.FormTrim("q")
|
namePattern := ctx.FormTrim("q")
|
||||||
@ -254,8 +244,6 @@ func TagsList(ctx *context.Context) {
|
|||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases)
|
ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases)
|
||||||
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplTagsList)
|
ctx.HTML(http.StatusOK, tplTagsList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,18 +5,13 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
gocontext "context"
|
gocontext "context"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"image"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -29,33 +24,21 @@ import (
|
|||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
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"
|
|
||||||
"code.gitea.io/gitea/models/renderhelper"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/base"
|
||||||
"code.gitea.io/gitea/modules/charset"
|
"code.gitea.io/gitea/modules/charset"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/highlight"
|
|
||||||
"code.gitea.io/gitea/modules/lfs"
|
"code.gitea.io/gitea/modules/lfs"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"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/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/svg"
|
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/web/feed"
|
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
issue_service "code.gitea.io/gitea/services/issue"
|
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
|
||||||
|
|
||||||
_ "golang.org/x/image/bmp" // for processing bmp images
|
_ "golang.org/x/image/bmp" // for processing bmp images
|
||||||
_ "golang.org/x/image/webp" // for processing webp images
|
_ "golang.org/x/image/webp" // for processing webp images
|
||||||
@ -70,140 +53,6 @@ const (
|
|||||||
tplMigrating base.TplName = "repo/migrate/migrating"
|
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
|
|
||||||
}
|
|
||||||
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+"/"+path.Base(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 {
|
type fileInfo struct {
|
||||||
isTextFile bool
|
isTextFile bool
|
||||||
isLFSFile bool
|
isLFSFile bool
|
||||||
@ -261,85 +110,6 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
|
|||||||
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
|
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["FileSize"] = fInfo.fileSize
|
|
||||||
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
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
|
||||||
|
|
||||||
if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
|
|
||||||
ctx.Data["IsMarkup"] = true
|
|
||||||
ctx.Data["MarkupType"] = markupType
|
|
||||||
|
|
||||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
|
||||||
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
|
||||||
CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
|
|
||||||
}).
|
|
||||||
WithMarkupType(markupType).
|
|
||||||
WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
|
||||||
|
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, 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 loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
||||||
// Show latest commit info of repository in table header,
|
// Show latest commit info of repository in table header,
|
||||||
// or of directory if not in root directory.
|
// or of directory if not in root directory.
|
||||||
@ -371,287 +141,6 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
|||||||
return true
|
return 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+"/"+path.Base(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)
|
|
||||||
|
|
||||||
commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetCommitByPath", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !loadLatestCommitData(ctx, commit) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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.fileSize >= setting.UI.MaxDisplayFileSize {
|
|
||||||
ctx.Data["IsFileTooLarge"] = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if fInfo.st.IsSvgImage() {
|
|
||||||
ctx.Data["IsImageFile"] = true
|
|
||||||
ctx.Data["CanCopyContent"] = true
|
|
||||||
ctx.Data["HasSourceRenderedToggle"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
|
||||||
|
|
||||||
shouldRenderSource := ctx.FormString("display") == "source"
|
|
||||||
readmeExist := util.IsReadmeFileName(blob.Name())
|
|
||||||
ctx.Data["ReadmeExist"] = readmeExist
|
|
||||||
|
|
||||||
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
|
||||||
if markupType == "" {
|
|
||||||
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
|
|
||||||
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
|
|
||||||
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
|
|
||||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
|
||||||
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
|
||||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
|
||||||
}).
|
|
||||||
WithMarkupType(markupType).
|
|
||||||
WithRelativePath(ctx.Repo.TreePath).
|
|
||||||
WithMetas(metas)
|
|
||||||
|
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
|
|
||||||
// It is used by "external renders", markupRender will execute external programs to get rendered content.
|
|
||||||
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
|
|
||||||
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
|
|
||||||
ctx.Data["IsMarkup"] = true
|
|
||||||
ctx.Data["MarkupType"] = markupType
|
|
||||||
|
|
||||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
|
||||||
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
|
||||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
|
||||||
}).
|
|
||||||
WithMarkupType(markupType).
|
|
||||||
WithRelativePath(ctx.Repo.TreePath)
|
|
||||||
|
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("Render", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Repo.GitRepo != nil {
|
|
||||||
checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID)
|
|
||||||
if checker != nil {
|
|
||||||
defer deferable()
|
|
||||||
attrs, err := checker.CheckPath(ctx.Repo.TreePath)
|
|
||||||
if err == nil {
|
|
||||||
ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
|
|
||||||
ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
|
||||||
markupRd, markupWr := io.Pipe()
|
markupRd, markupWr := io.Pipe()
|
||||||
defer markupWr.Close()
|
defer markupWr.Close()
|
||||||
@ -728,59 +217,6 @@ func checkHomeCodeViewable(ctx *context.Context) {
|
|||||||
ctx.NotFound("Home", errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
|
ctx.NotFound("Home", errors.New(ctx.Locale.TrString("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.PathParam(":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
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHomeCode(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
|
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
|
||||||
func LastCommit(ctx *context.Context) {
|
func LastCommit(ctx *context.Context) {
|
||||||
checkHomeCodeViewable(ctx)
|
checkHomeCodeViewable(ctx)
|
||||||
@ -877,220 +313,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||||||
return allEntries
|
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 := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
|
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("models.FindTopics", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["Topics"] = topics
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareOpenWithEditorApps(ctx *context.Context) {
|
|
||||||
var tmplApps []map[string]any
|
|
||||||
apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
|
|
||||||
if len(apps) == 0 {
|
|
||||||
apps = setting.DefaultOpenWithEditorApps()
|
|
||||||
}
|
|
||||||
for _, app := range apps {
|
|
||||||
schema, _, _ := strings.Cut(app.OpenURL, ":")
|
|
||||||
var iconHTML template.HTML
|
|
||||||
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
|
|
||||||
iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2")
|
|
||||||
} else {
|
|
||||||
iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future
|
|
||||||
}
|
|
||||||
tmplApps = append(tmplApps, map[string]any{
|
|
||||||
"DisplayName": app.DisplayName,
|
|
||||||
"OpenURL": app.OpenURL,
|
|
||||||
"IconHTML": iconHTML,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx.Data["OpenWithEditorApps"] = tmplApps
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderHomeCode(ctx *context.Context) {
|
|
||||||
ctx.Data["PageIsViewCode"] = true
|
|
||||||
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
|
|
||||||
prepareOpenWithEditorApps(ctx)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
checkOutdatedBranch(ctx)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := &git_model.FindRecentlyPushedNewBranchesOptions{
|
|
||||||
Repo: ctx.Repo.Repository,
|
|
||||||
BaseRepo: ctx.Repo.Repository,
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
log.Error("FindRecentlyPushedNewBranches failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Data["LicenseFileName"] = repo_service.LicenseFileName
|
|
||||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkOutdatedBranch(ctx *context.Context) {
|
|
||||||
if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
|
|
||||||
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("GetBranchCommitID: %v", err)
|
|
||||||
// Don't return an error page, as it can be rechecked the next time the user opens the page.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("GetBranch: %v", err)
|
|
||||||
// Don't return an error page, as it can be rechecked the next time the user opens the page.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbBranch.CommitID != commit.ID.String() {
|
|
||||||
ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderUserCards render a page show users according the input template
|
// 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) {
|
func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
|
||||||
page := ctx.FormInt("page")
|
page := ctx.FormInt("page")
|
||||||
|
313
routers/web/repo/view_file.go
Normal file
313
routers/web/repo/view_file.go
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
issue_model "code.gitea.io/gitea/models/issues"
|
||||||
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/actions"
|
||||||
|
"code.gitea.io/gitea/modules/charset"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/highlight"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
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+"/"+path.Base(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)
|
||||||
|
|
||||||
|
commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetCommitByPath", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loadLatestCommitData(ctx, commit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.fileSize >= setting.UI.MaxDisplayFileSize {
|
||||||
|
ctx.Data["IsFileTooLarge"] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if fInfo.st.IsSvgImage() {
|
||||||
|
ctx.Data["IsImageFile"] = true
|
||||||
|
ctx.Data["CanCopyContent"] = true
|
||||||
|
ctx.Data["HasSourceRenderedToggle"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||||
|
|
||||||
|
shouldRenderSource := ctx.FormString("display") == "source"
|
||||||
|
readmeExist := util.IsReadmeFileName(blob.Name())
|
||||||
|
ctx.Data["ReadmeExist"] = readmeExist
|
||||||
|
|
||||||
|
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
||||||
|
if markupType == "" {
|
||||||
|
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
|
||||||
|
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
|
||||||
|
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
|
||||||
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||||
|
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
||||||
|
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||||
|
}).
|
||||||
|
WithMarkupType(markupType).
|
||||||
|
WithRelativePath(ctx.Repo.TreePath).
|
||||||
|
WithMetas(metas)
|
||||||
|
|
||||||
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
|
||||||
|
// It is used by "external renders", markupRender will execute external programs to get rendered content.
|
||||||
|
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
|
||||||
|
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
|
||||||
|
ctx.Data["IsMarkup"] = true
|
||||||
|
ctx.Data["MarkupType"] = markupType
|
||||||
|
|
||||||
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||||
|
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
||||||
|
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||||
|
}).
|
||||||
|
WithMarkupType(markupType).
|
||||||
|
WithRelativePath(ctx.Repo.TreePath)
|
||||||
|
|
||||||
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Render", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Repo.GitRepo != nil {
|
||||||
|
checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID)
|
||||||
|
if checker != nil {
|
||||||
|
defer deferable()
|
||||||
|
attrs, err := checker.CheckPath(ctx.Repo.TreePath)
|
||||||
|
if err == nil {
|
||||||
|
ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
|
||||||
|
ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
351
routers/web/repo/view_home.go
Normal file
351
routers/web/repo/view_home.go
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
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"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/svg"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkOutdatedBranch(ctx *context.Context) {
|
||||||
|
if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
|
||||||
|
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetBranchCommitID: %v", err)
|
||||||
|
// Don't return an error page, as it can be rechecked the next time the user opens the page.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetBranch: %v", err)
|
||||||
|
// Don't return an error page, as it can be rechecked the next time the user opens the page.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbBranch.CommitID != commit.ID.String() {
|
||||||
|
ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHomeSidebarRepoTopics(ctx *context.Context) {
|
||||||
|
topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("models.FindTopics", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Topics"] = topics
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareOpenWithEditorApps(ctx *context.Context) {
|
||||||
|
var tmplApps []map[string]any
|
||||||
|
apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
|
||||||
|
if len(apps) == 0 {
|
||||||
|
apps = setting.DefaultOpenWithEditorApps()
|
||||||
|
}
|
||||||
|
for _, app := range apps {
|
||||||
|
schema, _, _ := strings.Cut(app.OpenURL, ":")
|
||||||
|
var iconHTML template.HTML
|
||||||
|
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
|
||||||
|
iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2")
|
||||||
|
} else {
|
||||||
|
iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future
|
||||||
|
}
|
||||||
|
tmplApps = append(tmplApps, map[string]any{
|
||||||
|
"DisplayName": app.DisplayName,
|
||||||
|
"OpenURL": app.OpenURL,
|
||||||
|
"IconHTML": iconHTML,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx.Data["OpenWithEditorApps"] = tmplApps
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHomeSidebarCitationFile(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHomeSidebarLicenses(ctx *context.Context) {
|
||||||
|
repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRepoLicenses", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList()
|
||||||
|
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareToRenderDirectory(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+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
|
||||||
|
}
|
||||||
|
|
||||||
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("findReadmeFileInEntries", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareToRenderReadmeFile(ctx, subfolder, readmeFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHomeSidebarLanguageStats(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 prepareHomeSidebarLatestRelease(ctx *context.Context) {
|
||||||
|
if !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeReleases) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||||
|
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||||
|
ctx.ServerError("GetLatestReleaseByRepoID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if release != nil {
|
||||||
|
if err = release.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.ServerError("release.LoadAttributes", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["LatestRelease"] = release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderHomeCode(ctx *context.Context) {
|
||||||
|
ctx.Data["PageIsViewCode"] = true
|
||||||
|
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
|
||||||
|
prepareOpenWithEditorApps(ctx)
|
||||||
|
|
||||||
|
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
|
||||||
|
prepareHomeSidebarRepoTopics(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
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOutdatedBranch(ctx)
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
prepareToRenderDirectory(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
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &git_model.FindRecentlyPushedNewBranchesOptions{
|
||||||
|
Repo: ctx.Repo.Repository,
|
||||||
|
BaseRepo: ctx.Repo.Repository,
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
log.Error("FindRecentlyPushedNewBranches failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var treeNames, paths []string
|
||||||
|
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
||||||
|
treeLink := branchLink
|
||||||
|
if ctx.Repo.TreePath != "" {
|
||||||
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTreePathRoot := ctx.Repo.TreePath == ""
|
||||||
|
if isTreePathRoot {
|
||||||
|
prepareHomeSidebarLicenses(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prepareHomeSidebarCitationFile(ctx, entry)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareHomeSidebarLanguageStats(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareHomeSidebarLatestRelease(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Paths"] = paths
|
||||||
|
ctx.Data["TreeLink"] = treeLink
|
||||||
|
ctx.Data["TreeNames"] = treeNames
|
||||||
|
ctx.Data["BranchLink"] = branchLink
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home render repository home page
|
||||||
|
func Home(ctx *context.Context) {
|
||||||
|
if setting.Other.EnableFeed {
|
||||||
|
isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":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
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHomeCode(ctx)
|
||||||
|
}
|
218
routers/web/repo/view_readme.go
Normal file
218
routers/web/repo/view_readme.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/charset"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareToRenderReadmeFile(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["FileSize"] = fInfo.fileSize
|
||||||
|
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
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||||
|
|
||||||
|
if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
|
||||||
|
ctx.Data["IsMarkup"] = true
|
||||||
|
ctx.Data["MarkupType"] = markupType
|
||||||
|
|
||||||
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||||
|
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
|
||||||
|
CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
|
||||||
|
}).
|
||||||
|
WithMarkupType(markupType).
|
||||||
|
WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
||||||
|
|
||||||
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, 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
|
||||||
|
}
|
||||||
|
}
|
@ -396,13 +396,6 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
|||||||
ctx.Repo.Repository = repo
|
ctx.Repo.Repository = repo
|
||||||
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
||||||
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
||||||
|
|
||||||
repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetRepoLicenses", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepoAssignment returns a middleware to handle repository assignment
|
// RepoAssignment returns a middleware to handle repository assignment
|
||||||
@ -1036,7 +1029,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
|
|||||||
ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch
|
ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch
|
||||||
ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag
|
ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag
|
||||||
ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit
|
ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit
|
||||||
ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch()
|
ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef
|
||||||
|
|
||||||
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
|
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,35 +3,7 @@
|
|||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container {{if .IsBlame}}fluid padded{{end}}">
|
<div class="ui container {{if .IsBlame}}fluid padded{{end}}">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
{{template "repo/code/recently_pushed_new_branches" .}}
|
|
||||||
{{if and (not .HideRepoInfo) (not .IsBlame)}}
|
|
||||||
<div class="repo-description tw-break-anywhere">
|
|
||||||
{{- $description := .Repository.DescriptionHTML ctx -}}
|
|
||||||
{{if $description}}{{$description | RenderCodeBlock}}{{end}}
|
|
||||||
{{if .Repository.Website}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
|
|
||||||
{{/* it should match the code in issue-home.js */}}
|
|
||||||
{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
|
|
||||||
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
|
|
||||||
<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit">
|
|
||||||
<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
|
|
||||||
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
|
|
||||||
{{range .Topics}}
|
|
||||||
{{/* keep the same layout as Fomantic UI generated labels */}}
|
|
||||||
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
|
|
||||||
{{end}}
|
|
||||||
<div class="text"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
|
|
||||||
<button class="ui primary button" id="save_topic" data-link="{{.RepoLink}}/topics">{{ctx.Locale.Tr "save"}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .Repository.IsArchived}}
|
{{if .Repository.IsArchived}}
|
||||||
<div class="ui warning message tw-text-center">
|
<div class="ui warning message tw-text-center">
|
||||||
{{if .Repository.ArchivedUnix.IsZero}}
|
{{if .Repository.ArchivedUnix.IsZero}}
|
||||||
@ -41,134 +13,138 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/sub_menu" .}}
|
|
||||||
{{$n := len .TreeNames}}
|
|
||||||
{{$l := Eval $n "-" 1}}
|
|
||||||
{{$isHomepage := (eq $n 0)}}
|
|
||||||
<div class="repo-button-row" data-is-homepage="{{$isHomepage}}">
|
|
||||||
<div class="repo-button-row-left">
|
|
||||||
{{$branchDropdownCurrentRefType := "branch"}}
|
|
||||||
{{$branchDropdownCurrentRefShortName := .BranchName}}
|
|
||||||
{{if .IsViewTag}}
|
|
||||||
{{$branchDropdownCurrentRefType = "tag"}}
|
|
||||||
{{$branchDropdownCurrentRefShortName = .TagName}}
|
|
||||||
{{end}}
|
|
||||||
{{template "repo/branch_dropdown" dict
|
|
||||||
"Repository" .Repository
|
|
||||||
"ShowTabBranches" true
|
|
||||||
"ShowTabTags" true
|
|
||||||
"CurrentRefType" $branchDropdownCurrentRefType
|
|
||||||
"CurrentRefShortName" $branchDropdownCurrentRefShortName
|
|
||||||
"CurrentTreePath" .TreePath
|
|
||||||
"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
|
|
||||||
"AllowCreateNewRef" .CanCreateBranch
|
|
||||||
"ShowViewAllRefsEntry" true
|
|
||||||
}}
|
|
||||||
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
|
|
||||||
{{$cmpBranch := ""}}
|
|
||||||
{{if ne .Repository.ID .BaseRepo.ID}}
|
|
||||||
{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}}
|
|
||||||
{{end}}
|
|
||||||
{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}}
|
|
||||||
{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}}
|
|
||||||
<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}"
|
|
||||||
data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}">
|
|
||||||
{{svg "octicon-git-pull-request"}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
<!-- Show go to file and breadcrumbs if not on home page -->
|
|
||||||
{{if $isHomepage}}
|
|
||||||
<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
|
{{template "repo/code/recently_pushed_new_branches" .}}
|
||||||
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
|
|
||||||
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
{{$treeNamesLen := len .TreeNames}}
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{$isTreePathRoot := eq $treeNamesLen 0}}
|
||||||
<div class="menu">
|
{{$showSidebar := $isTreePathRoot}}
|
||||||
<a class="item" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
|
||||||
{{ctx.Locale.Tr "repo.editor.new_file"}}
|
<div class="repo-home-filelist">
|
||||||
</a>
|
{{template "repo/sub_menu" .}}
|
||||||
{{if .RepositoryUploadEnabled}}
|
<div class="repo-button-row">
|
||||||
<a class="item" href="{{.RepoLink}}/_upload/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
<div class="repo-button-row-left">
|
||||||
{{ctx.Locale.Tr "repo.editor.upload_file"}}
|
{{$branchDropdownCurrentRefType := "branch"}}
|
||||||
</a>
|
{{$branchDropdownCurrentRefShortName := .BranchName}}
|
||||||
|
{{if .IsViewTag}}
|
||||||
|
{{$branchDropdownCurrentRefType = "tag"}}
|
||||||
|
{{$branchDropdownCurrentRefShortName = .TagName}}
|
||||||
|
{{end}}
|
||||||
|
{{template "repo/branch_dropdown" dict
|
||||||
|
"Repository" .Repository
|
||||||
|
"ShowTabBranches" true
|
||||||
|
"ShowTabTags" true
|
||||||
|
"CurrentRefType" $branchDropdownCurrentRefType
|
||||||
|
"CurrentRefShortName" $branchDropdownCurrentRefShortName
|
||||||
|
"CurrentTreePath" .TreePath
|
||||||
|
"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
|
||||||
|
"AllowCreateNewRef" .CanCreateBranch
|
||||||
|
"ShowViewAllRefsEntry" true
|
||||||
|
}}
|
||||||
|
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
|
||||||
|
{{$cmpBranch := ""}}
|
||||||
|
{{if ne .Repository.ID .BaseRepo.ID}}
|
||||||
|
{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<a class="item" href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
{{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}}
|
||||||
{{ctx.Locale.Tr "repo.editor.patch"}}
|
{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}}
|
||||||
|
<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}"
|
||||||
|
data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}">
|
||||||
|
{{svg "octicon-git-pull-request"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{{end}}
|
||||||
</button>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if and $isHomepage (.Repository.IsTemplate)}}
|
<!-- Show go to file if on home page -->
|
||||||
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
|
{{if $isTreePathRoot}}
|
||||||
{{ctx.Locale.Tr "repo.use_template"}}
|
<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
|
||||||
</a>
|
{{end}}
|
||||||
{{end}}
|
|
||||||
{{if $isHomepage}}
|
{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
|
||||||
{{/* only show the "code search" on the repo home page, it only does global search,
|
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
|
||||||
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
|
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
||||||
<form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get">
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="ui small action input tw-flex-1">
|
<div class="menu">
|
||||||
<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
|
<a class="item" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
||||||
{{template "shared/search/button"}}
|
{{ctx.Locale.Tr "repo.editor.new_file"}}
|
||||||
</div>
|
</a>
|
||||||
</form>
|
{{if .RepositoryUploadEnabled}}
|
||||||
{{else}}
|
<a class="item" href="{{.RepoLink}}/_upload/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
||||||
<span class="breadcrumb repo-path tw-ml-1">
|
{{ctx.Locale.Tr "repo.editor.upload_file"}}
|
||||||
<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
|
</a>
|
||||||
{{- range $i, $v := .TreeNames -}}
|
{{end}}
|
||||||
<span class="breadcrumb-divider">/</span>
|
<a class="item" href="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
|
||||||
{{- if eq $i $l -}}
|
{{ctx.Locale.Tr "repo.editor.patch"}}
|
||||||
<span class="active section" title="{{$v}}">{{$v}}</span>
|
</a>
|
||||||
<button class="btn interact-fg tw-mx-1" data-clipboard-text="{{$.TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button>
|
</div>
|
||||||
{{- else -}}
|
</button>
|
||||||
{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span>
|
{{end}}
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
{{if and $isTreePathRoot .Repository.IsTemplate}}
|
||||||
</span>
|
<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
|
||||||
{{end}}
|
{{ctx.Locale.Tr "repo.use_template"}}
|
||||||
</div>
|
</a>
|
||||||
<div class="repo-button-row-right">
|
{{end}}
|
||||||
<!-- Only show clone panel in repository home page -->
|
|
||||||
{{if $isHomepage}}
|
{{if not $isTreePathRoot}}
|
||||||
<div class="clone-panel ui action tiny input">
|
{{$treeNameIdxLast := Eval $treeNamesLen "-" 1}}
|
||||||
{{template "repo/clone_buttons" .}}
|
<span class="breadcrumb repo-path tw-ml-1">
|
||||||
<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
|
||||||
{{svg "octicon-kebab-horizontal"}}
|
{{- range $i, $v := .TreeNames -}}
|
||||||
<div class="menu">
|
<span class="breadcrumb-divider">/</span>
|
||||||
{{if not $.DisableDownloadSourceArchives}}
|
{{- if eq $i $treeNameIdxLast -}}
|
||||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
|
<span class="active section" title="{{$v}}">{{$v}}</span>
|
||||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
|
<button class="btn interact-fg tw-mx-1" data-clipboard-text="{{$.TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button>
|
||||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
|
{{- else -}}
|
||||||
{{end}}
|
{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span>
|
||||||
{{if .CitiationExist}}
|
{{- end -}}
|
||||||
<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
|
{{- end -}}
|
||||||
{{end}}
|
</span>
|
||||||
{{range .OpenWithEditorApps}}
|
{{end}}
|
||||||
<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/cite/cite_modal" .}}
|
|
||||||
{{end}}
|
<div class="repo-button-row-right">
|
||||||
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
<!-- Only show clone panel in repository home page -->
|
||||||
<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
|
{{if $isTreePathRoot}}
|
||||||
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
|
<div class="clone-panel ui action tiny input">
|
||||||
</a>
|
{{template "repo/clone_buttons" .}}
|
||||||
|
<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||||
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
|
<div class="menu">
|
||||||
|
{{if not $.DisableDownloadSourceArchives}}
|
||||||
|
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
|
||||||
|
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
|
||||||
|
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
|
||||||
|
{{end}}
|
||||||
|
{{range .OpenWithEditorApps}}
|
||||||
|
<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
||||||
|
</div>
|
||||||
|
{{template "repo/cite/cite_modal" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
||||||
|
<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
|
||||||
|
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .IsViewFile}}
|
||||||
|
{{template "repo/view_file" .}}
|
||||||
|
{{else if .IsBlame}}
|
||||||
|
{{template "repo/blame" .}}
|
||||||
|
{{else}}{{/* IsViewDirectory */}}
|
||||||
|
{{template "repo/view_list" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if $showSidebar}}
|
||||||
|
<div class="repo-home-sidebar-top">{{template "repo/home_sidebar_top" .}}</div>
|
||||||
|
<div class="repo-home-sidebar-bottom">{{template "repo/home_sidebar_bottom" .}}</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .IsViewFile}}
|
|
||||||
{{template "repo/view_file" .}}
|
|
||||||
{{else if .IsBlame}}
|
|
||||||
{{template "repo/blame" .}}
|
|
||||||
{{else}}{{/* IsViewDirectory */}}
|
|
||||||
{{template "repo/view_list" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
59
templates/repo/home_sidebar_bottom.tmpl
Normal file
59
templates/repo/home_sidebar_bottom.tmpl
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<div class="flex-list">
|
||||||
|
{{if .LatestRelease}}
|
||||||
|
<div class="flex-item">
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
<a class="item muted" href="{{.Link}}/releases">
|
||||||
|
{{ctx.Locale.Tr "repo.releases"}}
|
||||||
|
<span class="ui small label">{{.NumReleases}}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item">
|
||||||
|
<div class="flex-item-icon">
|
||||||
|
{{svg "octicon-tag" 16}}
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-header">
|
||||||
|
<div class="flex-item-title tw-gap-2">
|
||||||
|
<a class="gt-ellipsis muted" href="{{.LatestRelease.Link}}" title="{{.LatestRelease.Title}}">{{.LatestRelease.Title}}</a>
|
||||||
|
<span class="ui basic green label tw-h-100">{{ctx.Locale.Tr "latest"}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-body">
|
||||||
|
<span class="time">{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if and (not .IsEmptyRepo) .LanguageStats}}
|
||||||
|
<div class="flex-item">
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
{{ctx.Locale.Tr "repo.repo_lang"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-item-body">
|
||||||
|
<div class="language-stats">
|
||||||
|
{{range .LanguageStats}}
|
||||||
|
<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content={{.Language}} data-tooltip-follow-cursor="horizontal"></div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="language-stats-details">
|
||||||
|
{{range .LanguageStats}}
|
||||||
|
<div class="item">
|
||||||
|
<i class="color-icon" style="background-color: {{.Color}}"></i>
|
||||||
|
<span class="tw-font-semibold">
|
||||||
|
{{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}}
|
||||||
|
</span>
|
||||||
|
{{.Percentage}}%
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
67
templates/repo/home_sidebar_top.tmpl
Normal file
67
templates/repo/home_sidebar_top.tmpl
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<form class="ignore-dirty tw-flex tw-flex-1 tw-mt-1" action="{{.RepoLink}}/search" method="get">
|
||||||
|
<div class="ui small action input tw-flex-1">
|
||||||
|
<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
|
||||||
|
{{template "shared/search/button"}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex-list">
|
||||||
|
<div class="flex-item">
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
{{ctx.Locale.Tr "repo.repo_desc"}}
|
||||||
|
</div>
|
||||||
|
{{if and (not .HideRepoInfo) (not .IsBlame)}}
|
||||||
|
<div class="flex-item-body repo-description tw-break-anywhere tw-gap-2 tw-mt-2">
|
||||||
|
{{- $description := .Repository.DescriptionHTML ctx -}}
|
||||||
|
{{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.repo_no_desc"}}{{end}}
|
||||||
|
{{if .Repository.Website}}{{svg "octicon-link"}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
|
||||||
|
{{/* !!!! it SHOULD and MUST match the code in issue-home.js */}}
|
||||||
|
{{range .Topics}}<a class="repo-topic ui large label gt-ellipsis" title={{.Name}} href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
|
||||||
|
</div>
|
||||||
|
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
|
||||||
|
<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
|
||||||
|
<div class="ui form tw-hidden flex-item-body tw-gap-2 tw-my-2" id="topic_edit">
|
||||||
|
<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
|
||||||
|
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
|
||||||
|
{{range .Topics}}
|
||||||
|
{{/* keep the same layout as Fomantic UI generated labels */}}
|
||||||
|
<a class="ui label transition visible tw-cursor-default tw-inline-block repo-topic" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
|
||||||
|
{{end}}
|
||||||
|
<div class="text"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="ui primary button" id="save_topic" data-link="{{.RepoLink}}/topics">{{ctx.Locale.Tr "save"}}</button>
|
||||||
|
<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .ReadmeExist}}
|
||||||
|
<div class="flex-item-body tw-mt-2">
|
||||||
|
<a class="tw-flex tw-items-center tw-gap-2 muted" href="{{.TreeLink}}/{{.FileName}}">
|
||||||
|
{{svg "octicon-book"}}{{ctx.Locale.Tr "readme"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .DetectedRepoLicenses}}
|
||||||
|
<div class="flex-item-body">
|
||||||
|
<a class="tw-flex tw-items-center tw-gap-2 muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" title="{{StringUtils.Join .DetectedRepoLicenses ", "}}">
|
||||||
|
{{svg "octicon-law"}}{{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .CitiationExist}}
|
||||||
|
<div class="flex-item-body">
|
||||||
|
<a class="tw-flex tw-items-center tw-gap-2 muted" id="cite-repo-button">
|
||||||
|
{{svg "octicon-cross-reference"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -13,11 +13,6 @@
|
|||||||
{{svg "octicon-tag"}} <b>{{ctx.Locale.PrettyNumber .NumTags}}</b> {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}
|
{{svg "octicon-tag"}} <b>{{ctx.Locale.PrettyNumber .NumTags}}</b> {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .DetectedRepoLicenses}}
|
|
||||||
<a class="item muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" data-tooltip-placement="top" data-tooltip-content="{{StringUtils.Join .DetectedRepoLicenses ", "}}">
|
|
||||||
{{svg "octicon-law"}} <b>{{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}</b>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
|
<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
|
||||||
{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
|
{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
|
||||||
{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
|
{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
|
||||||
@ -25,27 +20,5 @@
|
|||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo) .LanguageStats}}
|
|
||||||
<div class="ui segment sub-menu language-stats-details tw-hidden">
|
|
||||||
{{range .LanguageStats}}
|
|
||||||
<div class="item">
|
|
||||||
<i class="color-icon" style="background-color: {{.Color}}"></i>
|
|
||||||
<span class="tw-font-semibold">
|
|
||||||
{{if eq .Language "other"}}
|
|
||||||
{{ctx.Locale.Tr "repo.language_other"}}
|
|
||||||
{{else}}
|
|
||||||
{{.Language}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
{{.Percentage}}%
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<a class="ui segment language-stats show-panel toggle" data-panel=".repository-summary > .sub-menu">
|
|
||||||
{{range .LanguageStats}}
|
|
||||||
<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content={{.Language}} data-tooltip-follow-cursor="horizontal"></div>
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<table id="repo-files-table" class="ui single line table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
<table id="repo-files-table" class="ui single line fixed table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="commit-list">
|
<tr class="commit-list">
|
||||||
<th class="tw-overflow-hidden" colspan="2">
|
<th class="tw-overflow-hidden" colspan="2">
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
@import "./repo/linebutton.css";
|
@import "./repo/linebutton.css";
|
||||||
@import "./repo/wiki.css";
|
@import "./repo/wiki.css";
|
||||||
@import "./repo/header.css";
|
@import "./repo/header.css";
|
||||||
|
@import "./repo/home.css";
|
||||||
@import "./repo/reactions.css";
|
@import "./repo/reactions.css";
|
||||||
|
|
||||||
@import "./editor/fileeditor.css";
|
@import "./editor/fileeditor.css";
|
||||||
|
@ -422,14 +422,6 @@ td .commit-summary {
|
|||||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list .sidebar {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.file.list .sidebar .svg {
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-editor-header {
|
.repo-editor-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -1822,16 +1814,6 @@ td .commit-summary {
|
|||||||
background: var(--color-secondary);
|
background: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .repository-summary .segment.language-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
padding: 0;
|
|
||||||
height: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: 0 0 3px 3px !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cite-repo-modal #citation-panel {
|
#cite-repo-modal #citation-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -2172,11 +2154,7 @@ td .commit-summary {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-button-row[data-is-homepage="false"] .repo-button-row-right {
|
@media (max-width: 1200px) {
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.repository:not(.wiki) .repo-button-row {
|
.repository:not(.wiki) .repo-button-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@ -2302,6 +2280,7 @@ tbody.commit-list {
|
|||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: inline-block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#new-dependency-drop-list.ui.selection.dropdown {
|
#new-dependency-drop-list.ui.selection.dropdown {
|
||||||
@ -2820,9 +2799,9 @@ tbody.commit-list {
|
|||||||
/* FIXME: These media selectors are not ideal (just keep them from old code).
|
/* FIXME: These media selectors are not ideal (just keep them from old code).
|
||||||
There are many different pages, some need the max-width while some others don't,
|
There are many different pages, some need the max-width while some others don't,
|
||||||
they should be tested and improved in the future. */
|
they should be tested and improved in the future. */
|
||||||
@media (min-width: 768px) and (max-width: 991.98px) {
|
@media (min-width: 768px) and (max-width: 1235px) {
|
||||||
.branch-selector-dropdown .branch-dropdown-button {
|
.branch-selector-dropdown .branch-dropdown-button {
|
||||||
max-width: 185px;
|
max-width: 301px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
77
web_src/css/repo/home.css
Normal file
77
web_src/css/repo/home.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
.repo-grid-filelist-sidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 300px;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-filelist-sidebar .repo-home-filelist {
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid-filelist-sidebar .repo-home-sidebar-top {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
.repo-grid-filelist-sidebar .repo-home-sidebar-bottom {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
.repo-home-sidebar-bottom > :first-child {
|
||||||
|
border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.repo-grid-filelist-sidebar {
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
}
|
||||||
|
.repo-grid-filelist-sidebar .repo-home-filelist {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
.repo-grid-filelist-sidebar .repo-home-sidebar-top {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.repo-grid-filelist-sidebar .repo-home-sidebar-bottom {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.repo-home-sidebar-bottom > :first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0;
|
||||||
|
height: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-stats-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-stats-details .item {
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
padding: 0 0.5em; /* make the UI look better for narrow (mobile) view */
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
@ -41,35 +41,28 @@ export async function initCitationFileCopyContent() {
|
|||||||
citationCopyApa.classList.toggle('primary', !isBibtex);
|
citationCopyApa.classList.toggle('primary', !isBibtex);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async (e: MouseEvent & {target: HTMLAnchorElement}) => {
|
document.querySelector('#cite-repo-button')?.addEventListener('click', async () => {
|
||||||
const dropdownBtn = e.target.closest('.ui.dropdown.button');
|
|
||||||
dropdownBtn.classList.add('is-loading');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
await initInputCitationValue(citationCopyApa, citationCopyBibtex);
|
||||||
await initInputCitationValue(citationCopyApa, citationCopyBibtex);
|
} catch (e) {
|
||||||
} catch (e) {
|
console.error(`initCitationFileCopyContent error: ${e}`, e);
|
||||||
console.error(`initCitationFileCopyContent error: ${e}`, e);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateUi();
|
|
||||||
|
|
||||||
citationCopyApa.addEventListener('click', () => {
|
|
||||||
localStorage.setItem('citation-copy-format', 'apa');
|
|
||||||
updateUi();
|
|
||||||
});
|
|
||||||
|
|
||||||
citationCopyBibtex.addEventListener('click', () => {
|
|
||||||
localStorage.setItem('citation-copy-format', 'bibtex');
|
|
||||||
updateUi();
|
|
||||||
});
|
|
||||||
|
|
||||||
inputContent.addEventListener('click', () => {
|
|
||||||
inputContent.select();
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
dropdownBtn.classList.remove('is-loading');
|
|
||||||
}
|
}
|
||||||
|
updateUi();
|
||||||
|
|
||||||
|
citationCopyApa.addEventListener('click', () => {
|
||||||
|
localStorage.setItem('citation-copy-format', 'apa');
|
||||||
|
updateUi();
|
||||||
|
});
|
||||||
|
|
||||||
|
citationCopyBibtex.addEventListener('click', () => {
|
||||||
|
localStorage.setItem('citation-copy-format', 'bibtex');
|
||||||
|
updateUi();
|
||||||
|
});
|
||||||
|
|
||||||
|
inputContent.addEventListener('click', () => {
|
||||||
|
inputContent.select();
|
||||||
|
});
|
||||||
|
|
||||||
fomanticQuery('#cite-repo-modal').modal('show');
|
fomanticQuery('#cite-repo-modal').modal('show');
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
|||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
export function initRepoTopicBar() {
|
export function initRepoTopicBar() {
|
||||||
const mgrBtn = document.querySelector('#manage_topic');
|
const mgrBtn = document.querySelector<HTMLButtonElement>('#manage_topic');
|
||||||
if (!mgrBtn) return;
|
if (!mgrBtn) return;
|
||||||
|
|
||||||
const editDiv = document.querySelector('#topic_edit');
|
const editDiv = document.querySelector('#topic_edit');
|
||||||
@ -18,7 +18,7 @@ export function initRepoTopicBar() {
|
|||||||
mgrBtn.addEventListener('click', () => {
|
mgrBtn.addEventListener('click', () => {
|
||||||
hideElem(viewDiv);
|
hideElem(viewDiv);
|
||||||
showElem(editDiv);
|
showElem(editDiv);
|
||||||
topicDropdown.querySelector('input.search').focus();
|
topicDropdown.querySelector<HTMLInputElement>('input.search').focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
|
document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
|
||||||
@ -28,9 +28,9 @@ export function initRepoTopicBar() {
|
|||||||
mgrBtn.focus();
|
mgrBtn.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#save_topic').addEventListener('click', async (e) => {
|
document.querySelector('#save_topic').addEventListener('click', async (e: MouseEvent & {target: HTMLButtonElement}) => {
|
||||||
lastErrorToast?.hideToast();
|
lastErrorToast?.hideToast();
|
||||||
const topics = editDiv.querySelector('input[name=topics]').value;
|
const topics = editDiv.querySelector<HTMLInputElement>('input[name=topics]').value;
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('topics', topics);
|
data.append('topics', topics);
|
||||||
@ -45,12 +45,13 @@ export function initRepoTopicBar() {
|
|||||||
const topicArray = topics.split(',');
|
const topicArray = topics.split(',');
|
||||||
topicArray.sort();
|
topicArray.sort();
|
||||||
for (const topic of topicArray) {
|
for (const topic of topicArray) {
|
||||||
// it should match the code in repo/home.tmpl
|
// TODO: sort items in topicDropdown, or items in edit div will have different order to the items in view div
|
||||||
|
// !!!! it SHOULD and MUST match the code in "home_sidebar_top.tmpl" !!!!
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.classList.add('repo-topic', 'ui', 'large', 'label');
|
link.classList.add('repo-topic', 'ui', 'large', 'label', 'gt-ellipsis');
|
||||||
link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
|
link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
|
||||||
link.textContent = topic;
|
link.textContent = topic;
|
||||||
mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
|
viewDiv.append(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hideElem(editDiv);
|
hideElem(editDiv);
|
||||||
|
Loading…
Reference in New Issue
Block a user