| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package repo
-
- import (
- "errors"
- "fmt"
- "html/template"
- "net/http"
- "path"
- "strconv"
- "strings"
- "time"
-
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/htmlutil"
- "code.gitea.io/gitea/modules/httplib"
- "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("gitea-"+schema, 16)
- } else {
- iconHTML = svg.RenderHTML("gitea-git", 16) // 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(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
- }
-
- 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.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.Redirect(submoduleWebLink.CommitWebLink)
- }
- }
-
- func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
- return func(ctx *context.Context) {
- 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
- }
-
- // 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
- ctx.Data["PageIsViewCode"] = true
- ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons
-
- 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
- }
-
- // prepare the tree path
- var treeNames, paths []string
- branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.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
-
- // 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,
- prepareHomeSidebarCitationFile(entry),
- prepareHomeSidebarLanguageStats,
- prepareHomeSidebarLatestRelease,
- )
- }
-
- for _, prepare := range prepareFuncs {
- prepare(ctx)
- if ctx.Written() {
- return
- }
- }
-
- if isViewHomeOnlyContent(ctx) {
- ctx.HTML(http.StatusOK, tplRepoViewContent)
- } else if len(treeNames) != 0 {
- 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)
- }
|