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

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