Files

414 lines
13 KiB
Go
Raw Permalink Normal View History

// Copyright 2017 The Gitea Authors. All rights reserved.
2014-07-26 00:24:27 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2014-07-26 00:24:27 -04:00
package repo
import (
2021-10-08 14:08:22 +01:00
gocontext "context"
2024-09-09 22:23:07 -04:00
"errors"
2016-08-09 12:56:00 -07:00
"fmt"
"html/template"
2021-01-23 01:49:13 +08:00
"io"
"net/http"
"net/url"
"path"
2014-07-26 00:24:27 -04:00
"strings"
2021-10-08 14:08:22 +01:00
"time"
2014-07-26 00:24:27 -04:00
2023-07-31 07:04:45 +02:00
_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images
_ "image/png" // for processing png images
activities_model "code.gitea.io/gitea/models/activities"
admin_model "code.gitea.io/gitea/models/admin"
2021-12-10 16:14:24 +08:00
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
2022-06-12 23:51:54 +08:00
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
2021-11-10 03:57:58 +08:00
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
2019-08-15 09:07:28 -03:00
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
2016-12-26 02:16:37 +01:00
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
2021-06-05 14:32:19 +02:00
"code.gitea.io/gitea/modules/typesniffer"
2021-10-24 23:12:43 +02:00
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
2024-10-02 04:25:08 +09:00
repo_service "code.gitea.io/gitea/services/repository"
2023-07-31 07:04:45 +02:00
_ "golang.org/x/image/bmp" // for processing bmp images
_ "golang.org/x/image/webp" // for processing webp images
2014-07-26 00:24:27 -04:00
)
const (
2025-03-15 16:26:49 +08:00
tplRepoEMPTY templates.TplName = "repo/empty"
tplRepoHome templates.TplName = "repo/home"
tplRepoView templates.TplName = "repo/view"
tplRepoViewContent templates.TplName = "repo/view_content"
tplRepoViewList templates.TplName = "repo/view_list"
tplWatchers templates.TplName = "repo/watchers"
tplForks templates.TplName = "repo/forks"
tplMigrating templates.TplName = "repo/migrate/migrating"
2014-07-26 00:24:27 -04:00
)
2022-12-14 18:11:11 +08:00
type fileInfo struct {
blobOrLfsSize int64
lfsMeta *lfs.Pointer
st typesniffer.SniffedType
2022-12-14 18:11:11 +08:00
}
2016-08-30 02:08:38 -07:00
func (fi *fileInfo) isLFSFile() bool {
return fi.lfsMeta != nil && fi.lfsMeta.Oid != ""
}
func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) {
dataRc, err = blob.DataAsync()
2022-04-27 04:31:15 +08:00
if err != nil {
2022-12-14 18:11:11 +08:00
return nil, nil, nil, err
2022-04-27 04:31:15 +08:00
}
const prefetchSize = lfs.MetaFileMaxSize
buf = make([]byte, prefetchSize)
2022-04-27 04:31:15 +08:00
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
fi = &fileInfo{blobOrLfsSize: blob.Size(), st: typesniffer.DetectContentType(buf)}
2022-04-27 04:31:15 +08:00
// FIXME: what happens when README file is an image?
if !fi.st.IsText() || !setting.LFS.StartServer {
return buf, dataRc, fi, nil
2022-12-14 18:11:11 +08:00
}
2022-04-27 04:31:15 +08:00
2022-12-14 18:11:11 +08:00
pointer, _ := lfs.ReadPointerFromBuffer(buf)
if !pointer.IsValid() { // fallback to a plain file
return buf, dataRc, fi, nil
2022-12-14 18:11:11 +08:00
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
if err != nil { // fallback to a plain file
fi.lfsMeta = &pointer
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
return buf, dataRc, fi, nil
2022-12-14 18:11:11 +08:00
}
// close the old dataRc and open the real LFS target
_ = dataRc.Close()
2022-12-14 18:11:11 +08:00
dataRc, err = lfs.ReadMetaObject(pointer)
if err != nil {
return nil, nil, nil, err
2022-04-27 04:31:15 +08:00
}
buf = make([]byte, prefetchSize)
2022-12-14 18:11:11 +08:00
n, err = util.ReadAtMost(dataRc, buf)
if err != nil {
_ = dataRc.Close()
return nil, nil, fi, err
2022-12-14 18:11:11 +08:00
}
buf = buf[:n]
fi.st = typesniffer.DetectContentType(buf)
fi.blobOrLfsSize = meta.Pointer.Size
fi.lfsMeta = &meta.Pointer
return buf, dataRc, fi, nil
2022-12-14 18:11:11 +08:00
}
2024-01-15 17:42:15 +01:00
func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
if latestCommit != nil {
verification := asymkey_service.ParseCommitWithSignature(ctx, latestCommit)
2024-01-15 17:42:15 +01:00
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
}, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return false
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
2024-01-15 17:42:15 +01:00
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
if !ctx.Repo.CanRead(unit_model.TypeActions) {
git_model.CommitStatusesHideActionsURL(ctx, statuses)
}
2024-01-15 17:42:15 +01:00
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses
}
return true
}
2026-01-26 10:34:38 +08:00
func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, renderer markup.Renderer, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
2025-10-23 07:41:38 +08:00
done := make(chan struct{})
go func() {
sb := &strings.Builder{}
2025-10-23 07:41:38 +08:00
if markup.RendererNeedPostProcess(renderer) {
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.EscapeOptionsForView())
2025-10-23 07:41:38 +08:00
} else {
escaped = &charset.EscapeStatus{}
_, _ = io.Copy(sb, markupRd)
}
output = template.HTML(sb.String())
close(done)
}()
2025-10-23 07:41:38 +08:00
err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr)
_ = markupWr.CloseWithError(err)
<-done
return escaped, output, err
}
2021-10-08 14:08:22 +01:00
func checkHomeCodeViewable(ctx *context.Context) {
if ctx.Repo.HasUnits() {
if ctx.Repo.Repository.IsBeingCreated() {
task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
if err != nil {
if admin_model.IsErrTaskDoesNotExist(err) {
2021-11-13 11:28:50 +00:00
ctx.Data["Repo"] = ctx.Repo
ctx.Data["CloneAddr"] = ""
ctx.Data["Failed"] = true
ctx.HTML(http.StatusOK, tplMigrating)
return
}
ctx.ServerError("models.GetMigratingTask", err)
return
}
cfg, err := task.MigrateConfig()
if err != nil {
ctx.ServerError("task.MigrateConfig", err)
return
}
ctx.Data["Repo"] = ctx.Repo
ctx.Data["MigrateTask"] = task
2023-09-16 18:03:02 +02:00
ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr)
ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed
ctx.HTML(http.StatusOK, tplMigrating)
return
}
if ctx.IsSigned {
// Set repo notification-status read if unread
if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("ReadBy", err)
return
}
}
2021-11-10 03:57:58 +08:00
var firstUnit *unit_model.Unit
2024-04-17 16:31:37 +08:00
for _, repoUnitType := range ctx.Repo.Permission.ReadableUnitTypes() {
if repoUnitType == unit_model.TypeCode {
// we are doing this check in "code" unit related pages, so if the code unit is readable, no need to do any further redirection
return
}
2024-04-17 16:31:37 +08:00
unit, ok := unit_model.Units[repoUnitType]
if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
firstUnit = &unit
}
2017-05-18 22:54:24 +08:00
}
if firstUnit != nil {
2021-11-16 18:18:25 +00:00
ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI))
2017-05-18 22:54:24 +08:00
return
}
}
2025-02-17 14:13:17 +08:00
ctx.NotFound(errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
2017-05-18 22:54:24 +08:00
}
2021-10-08 14:08:22 +01:00
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
func LastCommit(ctx *context.Context) {
checkHomeCodeViewable(ctx)
if ctx.Written() {
return
}
// The "/lastcommit/" endpoint is used to render the embedded HTML content for the directory file listing with latest commit info
// It needs to construct correct links to the file items, but the route only accepts a commit ID, not a full ref name (branch or tag).
// So we need to get the ref name from the query parameter "refSubUrl".
// TODO: LAST-COMMIT-ASYNC-LOADING: it needs more tests to cover this
refSubURL := path.Clean(ctx.FormString("refSubUrl"))
prepareRepoViewContent(ctx, util.IfZero(refSubURL, ctx.Repo.RefTypeNameSubURL()))
2021-10-08 14:08:22 +01:00
renderDirectoryFiles(ctx, 0)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplRepoViewList)
}
func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
renderedIconPool := fileicon.NewRenderedIconPool()
fileIcons := map[string]template.HTML{}
for _, f := range files {
fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry)
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
}
2025-04-29 10:51:32 +08:00
fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.Data["FileIcons"] = fileIcons
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
}
2021-10-08 14:08:22 +01:00
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
if err != nil {
2023-09-29 16:42:39 +09:00
HandleGitError(ctx, "Repo.Commit.SubTree", err)
2021-10-08 14:08:22 +01:00
return nil
}
// TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details
lastCommitLoaderURL := ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL())
2021-10-08 14:08:22 +01:00
// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
2023-09-29 16:42:39 +09:00
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
2021-10-08 14:08:22 +01:00
return nil
}
if !entry.IsDir() {
2023-09-29 16:42:39 +09:00
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
2021-10-08 14:08:22 +01:00
return nil
}
allEntries, err := tree.ListEntries()
if err != nil {
ctx.ServerError("ListEntries", err)
return nil
}
allEntries.CustomSort(base.NaturalSortCompare)
2021-10-08 14:08:22 +01:00
commitInfoCtx := gocontext.Context(ctx)
if timeout > 0 {
var cancel gocontext.CancelFunc
commitInfoCtx, cancel = gocontext.WithTimeout(ctx, timeout)
defer cancel()
}
files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath)
2021-10-08 14:08:22 +01:00
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return nil
}
{
if timeout != 0 && !setting.IsProd && !setting.IsInTesting {
log.Debug("first call to get directory file commit info")
clearFilesCommitInfo := func() {
log.Warn("clear directory file commit info to force async loading on frontend")
for i := range files {
files[i].Commit = nil
}
}
_ = clearFilesCommitInfo
// clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests
}
}
ctx.Data["Files"] = files
prepareDirectoryFileIcons(ctx, files)
for _, f := range files {
if f.Commit == nil {
ctx.Data["HasFilesWithoutLatestCommit"] = true
break
}
}
2021-10-08 14:08:22 +01:00
2024-01-15 17:42:15 +01:00
if !loadLatestCommitData(ctx, latestCommit) {
return nil
}
2021-10-08 14:08:22 +01:00
return allEntries
}
// 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 templates.TplName) {
page := ctx.FormInt("page")
2015-11-16 23:28:46 -05:00
if page <= 0 {
page = 1
}
2026-03-08 15:35:50 +01:00
pager := context.NewPagination(int64(total), setting.ItemsPerPage, page, 5)
2015-11-16 23:28:46 -05:00
ctx.Data["Page"] = pager
items, err := getter(db.ListOptions{
Page: pager.Paginater.Current(),
2022-06-12 23:51:54 +08:00
PageSize: setting.ItemsPerPage,
})
2015-11-16 23:28:46 -05:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx.ServerError("getter", err)
2015-11-16 23:28:46 -05:00
return
}
2015-12-21 04:24:11 -08:00
ctx.Data["Cards"] = items
2015-11-16 23:28:46 -05:00
ctx.HTML(http.StatusOK, tpl)
2015-11-16 23:28:46 -05:00
}
// Watchers render repository's watch users
2016-03-11 11:56:52 -05:00
func Watchers(ctx *context.Context) {
2015-11-16 23:28:46 -05:00
ctx.Data["Title"] = ctx.Tr("repo.watchers")
2015-12-21 04:24:11 -08:00
ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
return repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, opts)
}, tplWatchers)
2015-11-16 23:28:46 -05:00
}
// Stars render repository's starred users
2016-03-11 11:56:52 -05:00
func Stars(ctx *context.Context) {
2015-11-16 23:28:46 -05:00
ctx.Data["Title"] = ctx.Tr("repo.stargazers")
2015-12-21 04:24:11 -08:00
ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
}, tplWatchers)
2015-11-16 23:28:46 -05:00
}
2015-11-16 23:33:40 -05:00
// Forks render repository's forked users
2016-03-11 11:56:52 -05:00
func Forks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.forks")
2015-11-16 23:33:40 -05:00
2021-11-18 14:45:56 +00:00
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
pageSize := setting.ItemsPerPage
2021-11-18 14:45:56 +00:00
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
Page: page,
PageSize: pageSize,
2021-11-18 14:45:56 +00:00
})
2015-11-16 23:33:40 -05:00
if err != nil {
ctx.ServerError("FindForks", err)
2015-11-16 23:33:40 -05:00
return
}
if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
2015-11-16 23:33:40 -05:00
}
2021-11-18 14:45:56 +00:00
2026-03-08 15:35:50 +01:00
pager := context.NewPagination(total, pageSize, page, 5)
ctx.Data["ShowRepoOwnerAvatar"] = true
ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["Page"] = pager
ctx.Data["Repos"] = forks
2015-11-16 23:33:40 -05:00
ctx.HTML(http.StatusOK, tplForks)
2015-11-16 23:33:40 -05:00
}