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.

git.go 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package code
  4. import (
  5. "context"
  6. "strconv"
  7. "strings"
  8. repo_model "code.gitea.io/gitea/models/repo"
  9. "code.gitea.io/gitea/modules/git"
  10. "code.gitea.io/gitea/modules/indexer/code/internal"
  11. "code.gitea.io/gitea/modules/log"
  12. "code.gitea.io/gitea/modules/setting"
  13. )
  14. func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (string, error) {
  15. stdout, _, err := git.NewCommand(ctx, "show-ref", "-s").AddDynamicArguments(git.BranchPrefix + repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
  16. if err != nil {
  17. return "", err
  18. }
  19. return strings.TrimSpace(stdout), nil
  20. }
  21. // getRepoChanges returns changes to repo since last indexer update
  22. func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) {
  23. status, err := repo_model.GetIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode)
  24. if err != nil {
  25. return nil, err
  26. }
  27. needGenesis := len(status.CommitSha) == 0
  28. if !needGenesis {
  29. hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(status.CommitSha, revision)
  30. stdout, _, _ := hasAncestorCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
  31. needGenesis = len(stdout) == 0
  32. }
  33. if needGenesis {
  34. return genesisChanges(ctx, repo, revision)
  35. }
  36. return nonGenesisChanges(ctx, repo, revision)
  37. }
  38. func isIndexable(entry *git.TreeEntry) bool {
  39. if !entry.IsRegular() && !entry.IsExecutable() {
  40. return false
  41. }
  42. name := strings.ToLower(entry.Name())
  43. for _, g := range setting.Indexer.ExcludePatterns {
  44. if g.Match(name) {
  45. return false
  46. }
  47. }
  48. for _, g := range setting.Indexer.IncludePatterns {
  49. if g.Match(name) {
  50. return true
  51. }
  52. }
  53. return len(setting.Indexer.IncludePatterns) == 0
  54. }
  55. // parseGitLsTreeOutput parses the output of a `git ls-tree -r --full-name` command
  56. func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) {
  57. entries, err := git.ParseTreeEntries(stdout)
  58. if err != nil {
  59. return nil, err
  60. }
  61. idxCount := 0
  62. updates := make([]internal.FileUpdate, len(entries))
  63. for _, entry := range entries {
  64. if isIndexable(entry) {
  65. updates[idxCount] = internal.FileUpdate{
  66. Filename: entry.Name(),
  67. BlobSha: entry.ID.String(),
  68. Size: entry.Size(),
  69. Sized: true,
  70. }
  71. idxCount++
  72. }
  73. }
  74. return updates[:idxCount], nil
  75. }
  76. // genesisChanges get changes to add repo to the indexer for the first time
  77. func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) {
  78. var changes internal.RepoChanges
  79. stdout, _, runErr := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision).RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()})
  80. if runErr != nil {
  81. return nil, runErr
  82. }
  83. var err error
  84. changes.Updates, err = parseGitLsTreeOutput(stdout)
  85. return &changes, err
  86. }
  87. // nonGenesisChanges get changes since the previous indexer update
  88. func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) {
  89. diffCmd := git.NewCommand(ctx, "diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision)
  90. stdout, _, runErr := diffCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
  91. if runErr != nil {
  92. // previous commit sha may have been removed by a force push, so
  93. // try rebuilding from scratch
  94. log.Warn("git diff: %v", runErr)
  95. if err := (*globalIndexer.Load()).Delete(ctx, repo.ID); err != nil {
  96. return nil, err
  97. }
  98. return genesisChanges(ctx, repo, revision)
  99. }
  100. var changes internal.RepoChanges
  101. var err error
  102. updatedFilenames := make([]string, 0, 10)
  103. for _, line := range strings.Split(stdout, "\n") {
  104. line = strings.TrimSpace(line)
  105. if len(line) == 0 {
  106. continue
  107. }
  108. fields := strings.Split(line, "\t")
  109. if len(fields) < 2 {
  110. log.Warn("Unparseable output for diff --name-status: `%s`)", line)
  111. continue
  112. }
  113. filename := fields[1]
  114. if len(filename) == 0 {
  115. continue
  116. } else if filename[0] == '"' {
  117. filename, err = strconv.Unquote(filename)
  118. if err != nil {
  119. return nil, err
  120. }
  121. }
  122. switch status := fields[0][0]; status {
  123. case 'M', 'A':
  124. updatedFilenames = append(updatedFilenames, filename)
  125. case 'D':
  126. changes.RemovedFilenames = append(changes.RemovedFilenames, filename)
  127. case 'R', 'C':
  128. if len(fields) < 3 {
  129. log.Warn("Unparseable output for diff --name-status: `%s`)", line)
  130. continue
  131. }
  132. dest := fields[2]
  133. if len(dest) == 0 {
  134. log.Warn("Unparseable output for diff --name-status: `%s`)", line)
  135. continue
  136. }
  137. if dest[0] == '"' {
  138. dest, err = strconv.Unquote(dest)
  139. if err != nil {
  140. return nil, err
  141. }
  142. }
  143. if status == 'R' {
  144. changes.RemovedFilenames = append(changes.RemovedFilenames, filename)
  145. }
  146. updatedFilenames = append(updatedFilenames, dest)
  147. default:
  148. log.Warn("Unrecognized status: %c (line=%s)", status, line)
  149. }
  150. }
  151. cmd := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l").AddDynamicArguments(revision).
  152. AddDashesAndList(updatedFilenames...)
  153. lsTreeStdout, _, err := cmd.RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()})
  154. if err != nil {
  155. return nil, err
  156. }
  157. changes.Updates, err = parseGitLsTreeOutput(lsTreeStdout)
  158. return &changes, err
  159. }