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.

lfs_nogogit.go 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. // Copyright 2020 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. //go:build !gogit
  4. package pipeline
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. "io"
  10. "sort"
  11. "strings"
  12. "sync"
  13. "time"
  14. "code.gitea.io/gitea/modules/git"
  15. )
  16. // LFSResult represents commits found using a provided pointer file hash
  17. type LFSResult struct {
  18. Name string
  19. SHA string
  20. Summary string
  21. When time.Time
  22. ParentHashes []git.SHA1
  23. BranchName string
  24. FullCommitName string
  25. }
  26. type lfsResultSlice []*LFSResult
  27. func (a lfsResultSlice) Len() int { return len(a) }
  28. func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  29. func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
  30. // FindLFSFile finds commits that contain a provided pointer file hash
  31. func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
  32. resultsMap := map[string]*LFSResult{}
  33. results := make([]*LFSResult, 0)
  34. basePath := repo.Path
  35. // Use rev-list to provide us with all commits in order
  36. revListReader, revListWriter := io.Pipe()
  37. defer func() {
  38. _ = revListWriter.Close()
  39. _ = revListReader.Close()
  40. }()
  41. go func() {
  42. stderr := strings.Builder{}
  43. err := git.NewCommand(repo.Ctx, "rev-list", "--all").Run(&git.RunOpts{
  44. Dir: repo.Path,
  45. Stdout: revListWriter,
  46. Stderr: &stderr,
  47. })
  48. if err != nil {
  49. _ = revListWriter.CloseWithError(git.ConcatenateError(err, (&stderr).String()))
  50. } else {
  51. _ = revListWriter.Close()
  52. }
  53. }()
  54. // Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
  55. // so let's create a batch stdin and stdout
  56. batchStdinWriter, batchReader, cancel := repo.CatFileBatch(repo.Ctx)
  57. defer cancel()
  58. // We'll use a scanner for the revList because it's simpler than a bufio.Reader
  59. scan := bufio.NewScanner(revListReader)
  60. trees := [][]byte{}
  61. paths := []string{}
  62. fnameBuf := make([]byte, 4096)
  63. modeBuf := make([]byte, 40)
  64. workingShaBuf := make([]byte, 20)
  65. for scan.Scan() {
  66. // Get the next commit ID
  67. commitID := scan.Bytes()
  68. // push the commit to the cat-file --batch process
  69. _, err := batchStdinWriter.Write(commitID)
  70. if err != nil {
  71. return nil, err
  72. }
  73. _, err = batchStdinWriter.Write([]byte{'\n'})
  74. if err != nil {
  75. return nil, err
  76. }
  77. var curCommit *git.Commit
  78. curPath := ""
  79. commitReadingLoop:
  80. for {
  81. _, typ, size, err := git.ReadBatchLine(batchReader)
  82. if err != nil {
  83. return nil, err
  84. }
  85. switch typ {
  86. case "tag":
  87. // This shouldn't happen but if it does well just get the commit and try again
  88. id, err := git.ReadTagObjectID(batchReader, size)
  89. if err != nil {
  90. return nil, err
  91. }
  92. _, err = batchStdinWriter.Write([]byte(id + "\n"))
  93. if err != nil {
  94. return nil, err
  95. }
  96. continue
  97. case "commit":
  98. // Read in the commit to get its tree and in case this is one of the last used commits
  99. curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(string(commitID)), io.LimitReader(batchReader, size))
  100. if err != nil {
  101. return nil, err
  102. }
  103. if _, err := batchReader.Discard(1); err != nil {
  104. return nil, err
  105. }
  106. _, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n"))
  107. if err != nil {
  108. return nil, err
  109. }
  110. curPath = ""
  111. case "tree":
  112. var n int64
  113. for n < size {
  114. mode, fname, sha20byte, count, err := git.ParseTreeLine(batchReader, modeBuf, fnameBuf, workingShaBuf)
  115. if err != nil {
  116. return nil, err
  117. }
  118. n += int64(count)
  119. if bytes.Equal(sha20byte, hash[:]) {
  120. result := LFSResult{
  121. Name: curPath + string(fname),
  122. SHA: curCommit.ID.String(),
  123. Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
  124. When: curCommit.Author.When,
  125. ParentHashes: curCommit.Parents,
  126. }
  127. resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
  128. } else if string(mode) == git.EntryModeTree.String() {
  129. sha40Byte := make([]byte, 40)
  130. git.To40ByteSHA(sha20byte, sha40Byte)
  131. trees = append(trees, sha40Byte)
  132. paths = append(paths, curPath+string(fname)+"/")
  133. }
  134. }
  135. if _, err := batchReader.Discard(1); err != nil {
  136. return nil, err
  137. }
  138. if len(trees) > 0 {
  139. _, err := batchStdinWriter.Write(trees[len(trees)-1])
  140. if err != nil {
  141. return nil, err
  142. }
  143. _, err = batchStdinWriter.Write([]byte("\n"))
  144. if err != nil {
  145. return nil, err
  146. }
  147. curPath = paths[len(paths)-1]
  148. trees = trees[:len(trees)-1]
  149. paths = paths[:len(paths)-1]
  150. } else {
  151. break commitReadingLoop
  152. }
  153. }
  154. }
  155. }
  156. if err := scan.Err(); err != nil {
  157. return nil, err
  158. }
  159. for _, result := range resultsMap {
  160. hasParent := false
  161. for _, parentHash := range result.ParentHashes {
  162. if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
  163. break
  164. }
  165. }
  166. if !hasParent {
  167. results = append(results, result)
  168. }
  169. }
  170. sort.Sort(lfsResultSlice(results))
  171. // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
  172. shasToNameReader, shasToNameWriter := io.Pipe()
  173. nameRevStdinReader, nameRevStdinWriter := io.Pipe()
  174. errChan := make(chan error, 1)
  175. wg := sync.WaitGroup{}
  176. wg.Add(3)
  177. go func() {
  178. defer wg.Done()
  179. scanner := bufio.NewScanner(nameRevStdinReader)
  180. i := 0
  181. for scanner.Scan() {
  182. line := scanner.Text()
  183. if len(line) == 0 {
  184. continue
  185. }
  186. result := results[i]
  187. result.FullCommitName = line
  188. result.BranchName = strings.Split(line, "~")[0]
  189. i++
  190. }
  191. }()
  192. go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
  193. go func() {
  194. defer wg.Done()
  195. defer shasToNameWriter.Close()
  196. for _, result := range results {
  197. _, err := shasToNameWriter.Write([]byte(result.SHA))
  198. if err != nil {
  199. errChan <- err
  200. break
  201. }
  202. _, err = shasToNameWriter.Write([]byte{'\n'})
  203. if err != nil {
  204. errChan <- err
  205. break
  206. }
  207. }
  208. }()
  209. wg.Wait()
  210. select {
  211. case err, has := <-errChan:
  212. if has {
  213. return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
  214. }
  215. default:
  216. }
  217. return results, nil
  218. }