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.2KB

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