You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "fmt"
  7. gotemplate "html/template"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. repo_model "code.gitea.io/gitea/models/repo"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/charset"
  15. "code.gitea.io/gitea/modules/context"
  16. "code.gitea.io/gitea/modules/git"
  17. "code.gitea.io/gitea/modules/highlight"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/templates"
  20. "code.gitea.io/gitea/modules/timeutil"
  21. "code.gitea.io/gitea/modules/util"
  22. )
  23. const (
  24. tplBlame base.TplName = "repo/home"
  25. )
  26. type blameRow struct {
  27. RowNumber int
  28. Avatar gotemplate.HTML
  29. RepoLink string
  30. PartSha string
  31. PreviousSha string
  32. PreviousShaURL string
  33. IsFirstCommit bool
  34. CommitURL string
  35. CommitMessage string
  36. CommitSince gotemplate.HTML
  37. Code gotemplate.HTML
  38. EscapeStatus charset.EscapeStatus
  39. }
  40. // RefBlame render blame page
  41. func RefBlame(ctx *context.Context) {
  42. fileName := ctx.Repo.TreePath
  43. if len(fileName) == 0 {
  44. ctx.NotFound("Blame FileName", nil)
  45. return
  46. }
  47. userName := ctx.Repo.Owner.Name
  48. repoName := ctx.Repo.Repository.Name
  49. commitID := ctx.Repo.CommitID
  50. branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
  51. treeLink := branchLink
  52. rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
  53. if len(ctx.Repo.TreePath) > 0 {
  54. treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  55. }
  56. var treeNames []string
  57. paths := make([]string, 0, 5)
  58. if len(ctx.Repo.TreePath) > 0 {
  59. treeNames = strings.Split(ctx.Repo.TreePath, "/")
  60. for i := range treeNames {
  61. paths = append(paths, strings.Join(treeNames[:i+1], "/"))
  62. }
  63. ctx.Data["HasParentPath"] = true
  64. if len(paths)-2 >= 0 {
  65. ctx.Data["ParentPath"] = "/" + paths[len(paths)-1]
  66. }
  67. }
  68. // Get current entry user currently looking at.
  69. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
  70. if err != nil {
  71. ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
  72. return
  73. }
  74. blob := entry.Blob()
  75. ctx.Data["Paths"] = paths
  76. ctx.Data["TreeLink"] = treeLink
  77. ctx.Data["TreeNames"] = treeNames
  78. ctx.Data["BranchLink"] = branchLink
  79. ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  80. ctx.Data["PageIsViewCode"] = true
  81. ctx.Data["IsBlame"] = true
  82. ctx.Data["FileSize"] = blob.Size()
  83. ctx.Data["FileName"] = blob.Name()
  84. ctx.Data["NumLines"], err = blob.GetBlobLineCount()
  85. if err != nil {
  86. ctx.NotFound("GetBlobLineCount", err)
  87. return
  88. }
  89. blameReader, err := git.CreateBlameReader(ctx, repo_model.RepoPath(userName, repoName), commitID, fileName)
  90. if err != nil {
  91. ctx.NotFound("CreateBlameReader", err)
  92. return
  93. }
  94. defer blameReader.Close()
  95. blameParts := make([]git.BlamePart, 0)
  96. for {
  97. blamePart, err := blameReader.NextPart()
  98. if err != nil {
  99. ctx.NotFound("NextPart", err)
  100. return
  101. }
  102. if blamePart == nil {
  103. break
  104. }
  105. blameParts = append(blameParts, *blamePart)
  106. }
  107. // Get Topics of this repo
  108. renderRepoTopics(ctx)
  109. if ctx.Written() {
  110. return
  111. }
  112. commitNames, previousCommits := processBlameParts(ctx, blameParts)
  113. if ctx.Written() {
  114. return
  115. }
  116. renderBlame(ctx, blameParts, commitNames, previousCommits)
  117. ctx.HTML(http.StatusOK, tplBlame)
  118. }
  119. func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]*user_model.UserCommit, map[string]string) {
  120. // store commit data by SHA to look up avatar info etc
  121. commitNames := make(map[string]*user_model.UserCommit)
  122. // previousCommits contains links from SHA to parent SHA,
  123. // if parent also contains the current TreePath.
  124. previousCommits := make(map[string]string)
  125. // and as blameParts can reference the same commits multiple
  126. // times, we cache the lookup work locally
  127. commits := make([]*git.Commit, 0, len(blameParts))
  128. commitCache := map[string]*git.Commit{}
  129. commitCache[ctx.Repo.Commit.ID.String()] = ctx.Repo.Commit
  130. for _, part := range blameParts {
  131. sha := part.Sha
  132. if _, ok := commitNames[sha]; ok {
  133. continue
  134. }
  135. // find the blamePart commit, to look up parent & email address for avatars
  136. commit, ok := commitCache[sha]
  137. var err error
  138. if !ok {
  139. commit, err = ctx.Repo.GitRepo.GetCommit(sha)
  140. if err != nil {
  141. if git.IsErrNotExist(err) {
  142. ctx.NotFound("Repo.GitRepo.GetCommit", err)
  143. } else {
  144. ctx.ServerError("Repo.GitRepo.GetCommit", err)
  145. }
  146. return nil, nil
  147. }
  148. commitCache[sha] = commit
  149. }
  150. // find parent commit
  151. if commit.ParentCount() > 0 {
  152. psha := commit.Parents[0]
  153. previousCommit, ok := commitCache[psha.String()]
  154. if !ok {
  155. previousCommit, _ = commit.Parent(0)
  156. if previousCommit != nil {
  157. commitCache[psha.String()] = previousCommit
  158. }
  159. }
  160. // only store parent commit ONCE, if it has the file
  161. if previousCommit != nil {
  162. if haz1, _ := previousCommit.HasFile(ctx.Repo.TreePath); haz1 {
  163. previousCommits[commit.ID.String()] = previousCommit.ID.String()
  164. }
  165. }
  166. }
  167. commits = append(commits, commit)
  168. }
  169. // populate commit email addresses to later look up avatars.
  170. for _, c := range user_model.ValidateCommitsWithEmails(commits) {
  171. commitNames[c.ID.String()] = c
  172. }
  173. return commitNames, previousCommits
  174. }
  175. func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]*user_model.UserCommit, previousCommits map[string]string) {
  176. repoLink := ctx.Repo.RepoLink
  177. language := ""
  178. indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
  179. if err == nil {
  180. defer deleteTemporaryFile()
  181. filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
  182. CachedOnly: true,
  183. Attributes: []string{"linguist-language", "gitlab-language"},
  184. Filenames: []string{ctx.Repo.TreePath},
  185. IndexFile: indexFilename,
  186. WorkTree: worktree,
  187. })
  188. if err != nil {
  189. log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
  190. }
  191. language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
  192. if language == "" || language == "unspecified" {
  193. language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
  194. }
  195. if language == "unspecified" {
  196. language = ""
  197. }
  198. }
  199. lines := make([]string, 0)
  200. rows := make([]*blameRow, 0)
  201. escapeStatus := charset.EscapeStatus{}
  202. i := 0
  203. commitCnt := 0
  204. for _, part := range blameParts {
  205. for index, line := range part.Lines {
  206. i++
  207. lines = append(lines, line)
  208. br := &blameRow{
  209. RowNumber: i,
  210. }
  211. commit := commitNames[part.Sha]
  212. previousSha := previousCommits[part.Sha]
  213. if index == 0 {
  214. // Count commit number
  215. commitCnt++
  216. // User avatar image
  217. commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale.Language())
  218. var avatar string
  219. if commit.User != nil {
  220. avatar = string(templates.Avatar(commit.User, 18, "mr-3"))
  221. } else {
  222. avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
  223. }
  224. br.Avatar = gotemplate.HTML(avatar)
  225. br.RepoLink = repoLink
  226. br.PartSha = part.Sha
  227. br.PreviousSha = previousSha
  228. br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(previousSha), util.PathEscapeSegments(ctx.Repo.TreePath))
  229. br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
  230. br.CommitMessage = commit.CommitMessage
  231. br.CommitSince = commitSince
  232. }
  233. if i != len(lines)-1 {
  234. line += "\n"
  235. }
  236. fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
  237. line = highlight.Code(fileName, language, line)
  238. br.EscapeStatus, line = charset.EscapeControlString(line)
  239. br.Code = gotemplate.HTML(line)
  240. rows = append(rows, br)
  241. escapeStatus = escapeStatus.Or(br.EscapeStatus)
  242. }
  243. }
  244. ctx.Data["EscapeStatus"] = escapeStatus
  245. ctx.Data["BlameRows"] = rows
  246. ctx.Data["CommitCnt"] = commitCnt
  247. }