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