// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo import ( "errors" "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) } 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.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 prepareUpstreamDivergingInfo(ctx *context.Context) { if !ctx.Repo.Repository.IsFork || !ctx.Repo.IsViewBranch || 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 prepareRecentlyPushedNewBranches(ctx *context.Context) { 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) } } } } func handleRepoEmptyOrBroken(ctx *context.Context) { 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) } func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { return func(ctx *context.Context) { if entry.IsDir() { prepareToRenderDirectory(ctx) } else { prepareToRenderFile(ctx, entry) } } } func handleRepoHomeFeed(ctx *context.Context) bool { 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 true } } return false } // Home render repository home page func Home(ctx *context.Context) { if handleRepoHomeFeed(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 len(ctx.Repo.Repository.Description) > 0 { 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 } // 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 } // prepare the tree path 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] } } 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 } } ctx.HTML(http.StatusOK, tplRepoHome) }