code.gitea.io/gitea@v1.21.7/routers/web/repo/view.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // Copyright 2014 The Gogs Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"bytes"
     9  	gocontext "context"
    10  	"encoding/base64"
    11  	"fmt"
    12  	"html/template"
    13  	"image"
    14  	"io"
    15  	"net/http"
    16  	"net/url"
    17  	"path"
    18  	"slices"
    19  	"strings"
    20  	"time"
    21  
    22  	_ "image/gif"  // for processing gif images
    23  	_ "image/jpeg" // for processing jpeg images
    24  	_ "image/png"  // for processing png images
    25  
    26  	activities_model "code.gitea.io/gitea/models/activities"
    27  	admin_model "code.gitea.io/gitea/models/admin"
    28  	asymkey_model "code.gitea.io/gitea/models/asymkey"
    29  	"code.gitea.io/gitea/models/db"
    30  	git_model "code.gitea.io/gitea/models/git"
    31  	issue_model "code.gitea.io/gitea/models/issues"
    32  	repo_model "code.gitea.io/gitea/models/repo"
    33  	unit_model "code.gitea.io/gitea/models/unit"
    34  	user_model "code.gitea.io/gitea/models/user"
    35  	"code.gitea.io/gitea/modules/actions"
    36  	"code.gitea.io/gitea/modules/base"
    37  	"code.gitea.io/gitea/modules/charset"
    38  	"code.gitea.io/gitea/modules/container"
    39  	"code.gitea.io/gitea/modules/context"
    40  	"code.gitea.io/gitea/modules/git"
    41  	"code.gitea.io/gitea/modules/highlight"
    42  	"code.gitea.io/gitea/modules/lfs"
    43  	"code.gitea.io/gitea/modules/log"
    44  	"code.gitea.io/gitea/modules/markup"
    45  	repo_module "code.gitea.io/gitea/modules/repository"
    46  	"code.gitea.io/gitea/modules/setting"
    47  	"code.gitea.io/gitea/modules/structs"
    48  	"code.gitea.io/gitea/modules/typesniffer"
    49  	"code.gitea.io/gitea/modules/util"
    50  	"code.gitea.io/gitea/routers/web/feed"
    51  	issue_service "code.gitea.io/gitea/services/issue"
    52  
    53  	"github.com/nektos/act/pkg/model"
    54  
    55  	_ "golang.org/x/image/bmp"  // for processing bmp images
    56  	_ "golang.org/x/image/webp" // for processing webp images
    57  )
    58  
    59  const (
    60  	tplRepoEMPTY    base.TplName = "repo/empty"
    61  	tplRepoHome     base.TplName = "repo/home"
    62  	tplRepoViewList base.TplName = "repo/view_list"
    63  	tplWatchers     base.TplName = "repo/watchers"
    64  	tplForks        base.TplName = "repo/forks"
    65  	tplMigrating    base.TplName = "repo/migrate/migrating"
    66  )
    67  
    68  // locate a README for a tree in one of the supported paths.
    69  //
    70  // entries is passed to reduce calls to ListEntries(), so
    71  // this has precondition:
    72  //
    73  //	entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
    74  //
    75  // FIXME: There has to be a more efficient way of doing this
    76  func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
    77  	// Create a list of extensions in priority order
    78  	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
    79  	// 2. Txt files - e.g. README.txt
    80  	// 3. No extension - e.g. README
    81  	exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
    82  	extCount := len(exts)
    83  	readmeFiles := make([]*git.TreeEntry, extCount+1)
    84  
    85  	docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
    86  	for _, entry := range entries {
    87  		if tryWellKnownDirs && entry.IsDir() {
    88  			// as a special case for the top-level repo introduction README,
    89  			// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
    90  			// (note that docsEntries is ignored unless we are at the root)
    91  			lowerName := strings.ToLower(entry.Name())
    92  			switch lowerName {
    93  			case "docs":
    94  				if entry.Name() == "docs" || docsEntries[0] == nil {
    95  					docsEntries[0] = entry
    96  				}
    97  			case ".gitea":
    98  				if entry.Name() == ".gitea" || docsEntries[1] == nil {
    99  					docsEntries[1] = entry
   100  				}
   101  			case ".github":
   102  				if entry.Name() == ".github" || docsEntries[2] == nil {
   103  					docsEntries[2] = entry
   104  				}
   105  			}
   106  			continue
   107  		}
   108  		if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
   109  			log.Debug("Potential readme file: %s", entry.Name())
   110  			if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
   111  				if entry.IsLink() {
   112  					target, err := entry.FollowLinks()
   113  					if err != nil && !git.IsErrBadLink(err) {
   114  						return "", nil, err
   115  					} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
   116  						readmeFiles[i] = entry
   117  					}
   118  				} else {
   119  					readmeFiles[i] = entry
   120  				}
   121  			}
   122  		}
   123  	}
   124  	var readmeFile *git.TreeEntry
   125  	for _, f := range readmeFiles {
   126  		if f != nil {
   127  			readmeFile = f
   128  			break
   129  		}
   130  	}
   131  
   132  	if ctx.Repo.TreePath == "" && readmeFile == nil {
   133  		for _, subTreeEntry := range docsEntries {
   134  			if subTreeEntry == nil {
   135  				continue
   136  			}
   137  			subTree := subTreeEntry.Tree()
   138  			if subTree == nil {
   139  				// this should be impossible; if subTreeEntry exists so should this.
   140  				continue
   141  			}
   142  			var err error
   143  			childEntries, err := subTree.ListEntries()
   144  			if err != nil {
   145  				return "", nil, err
   146  			}
   147  
   148  			subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
   149  			if err != nil && !git.IsErrNotExist(err) {
   150  				return "", nil, err
   151  			}
   152  			if readmeFile != nil {
   153  				return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil
   154  			}
   155  		}
   156  	}
   157  
   158  	return "", readmeFile, nil
   159  }
   160  
   161  func renderDirectory(ctx *context.Context) {
   162  	entries := renderDirectoryFiles(ctx, 1*time.Second)
   163  	if ctx.Written() {
   164  		return
   165  	}
   166  
   167  	if ctx.Repo.TreePath != "" {
   168  		ctx.Data["HideRepoInfo"] = true
   169  		ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
   170  	}
   171  
   172  	subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
   173  	if err != nil {
   174  		ctx.ServerError("findReadmeFileInEntries", err)
   175  		return
   176  	}
   177  
   178  	renderReadmeFile(ctx, subfolder, readmeFile)
   179  }
   180  
   181  // localizedExtensions prepends the provided language code with and without a
   182  // regional identifier to the provided extension.
   183  // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
   184  // Note: ext should be prefixed with a `.`
   185  func localizedExtensions(ext, languageCode string) (localizedExts []string) {
   186  	if len(languageCode) < 1 {
   187  		return []string{ext}
   188  	}
   189  
   190  	lowerLangCode := "." + strings.ToLower(languageCode)
   191  
   192  	if strings.Contains(lowerLangCode, "-") {
   193  		underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
   194  		indexOfDash := strings.Index(lowerLangCode, "-")
   195  		// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md]
   196  		return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext}
   197  	}
   198  
   199  	// e.g. [.en.md, .md]
   200  	return []string{lowerLangCode + ext, ext}
   201  }
   202  
   203  type fileInfo struct {
   204  	isTextFile bool
   205  	isLFSFile  bool
   206  	fileSize   int64
   207  	lfsMeta    *lfs.Pointer
   208  	st         typesniffer.SniffedType
   209  }
   210  
   211  func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
   212  	dataRc, err := blob.DataAsync()
   213  	if err != nil {
   214  		return nil, nil, nil, err
   215  	}
   216  
   217  	buf := make([]byte, 1024)
   218  	n, _ := util.ReadAtMost(dataRc, buf)
   219  	buf = buf[:n]
   220  
   221  	st := typesniffer.DetectContentType(buf)
   222  	isTextFile := st.IsText()
   223  
   224  	// FIXME: what happens when README file is an image?
   225  	if !isTextFile || !setting.LFS.StartServer {
   226  		return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
   227  	}
   228  
   229  	pointer, _ := lfs.ReadPointerFromBuffer(buf)
   230  	if !pointer.IsValid() { // fallback to plain file
   231  		return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
   232  	}
   233  
   234  	meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
   235  	if err != nil && err != git_model.ErrLFSObjectNotExist { // fallback to plain file
   236  		return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
   237  	}
   238  
   239  	dataRc.Close()
   240  	if err != nil {
   241  		return nil, nil, nil, err
   242  	}
   243  
   244  	dataRc, err = lfs.ReadMetaObject(pointer)
   245  	if err != nil {
   246  		return nil, nil, nil, err
   247  	}
   248  
   249  	buf = make([]byte, 1024)
   250  	n, err = util.ReadAtMost(dataRc, buf)
   251  	if err != nil {
   252  		dataRc.Close()
   253  		return nil, nil, nil, err
   254  	}
   255  	buf = buf[:n]
   256  
   257  	st = typesniffer.DetectContentType(buf)
   258  
   259  	return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
   260  }
   261  
   262  func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
   263  	target := readmeFile
   264  	if readmeFile != nil && readmeFile.IsLink() {
   265  		target, _ = readmeFile.FollowLinks()
   266  	}
   267  	if target == nil {
   268  		// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
   269  		// simply skip rendering the README
   270  		return
   271  	}
   272  
   273  	ctx.Data["RawFileLink"] = ""
   274  	ctx.Data["ReadmeInList"] = true
   275  	ctx.Data["ReadmeExist"] = true
   276  	ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
   277  
   278  	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob())
   279  	if err != nil {
   280  		ctx.ServerError("getFileReader", err)
   281  		return
   282  	}
   283  	defer dataRc.Close()
   284  
   285  	ctx.Data["FileIsText"] = fInfo.isTextFile
   286  	ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
   287  	ctx.Data["IsLFSFile"] = fInfo.isLFSFile
   288  
   289  	if fInfo.isLFSFile {
   290  		filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
   291  		ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
   292  	}
   293  
   294  	if !fInfo.isTextFile {
   295  		return
   296  	}
   297  
   298  	if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
   299  		// Pretend that this is a normal text file to display 'This file is too large to be shown'
   300  		ctx.Data["IsFileTooLarge"] = true
   301  		ctx.Data["IsTextFile"] = true
   302  		ctx.Data["FileSize"] = fInfo.fileSize
   303  		return
   304  	}
   305  
   306  	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
   307  
   308  	if markupType := markup.Type(readmeFile.Name()); markupType != "" {
   309  		ctx.Data["IsMarkup"] = true
   310  		ctx.Data["MarkupType"] = markupType
   311  
   312  		ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
   313  			Ctx:          ctx,
   314  			RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
   315  			Links: markup.Links{
   316  				Base:       ctx.Repo.RepoLink,
   317  				BranchPath: ctx.Repo.BranchNameSubURL(),
   318  				TreePath:   path.Join(ctx.Repo.TreePath, subfolder),
   319  			},
   320  			Metas:   ctx.Repo.Repository.ComposeDocumentMetas(),
   321  			GitRepo: ctx.Repo.GitRepo,
   322  		}, rd)
   323  		if err != nil {
   324  			log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
   325  			delete(ctx.Data, "IsMarkup")
   326  		}
   327  	}
   328  
   329  	if ctx.Data["IsMarkup"] != true {
   330  		ctx.Data["IsPlainText"] = true
   331  		content, err := io.ReadAll(rd)
   332  		if err != nil {
   333  			log.Error("Read readme content failed: %v", err)
   334  		}
   335  		contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content))
   336  		ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
   337  	}
   338  }
   339  
   340  func renderFile(ctx *context.Context, entry *git.TreeEntry) {
   341  	ctx.Data["IsViewFile"] = true
   342  	ctx.Data["HideRepoInfo"] = true
   343  	blob := entry.Blob()
   344  	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
   345  	if err != nil {
   346  		ctx.ServerError("getFileReader", err)
   347  		return
   348  	}
   349  	defer dataRc.Close()
   350  
   351  	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
   352  	ctx.Data["FileIsSymlink"] = entry.IsLink()
   353  	ctx.Data["FileName"] = blob.Name()
   354  	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
   355  
   356  	if ctx.Repo.TreePath == ".editorconfig" {
   357  		_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
   358  		if editorconfigWarning != nil {
   359  			ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error())
   360  		}
   361  		if editorconfigErr != nil {
   362  			ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
   363  		}
   364  	} else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) {
   365  		_, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit)
   366  		if issueConfigErr != nil {
   367  			ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
   368  		}
   369  	} else if actions.IsWorkflow(ctx.Repo.TreePath) {
   370  		content, err := actions.GetContentFromEntry(entry)
   371  		if err != nil {
   372  			log.Error("actions.GetContentFromEntry: %v", err)
   373  		}
   374  		_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
   375  		if workFlowErr != nil {
   376  			ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
   377  		}
   378  	} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
   379  		if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
   380  			_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
   381  			if len(warnings) > 0 {
   382  				ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
   383  			}
   384  		}
   385  	}
   386  
   387  	isDisplayingSource := ctx.FormString("display") == "source"
   388  	isDisplayingRendered := !isDisplayingSource
   389  
   390  	if fInfo.isLFSFile {
   391  		ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
   392  	}
   393  
   394  	isRepresentableAsText := fInfo.st.IsRepresentableAsText()
   395  	if !isRepresentableAsText {
   396  		// If we can't show plain text, always try to render.
   397  		isDisplayingSource = false
   398  		isDisplayingRendered = true
   399  	}
   400  	ctx.Data["IsLFSFile"] = fInfo.isLFSFile
   401  	ctx.Data["FileSize"] = fInfo.fileSize
   402  	ctx.Data["IsTextFile"] = fInfo.isTextFile
   403  	ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
   404  	ctx.Data["IsDisplayingSource"] = isDisplayingSource
   405  	ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
   406  	ctx.Data["IsExecutable"] = entry.IsExecutable()
   407  
   408  	isTextSource := fInfo.isTextFile || isDisplayingSource
   409  	ctx.Data["IsTextSource"] = isTextSource
   410  	if isTextSource {
   411  		ctx.Data["CanCopyContent"] = true
   412  	}
   413  
   414  	// Check LFS Lock
   415  	lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
   416  	ctx.Data["LFSLock"] = lfsLock
   417  	if err != nil {
   418  		ctx.ServerError("GetTreePathLock", err)
   419  		return
   420  	}
   421  	if lfsLock != nil {
   422  		u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
   423  		if err != nil {
   424  			ctx.ServerError("GetTreePathLock", err)
   425  			return
   426  		}
   427  		ctx.Data["LFSLockOwner"] = u.Name
   428  		ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
   429  		ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
   430  	}
   431  
   432  	// Assume file is not editable first.
   433  	if fInfo.isLFSFile {
   434  		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
   435  	} else if !isRepresentableAsText {
   436  		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
   437  	}
   438  
   439  	switch {
   440  	case isRepresentableAsText:
   441  		if fInfo.st.IsSvgImage() {
   442  			ctx.Data["IsImageFile"] = true
   443  			ctx.Data["CanCopyContent"] = true
   444  			ctx.Data["HasSourceRenderedToggle"] = true
   445  		}
   446  
   447  		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
   448  			ctx.Data["IsFileTooLarge"] = true
   449  			break
   450  		}
   451  
   452  		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
   453  
   454  		shouldRenderSource := ctx.FormString("display") == "source"
   455  		readmeExist := util.IsReadmeFileName(blob.Name())
   456  		ctx.Data["ReadmeExist"] = readmeExist
   457  
   458  		markupType := markup.Type(blob.Name())
   459  		// If the markup is detected by custom markup renderer it should not be reset later on
   460  		// to not pass it down to the render context.
   461  		detected := false
   462  		if markupType == "" {
   463  			detected = true
   464  			markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
   465  		}
   466  		if markupType != "" {
   467  			ctx.Data["HasSourceRenderedToggle"] = true
   468  		}
   469  
   470  		if markupType != "" && !shouldRenderSource {
   471  			ctx.Data["IsMarkup"] = true
   472  			ctx.Data["MarkupType"] = markupType
   473  			if !detected {
   474  				markupType = ""
   475  			}
   476  			metas := ctx.Repo.Repository.ComposeDocumentMetas()
   477  			metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
   478  			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
   479  				Ctx:          ctx,
   480  				Type:         markupType,
   481  				RelativePath: ctx.Repo.TreePath,
   482  				Links: markup.Links{
   483  					Base:       ctx.Repo.RepoLink,
   484  					BranchPath: ctx.Repo.BranchNameSubURL(),
   485  					TreePath:   path.Dir(ctx.Repo.TreePath),
   486  				},
   487  				Metas:   metas,
   488  				GitRepo: ctx.Repo.GitRepo,
   489  			}, rd)
   490  			if err != nil {
   491  				ctx.ServerError("Render", err)
   492  				return
   493  			}
   494  			// to prevent iframe load third-party url
   495  			ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
   496  		} else {
   497  			buf, _ := io.ReadAll(rd)
   498  
   499  			// empty: 0 lines; "a": one line; "a\n": two lines; "a\nb": two lines;
   500  			// the NumLines is only used for the display on the UI: "xxx lines"
   501  			if len(buf) == 0 {
   502  				ctx.Data["NumLines"] = 0
   503  			} else {
   504  				ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
   505  			}
   506  			ctx.Data["NumLinesSet"] = true
   507  
   508  			language := ""
   509  
   510  			indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
   511  			if err == nil {
   512  				defer deleteTemporaryFile()
   513  
   514  				filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
   515  					CachedOnly: true,
   516  					Attributes: []string{"linguist-language", "gitlab-language"},
   517  					Filenames:  []string{ctx.Repo.TreePath},
   518  					IndexFile:  indexFilename,
   519  					WorkTree:   worktree,
   520  				})
   521  				if err != nil {
   522  					log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
   523  				}
   524  
   525  				language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
   526  				if language == "" || language == "unspecified" {
   527  					language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
   528  				}
   529  				if language == "unspecified" {
   530  					language = ""
   531  				}
   532  			}
   533  			fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
   534  			ctx.Data["LexerName"] = lexerName
   535  			if err != nil {
   536  				log.Error("highlight.File failed, fallback to plain text: %v", err)
   537  				fileContent = highlight.PlainText(buf)
   538  			}
   539  			status := &charset.EscapeStatus{}
   540  			statuses := make([]*charset.EscapeStatus, len(fileContent))
   541  			for i, line := range fileContent {
   542  				statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
   543  				status = status.Or(statuses[i])
   544  			}
   545  			ctx.Data["EscapeStatus"] = status
   546  			ctx.Data["FileContent"] = fileContent
   547  			ctx.Data["LineEscapeStatus"] = statuses
   548  		}
   549  		if !fInfo.isLFSFile {
   550  			if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
   551  				if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
   552  					ctx.Data["CanEditFile"] = false
   553  					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
   554  				} else {
   555  					ctx.Data["CanEditFile"] = true
   556  					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
   557  				}
   558  			} else if !ctx.Repo.IsViewBranch {
   559  				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
   560  			} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
   561  				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
   562  			}
   563  		}
   564  
   565  	case fInfo.st.IsPDF():
   566  		ctx.Data["IsPDFFile"] = true
   567  	case fInfo.st.IsVideo():
   568  		ctx.Data["IsVideoFile"] = true
   569  	case fInfo.st.IsAudio():
   570  		ctx.Data["IsAudioFile"] = true
   571  	case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
   572  		ctx.Data["IsImageFile"] = true
   573  		ctx.Data["CanCopyContent"] = true
   574  	default:
   575  		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
   576  			ctx.Data["IsFileTooLarge"] = true
   577  			break
   578  		}
   579  
   580  		if markupType := markup.Type(blob.Name()); markupType != "" {
   581  			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
   582  			ctx.Data["IsMarkup"] = true
   583  			ctx.Data["MarkupType"] = markupType
   584  			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
   585  				Ctx:          ctx,
   586  				RelativePath: ctx.Repo.TreePath,
   587  				Links: markup.Links{
   588  					Base:       ctx.Repo.RepoLink,
   589  					BranchPath: ctx.Repo.BranchNameSubURL(),
   590  					TreePath:   path.Dir(ctx.Repo.TreePath),
   591  				},
   592  				Metas:   ctx.Repo.Repository.ComposeDocumentMetas(),
   593  				GitRepo: ctx.Repo.GitRepo,
   594  			}, rd)
   595  			if err != nil {
   596  				ctx.ServerError("Render", err)
   597  				return
   598  			}
   599  		}
   600  	}
   601  
   602  	if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
   603  		img, _, err := image.DecodeConfig(bytes.NewReader(buf))
   604  		if err == nil {
   605  			// There are Image formats go can't decode
   606  			// Instead of throwing an error in that case, we show the size only when we can decode
   607  			ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
   608  		}
   609  	}
   610  
   611  	if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
   612  		if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
   613  			ctx.Data["CanDeleteFile"] = false
   614  			ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
   615  		} else {
   616  			ctx.Data["CanDeleteFile"] = true
   617  			ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
   618  		}
   619  	} else if !ctx.Repo.IsViewBranch {
   620  		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
   621  	} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
   622  		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
   623  	}
   624  }
   625  
   626  func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
   627  	markupRd, markupWr := io.Pipe()
   628  	defer markupWr.Close()
   629  	done := make(chan struct{})
   630  	go func() {
   631  		sb := &strings.Builder{}
   632  		// We allow NBSP here this is rendered
   633  		escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
   634  		output = template.HTML(sb.String())
   635  		close(done)
   636  	}()
   637  	err = markup.Render(renderCtx, input, markupWr)
   638  	_ = markupWr.CloseWithError(err)
   639  	<-done
   640  	return escaped, output, err
   641  }
   642  
   643  func checkHomeCodeViewable(ctx *context.Context) {
   644  	if len(ctx.Repo.Units) > 0 {
   645  		if ctx.Repo.Repository.IsBeingCreated() {
   646  			task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID)
   647  			if err != nil {
   648  				if admin_model.IsErrTaskDoesNotExist(err) {
   649  					ctx.Data["Repo"] = ctx.Repo
   650  					ctx.Data["CloneAddr"] = ""
   651  					ctx.Data["Failed"] = true
   652  					ctx.HTML(http.StatusOK, tplMigrating)
   653  					return
   654  				}
   655  				ctx.ServerError("models.GetMigratingTask", err)
   656  				return
   657  			}
   658  			cfg, err := task.MigrateConfig()
   659  			if err != nil {
   660  				ctx.ServerError("task.MigrateConfig", err)
   661  				return
   662  			}
   663  
   664  			ctx.Data["Repo"] = ctx.Repo
   665  			ctx.Data["MigrateTask"] = task
   666  			ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr)
   667  			ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed
   668  			ctx.HTML(http.StatusOK, tplMigrating)
   669  			return
   670  		}
   671  
   672  		if ctx.IsSigned {
   673  			// Set repo notification-status read if unread
   674  			if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
   675  				ctx.ServerError("ReadBy", err)
   676  				return
   677  			}
   678  		}
   679  
   680  		var firstUnit *unit_model.Unit
   681  		for _, repoUnit := range ctx.Repo.Units {
   682  			if repoUnit.Type == unit_model.TypeCode {
   683  				return
   684  			}
   685  
   686  			unit, ok := unit_model.Units[repoUnit.Type]
   687  			if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
   688  				firstUnit = &unit
   689  			}
   690  		}
   691  
   692  		if firstUnit != nil {
   693  			ctx.Redirect(fmt.Sprintf("%s%s", ctx.Repo.Repository.Link(), firstUnit.URI))
   694  			return
   695  		}
   696  	}
   697  
   698  	ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
   699  }
   700  
   701  func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
   702  	if entry.Name() != "" {
   703  		return
   704  	}
   705  	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
   706  	if err != nil {
   707  		HandleGitError(ctx, "Repo.Commit.SubTree", err)
   708  		return
   709  	}
   710  	allEntries, err := tree.ListEntries()
   711  	if err != nil {
   712  		ctx.ServerError("ListEntries", err)
   713  		return
   714  	}
   715  	for _, entry := range allEntries {
   716  		if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
   717  			// Read Citation file contents
   718  			if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
   719  				log.Error("checkCitationFile: GetBlobContent: %v", err)
   720  			} else {
   721  				ctx.Data["CitiationExist"] = true
   722  				ctx.PageData["citationFileContent"] = content
   723  				break
   724  			}
   725  		}
   726  	}
   727  }
   728  
   729  // Home render repository home page
   730  func Home(ctx *context.Context) {
   731  	if setting.Other.EnableFeed {
   732  		isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
   733  		if isFeed {
   734  			switch {
   735  			case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType):
   736  				feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
   737  			case ctx.Repo.TreePath == "":
   738  				feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
   739  			case ctx.Repo.TreePath != "":
   740  				feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
   741  			}
   742  			return
   743  		}
   744  	}
   745  
   746  	checkHomeCodeViewable(ctx)
   747  	if ctx.Written() {
   748  		return
   749  	}
   750  
   751  	renderCode(ctx)
   752  }
   753  
   754  // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
   755  func LastCommit(ctx *context.Context) {
   756  	checkHomeCodeViewable(ctx)
   757  	if ctx.Written() {
   758  		return
   759  	}
   760  
   761  	renderDirectoryFiles(ctx, 0)
   762  	if ctx.Written() {
   763  		return
   764  	}
   765  
   766  	var treeNames []string
   767  	paths := make([]string, 0, 5)
   768  	if len(ctx.Repo.TreePath) > 0 {
   769  		treeNames = strings.Split(ctx.Repo.TreePath, "/")
   770  		for i := range treeNames {
   771  			paths = append(paths, strings.Join(treeNames[:i+1], "/"))
   772  		}
   773  
   774  		ctx.Data["HasParentPath"] = true
   775  		if len(paths)-2 >= 0 {
   776  			ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
   777  		}
   778  	}
   779  	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
   780  	ctx.Data["BranchLink"] = branchLink
   781  
   782  	ctx.HTML(http.StatusOK, tplRepoViewList)
   783  }
   784  
   785  func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
   786  	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
   787  	if err != nil {
   788  		HandleGitError(ctx, "Repo.Commit.SubTree", err)
   789  		return nil
   790  	}
   791  
   792  	ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
   793  
   794  	// Get current entry user currently looking at.
   795  	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
   796  	if err != nil {
   797  		HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
   798  		return nil
   799  	}
   800  
   801  	if !entry.IsDir() {
   802  		HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
   803  		return nil
   804  	}
   805  
   806  	allEntries, err := tree.ListEntries()
   807  	if err != nil {
   808  		ctx.ServerError("ListEntries", err)
   809  		return nil
   810  	}
   811  	allEntries.CustomSort(base.NaturalSortLess)
   812  
   813  	commitInfoCtx := gocontext.Context(ctx)
   814  	if timeout > 0 {
   815  		var cancel gocontext.CancelFunc
   816  		commitInfoCtx, cancel = gocontext.WithTimeout(ctx, timeout)
   817  		defer cancel()
   818  	}
   819  
   820  	selected := make(container.Set[string])
   821  	selected.AddMultiple(ctx.FormStrings("f[]")...)
   822  
   823  	entries := allEntries
   824  	if len(selected) > 0 {
   825  		entries = make(git.Entries, 0, len(selected))
   826  		for _, entry := range allEntries {
   827  			if selected.Contains(entry.Name()) {
   828  				entries = append(entries, entry)
   829  			}
   830  		}
   831  	}
   832  
   833  	var latestCommit *git.Commit
   834  	ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
   835  	if err != nil {
   836  		ctx.ServerError("GetCommitsInfo", err)
   837  		return nil
   838  	}
   839  
   840  	// Show latest commit info of repository in table header,
   841  	// or of directory if not in root directory.
   842  	ctx.Data["LatestCommit"] = latestCommit
   843  	if latestCommit != nil {
   844  
   845  		verification := asymkey_model.ParseCommitWithSignature(ctx, latestCommit)
   846  
   847  		if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
   848  			return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
   849  		}, nil); err != nil {
   850  			ctx.ServerError("CalculateTrustStatus", err)
   851  			return nil
   852  		}
   853  		ctx.Data["LatestCommitVerification"] = verification
   854  		ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
   855  
   856  		statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true})
   857  		if err != nil {
   858  			log.Error("GetLatestCommitStatus: %v", err)
   859  		}
   860  
   861  		ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
   862  		ctx.Data["LatestCommitStatuses"] = statuses
   863  	}
   864  
   865  	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
   866  	treeLink := branchLink
   867  
   868  	if len(ctx.Repo.TreePath) > 0 {
   869  		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
   870  	}
   871  
   872  	ctx.Data["TreeLink"] = treeLink
   873  	ctx.Data["SSHDomain"] = setting.SSH.Domain
   874  
   875  	return allEntries
   876  }
   877  
   878  func renderLanguageStats(ctx *context.Context) {
   879  	langs, err := repo_model.GetTopLanguageStats(ctx.Repo.Repository, 5)
   880  	if err != nil {
   881  		ctx.ServerError("Repo.GetTopLanguageStats", err)
   882  		return
   883  	}
   884  
   885  	ctx.Data["LanguageStats"] = langs
   886  }
   887  
   888  func renderRepoTopics(ctx *context.Context) {
   889  	topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
   890  		RepoID: ctx.Repo.Repository.ID,
   891  	})
   892  	if err != nil {
   893  		ctx.ServerError("models.FindTopics", err)
   894  		return
   895  	}
   896  	ctx.Data["Topics"] = topics
   897  }
   898  
   899  func renderCode(ctx *context.Context) {
   900  	ctx.Data["PageIsViewCode"] = true
   901  	ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
   902  
   903  	if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
   904  		showEmpty := true
   905  		var err error
   906  		if ctx.Repo.GitRepo != nil {
   907  			showEmpty, err = ctx.Repo.GitRepo.IsEmpty()
   908  			if err != nil {
   909  				log.Error("GitRepo.IsEmpty: %v", err)
   910  				ctx.Repo.Repository.Status = repo_model.RepositoryBroken
   911  				showEmpty = true
   912  				ctx.Flash.Error(ctx.Tr("error.occurred"), true)
   913  			}
   914  		}
   915  		if showEmpty {
   916  			ctx.HTML(http.StatusOK, tplRepoEMPTY)
   917  			return
   918  		}
   919  
   920  		// the repo is not really empty, so we should update the modal in database
   921  		// such problem may be caused by:
   922  		// 1) an error occurs during pushing/receiving.  2) the user replaces an empty git repo manually
   923  		// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos.
   924  		// it's possible for a repository to be non-empty by that flag but still 500
   925  		// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
   926  		ctx.Repo.Repository.IsEmpty = false
   927  		if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil {
   928  			ctx.ServerError("UpdateRepositoryCols", err)
   929  			return
   930  		}
   931  		if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
   932  			ctx.ServerError("UpdateRepoSize", err)
   933  			return
   934  		}
   935  
   936  		// the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values
   937  		link := ctx.Link
   938  		if ctx.Req.URL.RawQuery != "" {
   939  			link += "?" + ctx.Req.URL.RawQuery
   940  		}
   941  		ctx.Redirect(link)
   942  		return
   943  	}
   944  
   945  	title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
   946  	if len(ctx.Repo.Repository.Description) > 0 {
   947  		title += ": " + ctx.Repo.Repository.Description
   948  	}
   949  	ctx.Data["Title"] = title
   950  
   951  	// Get Topics of this repo
   952  	renderRepoTopics(ctx)
   953  	if ctx.Written() {
   954  		return
   955  	}
   956  
   957  	// Get current entry user currently looking at.
   958  	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
   959  	if err != nil {
   960  		HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
   961  		return
   962  	}
   963  
   964  	checkCitationFile(ctx, entry)
   965  	if ctx.Written() {
   966  		return
   967  	}
   968  
   969  	renderLanguageStats(ctx)
   970  	if ctx.Written() {
   971  		return
   972  	}
   973  
   974  	if entry.IsDir() {
   975  		renderDirectory(ctx)
   976  	} else {
   977  		renderFile(ctx, entry)
   978  	}
   979  	if ctx.Written() {
   980  		return
   981  	}
   982  
   983  	if ctx.Doer != nil {
   984  		if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
   985  			ctx.ServerError("GetBaseRepo", err)
   986  			return
   987  		}
   988  
   989  		showRecentlyPushedNewBranches := true
   990  		if ctx.Repo.Repository.IsMirror ||
   991  			!ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
   992  			showRecentlyPushedNewBranches = false
   993  		}
   994  		if showRecentlyPushedNewBranches {
   995  			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch)
   996  			if err != nil {
   997  				ctx.ServerError("GetRecentlyPushedBranches", err)
   998  				return
   999  			}
  1000  		}
  1001  	}
  1002  
  1003  	var treeNames []string
  1004  	paths := make([]string, 0, 5)
  1005  	if len(ctx.Repo.TreePath) > 0 {
  1006  		treeNames = strings.Split(ctx.Repo.TreePath, "/")
  1007  		for i := range treeNames {
  1008  			paths = append(paths, strings.Join(treeNames[:i+1], "/"))
  1009  		}
  1010  
  1011  		ctx.Data["HasParentPath"] = true
  1012  		if len(paths)-2 >= 0 {
  1013  			ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
  1014  		}
  1015  	}
  1016  
  1017  	ctx.Data["Paths"] = paths
  1018  
  1019  	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  1020  	treeLink := branchLink
  1021  	if len(ctx.Repo.TreePath) > 0 {
  1022  		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  1023  	}
  1024  	ctx.Data["TreeLink"] = treeLink
  1025  	ctx.Data["TreeNames"] = treeNames
  1026  	ctx.Data["BranchLink"] = branchLink
  1027  	ctx.HTML(http.StatusOK, tplRepoHome)
  1028  }
  1029  
  1030  // RenderUserCards render a page show users according the input template
  1031  func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
  1032  	page := ctx.FormInt("page")
  1033  	if page <= 0 {
  1034  		page = 1
  1035  	}
  1036  	pager := context.NewPagination(total, setting.ItemsPerPage, page, 5)
  1037  	ctx.Data["Page"] = pager
  1038  
  1039  	items, err := getter(db.ListOptions{
  1040  		Page:     pager.Paginater.Current(),
  1041  		PageSize: setting.ItemsPerPage,
  1042  	})
  1043  	if err != nil {
  1044  		ctx.ServerError("getter", err)
  1045  		return
  1046  	}
  1047  	ctx.Data["Cards"] = items
  1048  
  1049  	ctx.HTML(http.StatusOK, tpl)
  1050  }
  1051  
  1052  // Watchers render repository's watch users
  1053  func Watchers(ctx *context.Context) {
  1054  	ctx.Data["Title"] = ctx.Tr("repo.watchers")
  1055  	ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
  1056  	ctx.Data["PageIsWatchers"] = true
  1057  
  1058  	RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
  1059  		return repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, opts)
  1060  	}, tplWatchers)
  1061  }
  1062  
  1063  // Stars render repository's starred users
  1064  func Stars(ctx *context.Context) {
  1065  	ctx.Data["Title"] = ctx.Tr("repo.stargazers")
  1066  	ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
  1067  	ctx.Data["PageIsStargazers"] = true
  1068  	RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
  1069  		return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
  1070  	}, tplWatchers)
  1071  }
  1072  
  1073  // Forks render repository's forked users
  1074  func Forks(ctx *context.Context) {
  1075  	ctx.Data["Title"] = ctx.Tr("repo.forks")
  1076  
  1077  	page := ctx.FormInt("page")
  1078  	if page <= 0 {
  1079  		page = 1
  1080  	}
  1081  
  1082  	pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
  1083  	ctx.Data["Page"] = pager
  1084  
  1085  	forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
  1086  		Page:     pager.Paginater.Current(),
  1087  		PageSize: setting.ItemsPerPage,
  1088  	})
  1089  	if err != nil {
  1090  		ctx.ServerError("GetForks", err)
  1091  		return
  1092  	}
  1093  
  1094  	for _, fork := range forks {
  1095  		if err = fork.LoadOwner(ctx); err != nil {
  1096  			ctx.ServerError("LoadOwner", err)
  1097  			return
  1098  		}
  1099  	}
  1100  
  1101  	ctx.Data["Forks"] = forks
  1102  
  1103  	ctx.HTML(http.StatusOK, tplForks)
  1104  }