Some checks failed
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m19s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m39s
Build and Release / Lint (push) Successful in 4m46s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Add API.md (3200+ lines) with complete REST API documentation covering authentication, repository management, issues, pull requests, organizations, package registry, Actions, and Vault APIs. Includes code examples and error handling. Add GUIDE.md with user-focused documentation for getting started, repository operations, collaboration workflows, and CI/CD setup. Implement documentation tab in repository view with automatic detection and rendering of API.md and GUIDE.md files alongside README. Add locale strings and template for doc tab navigation.
707 lines
22 KiB
Go
707 lines
22 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitcaddy.com/server/v3/models/db"
|
|
git_model "code.gitcaddy.com/server/v3/models/git"
|
|
"code.gitcaddy.com/server/v3/models/renderhelper"
|
|
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
|
unit_model "code.gitcaddy.com/server/v3/models/unit"
|
|
user_model "code.gitcaddy.com/server/v3/models/user"
|
|
"code.gitcaddy.com/server/v3/modules/git"
|
|
"code.gitcaddy.com/server/v3/modules/gitrepo"
|
|
"code.gitcaddy.com/server/v3/modules/htmlutil"
|
|
"code.gitcaddy.com/server/v3/modules/httplib"
|
|
"code.gitcaddy.com/server/v3/modules/json"
|
|
"code.gitcaddy.com/server/v3/modules/log"
|
|
"code.gitcaddy.com/server/v3/modules/markup/markdown"
|
|
"code.gitcaddy.com/server/v3/modules/plugins"
|
|
repo_module "code.gitcaddy.com/server/v3/modules/repository"
|
|
"code.gitcaddy.com/server/v3/modules/setting"
|
|
"code.gitcaddy.com/server/v3/modules/svg"
|
|
"code.gitcaddy.com/server/v3/modules/util"
|
|
"code.gitcaddy.com/server/v3/routers/web/feed"
|
|
"code.gitcaddy.com/server/v3/services/context"
|
|
repo_service "code.gitcaddy.com/server/v3/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 iconName string
|
|
switch schema {
|
|
case "vscode":
|
|
iconName = "octicon-vscode"
|
|
case "vscodium":
|
|
iconName = "gitea-vscodium"
|
|
case "jetbrains":
|
|
iconName = "gitea-jetbrains"
|
|
default:
|
|
// TODO: it could support user's customized icon in the future
|
|
iconName = "gitea-git"
|
|
}
|
|
|
|
tmplApps = append(tmplApps, map[string]any{
|
|
"DisplayName": app.DisplayName,
|
|
"OpenURL": app.OpenURL,
|
|
"IconHTML": svg.RenderHTML(iconName, 16),
|
|
})
|
|
}
|
|
ctx.Data["OpenWithEditorApps"] = tmplApps
|
|
}
|
|
|
|
func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
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
|
|
}
|
|
|
|
// prepareHomeLicenseTab loads license file content for the License tab
|
|
func prepareHomeLicenseTab(ctx *context.Context) {
|
|
ctx.Data["LicenseTabExist"] = false
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
return
|
|
}
|
|
|
|
commit := ctx.Repo.Commit
|
|
if commit == nil {
|
|
return
|
|
}
|
|
|
|
// Check for LICENSE files
|
|
licenseFiles := []string{"LICENSE.md", "LICENSE", "LICENSE.txt", "COPYING"}
|
|
for _, filename := range licenseFiles {
|
|
entry, err := commit.GetTreeEntryByPath(filename)
|
|
if err == nil && !entry.IsDir() {
|
|
ctx.Data["LicenseTabExist"] = true
|
|
ctx.Data["LicenseTabFileName"] = filename
|
|
|
|
// Load license content for rendering
|
|
content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize)
|
|
if err != nil {
|
|
log.Error("prepareHomeLicenseTab: GetBlobContent: %v", err)
|
|
return
|
|
}
|
|
ctx.Data["LicenseTabContent"] = content
|
|
ctx.Data["LicenseTabIsMarkdown"] = strings.HasSuffix(strings.ToLower(filename), ".md")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// GalleryImage represents an image in the .gallery folder
|
|
type GalleryImage struct {
|
|
Name string
|
|
Caption string
|
|
}
|
|
|
|
// prepareHomeGalleryTab loads gallery images for the Gallery tab
|
|
func prepareHomeGalleryTab(ctx *context.Context) {
|
|
ctx.Data["GalleryTabExist"] = false
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
return
|
|
}
|
|
|
|
commit := ctx.Repo.Commit
|
|
if commit == nil {
|
|
return
|
|
}
|
|
|
|
// Check if .gallery folder exists
|
|
galleryEntry, err := commit.GetTreeEntryByPath(".gallery")
|
|
if err != nil || !galleryEntry.IsDir() {
|
|
return
|
|
}
|
|
|
|
// Load metadata if exists
|
|
var metadata struct {
|
|
Images []GalleryImage `json:"images"`
|
|
}
|
|
if metaEntry, err := commit.GetTreeEntryByPath(".gallery/gallery.json"); err == nil {
|
|
if content, err := metaEntry.Blob().GetBlobContent(100000); err == nil {
|
|
_ = json.Unmarshal([]byte(content), &metadata)
|
|
}
|
|
}
|
|
metadataMap := make(map[string]string)
|
|
for _, img := range metadata.Images {
|
|
metadataMap[img.Name] = img.Caption
|
|
}
|
|
|
|
// Get all images in .gallery folder
|
|
tree, err := commit.SubTree(".gallery")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
entries, err := tree.ListEntries()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var images []GalleryImage
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
// Skip metadata file
|
|
if name == "gallery.json" {
|
|
continue
|
|
}
|
|
// Only include image files
|
|
if isGalleryImageFile(name) {
|
|
images = append(images, GalleryImage{
|
|
Name: name,
|
|
Caption: metadataMap[name],
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(images) > 0 {
|
|
ctx.Data["GalleryTabExist"] = true
|
|
ctx.Data["GalleryTabImages"] = images
|
|
}
|
|
}
|
|
|
|
// isGalleryImageFile checks if filename has an image extension
|
|
func isGalleryImageFile(filename string) bool {
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
switch ext {
|
|
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// prepareHomeDocTab loads a markdown doc file for a named tab on the repo home page.
|
|
// keyPrefix is used to namespace context data (e.g. "Guide" → "GuideTabExist", "GuideTabContent").
|
|
func prepareHomeDocTab(ctx *context.Context, filename, keyPrefix string) {
|
|
ctx.Data[keyPrefix+"TabExist"] = false
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
return
|
|
}
|
|
|
|
commit := ctx.Repo.Commit
|
|
if commit == nil {
|
|
return
|
|
}
|
|
|
|
entry, err := commit.GetTreeEntryByPath(filename)
|
|
if err != nil || entry.IsDir() {
|
|
return
|
|
}
|
|
|
|
ctx.Data[keyPrefix+"TabExist"] = true
|
|
ctx.Data[keyPrefix+"TabFileName"] = filename
|
|
|
|
// Load content
|
|
content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize)
|
|
if err != nil {
|
|
log.Error("prepareHomeDocTab(%s): GetBlobContent: %v", filename, err)
|
|
return
|
|
}
|
|
|
|
// Render markdown using the full markup pipeline
|
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
|
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
|
CurrentTreePath: ".",
|
|
}).WithMarkupType("markdown").WithRelativePath(filename)
|
|
|
|
rendered, err := markdown.RenderString(rctx, content)
|
|
if err != nil {
|
|
log.Error("prepareHomeDocTab(%s): RenderString: %v", filename, err)
|
|
ctx.Data[keyPrefix+"TabContent"] = content
|
|
ctx.Data[keyPrefix+"TabIsMarkdown"] = false
|
|
return
|
|
}
|
|
ctx.Data[keyPrefix+"TabContent"] = rendered
|
|
ctx.Data[keyPrefix+"TabIsMarkdown"] = true
|
|
}
|
|
|
|
// prepareHomeGuideTab loads GUIDE.md for the User Guide tab
|
|
func prepareHomeGuideTab(ctx *context.Context) {
|
|
prepareHomeDocTab(ctx, "GUIDE.md", "Guide")
|
|
}
|
|
|
|
// prepareHomeAPIDocTab loads API.md for the API tab
|
|
func prepareHomeAPIDocTab(ctx *context.Context) {
|
|
prepareHomeDocTab(ctx, "API.md", "APIDoc")
|
|
}
|
|
|
|
func prepareToRenderDirectory(ctx *context.Context) {
|
|
entries := renderDirectoryFiles(ctx, 1*time.Second)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
// Check if vault plugin is available for Move to Vault action in file list
|
|
ctx.Data["CanMoveToVault"] = plugins.Get("vault") != nil
|
|
|
|
if ctx.Repo.TreePath != "" {
|
|
ctx.Data["HideRepoInfo"] = true
|
|
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefFullName.ShortName())
|
|
}
|
|
|
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, 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 prepareUpstreamDivergingInfo(ctx *context.Context) {
|
|
if !ctx.Repo.Repository.IsFork || !ctx.Repo.RefFullName.IsBranch() || ctx.Repo.TreePath != "" {
|
|
return
|
|
}
|
|
upstreamDivergingInfo, err := repo_service.GetUpstreamDivergingInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName)
|
|
if err != nil {
|
|
if !errors.Is(err, util.ErrNotExist) && !errors.Is(err, util.ErrInvalidArgument) {
|
|
log.Error("GetUpstreamDivergingInfo: %v", err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo
|
|
}
|
|
|
|
func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
|
|
if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status {
|
|
return
|
|
}
|
|
ctx.Repo.Repository.IsEmpty = empty
|
|
if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken {
|
|
ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is
|
|
}
|
|
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil {
|
|
ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func handleRepoEmptyOrBroken(ctx *context.Context) {
|
|
showEmpty := true
|
|
if ctx.Repo.GitRepo == nil {
|
|
// in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
|
|
ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
|
}
|
|
if ctx.Repo.GitRepo != nil {
|
|
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
|
|
if err != nil {
|
|
showEmpty = true // the repo is broken
|
|
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken)
|
|
log.Error("GitRepo.IsEmpty: %v", err)
|
|
ctx.Flash.Error(ctx.Tr("error.occurred"), true)
|
|
} else if reallyEmpty {
|
|
showEmpty = true // the repo is really empty
|
|
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
|
|
} else if branches, _, _ := ctx.Repo.GitRepo.GetBranchNames(0, 1); len(branches) == 0 {
|
|
showEmpty = true // it is not really empty, but there is no branch
|
|
// at the moment, other repo units like "actions" are not able to handle such case,
|
|
// so we just mark the repo as empty to prevent from displaying these units.
|
|
ctx.Data["RepoHasContentsWithoutBranch"] = true
|
|
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
|
|
} else {
|
|
// the repo is actually not empty and has branches, need to update the database later
|
|
showEmpty = false
|
|
}
|
|
}
|
|
if showEmpty {
|
|
ctx.HTML(http.StatusOK, tplRepoEMPTY)
|
|
return
|
|
}
|
|
|
|
// The repo is not really empty, so we should update the model in database, such problem may be caused by:
|
|
// 1) an error occurs during pushing/receiving.
|
|
// 2) the user replaces an empty git repo manually.
|
|
updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady)
|
|
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)
|
|
}
|
|
|
|
func isViewHomeOnlyContent(ctx *context.Context) bool {
|
|
return ctx.FormBool("only_content")
|
|
}
|
|
|
|
func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.CommitSubmoduleFile) {
|
|
submoduleWebLink := commitSubmoduleFile.SubmoduleWebLinkTree(ctx)
|
|
if submoduleWebLink == nil {
|
|
ctx.Data["NotFoundPrompt"] = ctx.Repo.TreePath
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
|
|
redirectLink := submoduleWebLink.CommitWebLink
|
|
if isViewHomeOnlyContent(ctx) {
|
|
ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`<a href="%s">%s</a>`, redirectLink, redirectLink)))
|
|
} else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) {
|
|
// don't auto-redirect to external URL, to avoid open redirect or phishing
|
|
ctx.Data["NotFoundPrompt"] = redirectLink
|
|
ctx.NotFound(nil)
|
|
} else {
|
|
ctx.RedirectToCurrentSite(redirectLink)
|
|
}
|
|
}
|
|
|
|
// isDotfilePath checks if any segment of the path starts with "."
|
|
func isDotfilePath(treePath string) bool {
|
|
for segment := range strings.SplitSeq(treePath, "/") {
|
|
if strings.HasPrefix(segment, ".") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
// Block non-admin access to hidden folder contents and dotfile paths
|
|
if ctx.Repo.TreePath != "" && !ctx.Repo.IsAdmin() {
|
|
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
|
|
if len(hiddenPaths) > 0 && repo_model.IsPathUnderHiddenFolder(hiddenPaths, ctx.Repo.TreePath) {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
if ctx.Repo.Repository.HideDotfiles && isDotfilePath(ctx.Repo.TreePath) {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
if entry.IsSubModule() {
|
|
commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
|
|
if err != nil {
|
|
HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err)
|
|
return
|
|
}
|
|
handleRepoViewSubmodule(ctx, commitSubmoduleFile)
|
|
} else if entry.IsDir() {
|
|
prepareToRenderDirectory(ctx)
|
|
} else {
|
|
prepareFileView(ctx, entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleRepoHomeFeed(ctx *context.Context) bool {
|
|
if !setting.Other.EnableFeed {
|
|
return false
|
|
}
|
|
isFeed, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req)
|
|
if !isFeed {
|
|
return false
|
|
}
|
|
if ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType) {
|
|
feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
|
|
} else if ctx.Repo.TreePath == "" {
|
|
feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
|
|
} else {
|
|
feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
|
|
showFileTree := true
|
|
if ctx.Doer != nil {
|
|
v, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyCodeViewShowFileTree, "true")
|
|
if err != nil {
|
|
log.Error("GetUserSetting: %v", err)
|
|
} else {
|
|
showFileTree, _ = strconv.ParseBool(v)
|
|
}
|
|
}
|
|
ctx.Data["UserSettingCodeViewShowFileTree"] = showFileTree
|
|
}
|
|
|
|
func redirectSrcToRaw(ctx *context.Context) bool {
|
|
// GitHub redirects a tree path with "?raw=1" to the raw path
|
|
// It is useful to embed some raw contents into Markdown files,
|
|
// then viewing the Markdown in "src" path could embed the raw content correctly.
|
|
if ctx.Repo.TreePath != "" && ctx.FormBool("raw") {
|
|
ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) bool {
|
|
if ctx.Repo.TreePath == "" || !ctx.FormBool("follow_symlink") {
|
|
return false
|
|
}
|
|
if treePathEntry.IsLink() {
|
|
if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil {
|
|
redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery
|
|
ctx.Redirect(redirect)
|
|
return true
|
|
} // else: don't handle the links we cannot resolve, so ignore the error
|
|
}
|
|
return false
|
|
}
|
|
|
|
func prepareRepoViewContent(ctx *context.Context, refTypeNameSubURL string) {
|
|
// for: home, file list, file view, blame
|
|
ctx.Data["PageIsViewCode"] = true
|
|
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show Upload File button or menu item
|
|
|
|
// prepare the tree path navigation
|
|
var treeNames, paths []string
|
|
branchLink := ctx.Repo.RepoLink + "/src/" + refTypeNameSubURL
|
|
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]
|
|
}
|
|
}
|
|
ctx.Data["Paths"] = paths
|
|
ctx.Data["TreeLink"] = treeLink
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["BranchLink"] = branchLink
|
|
}
|
|
|
|
// Home render repository home page
|
|
func Home(ctx *context.Context) {
|
|
if handleRepoHomeFeed(ctx) {
|
|
return
|
|
}
|
|
if redirectSrcToRaw(ctx) {
|
|
return
|
|
}
|
|
|
|
// Check whether the repo is viewable: not in migration, and the code unit should be enabled
|
|
// Ideally the "feed" logic should be after this, but old code did so, so keep it as-is.
|
|
checkHomeCodeViewable(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
|
if ctx.Repo.Repository.Description != "" {
|
|
title += ": " + ctx.Repo.Repository.Description
|
|
}
|
|
ctx.Data["Title"] = title
|
|
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
|
|
|
|
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
|
// empty or broken repositories need to be handled differently
|
|
handleRepoEmptyOrBroken(ctx)
|
|
return
|
|
}
|
|
|
|
prepareHomeTreeSideBarSwitch(ctx)
|
|
|
|
// get the current git entry which doer user is currently looking at.
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
|
if err != nil {
|
|
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
|
return
|
|
}
|
|
|
|
if redirectFollowSymlink(ctx, entry) {
|
|
return
|
|
}
|
|
|
|
// some UI components are only shown when the tree path is root
|
|
isTreePathRoot := ctx.Repo.TreePath == ""
|
|
|
|
prepareFuncs := []func(*context.Context){
|
|
prepareOpenWithEditorApps,
|
|
prepareHomeSidebarRepoTopics,
|
|
checkOutdatedBranch,
|
|
prepareToRenderDirOrFile(entry),
|
|
prepareRecentlyPushedNewBranches,
|
|
}
|
|
|
|
if isTreePathRoot {
|
|
prepareFuncs = append(prepareFuncs,
|
|
prepareUpstreamDivergingInfo,
|
|
prepareHomeSidebarLicenses,
|
|
prepareHomeLicenseTab,
|
|
prepareHomeGuideTab,
|
|
prepareHomeAPIDocTab,
|
|
prepareHomeGalleryTab,
|
|
prepareHomeSidebarCitationFile(entry),
|
|
prepareHomeSidebarLanguageStats,
|
|
prepareHomeSidebarLatestRelease,
|
|
)
|
|
}
|
|
|
|
for _, prepare := range prepareFuncs {
|
|
prepare(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
}
|
|
|
|
if isViewHomeOnlyContent(ctx) {
|
|
ctx.HTML(http.StatusOK, tplRepoViewContent)
|
|
} else if ctx.Repo.TreePath != "" {
|
|
ctx.HTML(http.StatusOK, tplRepoView)
|
|
} else {
|
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
|
}
|
|
}
|
|
|
|
func RedirectRepoTreeToSrc(ctx *context.Context) {
|
|
// Redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*",
|
|
// then use the deprecated "/src/*" handler to guess the ref type and render a file list page.
|
|
// This is done intentionally so that Gitea's repo URL structure matches other forges (GitHub/GitLab) provide,
|
|
// allowing us to construct submodule URLs across forges easily.
|
|
// For example, when viewing a submodule, we can simply construct the link as:
|
|
// * "https://gitea/owner/repo/tree/{CommitID}"
|
|
// * "https://github/owner/repo/tree/{CommitID}"
|
|
// * "https://gitlab/owner/repo/tree/{CommitID}"
|
|
// Then no matter which forge the submodule is using, the link works.
|
|
redirect := ctx.Repo.RepoLink + "/src/" + ctx.PathParamRaw("*")
|
|
if ctx.Req.URL.RawQuery != "" {
|
|
redirect += "?" + ctx.Req.URL.RawQuery
|
|
}
|
|
ctx.Redirect(redirect)
|
|
}
|
|
|
|
func RedirectRepoBlobToCommit(ctx *context.Context) {
|
|
// redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
|
|
// just like GitHub: browse files of a commit by "https://github/owner/repo/blob/{CommitID}"
|
|
// TODO: maybe we could guess more types to redirect to the related pages in the future
|
|
redirect := ctx.Repo.RepoLink + "/src/commit/" + ctx.PathParamRaw("*")
|
|
if ctx.Req.URL.RawQuery != "" {
|
|
redirect += "?" + ctx.Req.URL.RawQuery
|
|
}
|
|
ctx.Redirect(redirect)
|
|
}
|