選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

lfs_nogogit.go 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. ParentIDs []git.ObjectID
  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, objectID git.ObjectID) ([]*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, objectID.Type().FullLength()/2)
  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. if _, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n")); err != nil {
  107. return nil, err
  108. }
  109. curPath = ""
  110. case "tree":
  111. var n int64
  112. for n < size {
  113. mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
  114. if err != nil {
  115. return nil, err
  116. }
  117. n += int64(count)
  118. if bytes.Equal(binObjectID, objectID.RawValue()) {
  119. result := LFSResult{
  120. Name: curPath + string(fname),
  121. SHA: curCommit.ID.String(),
  122. Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
  123. When: curCommit.Author.When,
  124. ParentIDs: curCommit.Parents,
  125. }
  126. resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
  127. } else if string(mode) == git.EntryModeTree.String() {
  128. hexObjectID := make([]byte, objectID.Type().FullLength())
  129. git.BinToHex(objectID.Type(), binObjectID, hexObjectID)
  130. trees = append(trees, hexObjectID)
  131. paths = append(paths, curPath+string(fname)+"/")
  132. }
  133. }
  134. if _, err := batchReader.Discard(1); err != nil {
  135. return nil, err
  136. }
  137. if len(trees) > 0 {
  138. _, err := batchStdinWriter.Write(trees[len(trees)-1])
  139. if err != nil {
  140. return nil, err
  141. }
  142. _, err = batchStdinWriter.Write([]byte("\n"))
  143. if err != nil {
  144. return nil, err
  145. }
  146. curPath = paths[len(paths)-1]
  147. trees = trees[:len(trees)-1]
  148. paths = paths[:len(paths)-1]
  149. } else {
  150. break commitReadingLoop
  151. }
  152. default:
  153. if err := git.DiscardFull(batchReader, size+1); err != nil {
  154. return nil, err
  155. }
  156. }
  157. }
  158. }
  159. if err := scan.Err(); err != nil {
  160. return nil, err
  161. }
  162. for _, result := range resultsMap {
  163. hasParent := false
  164. for _, parentID := range result.ParentIDs {
  165. if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent {
  166. break
  167. }
  168. }
  169. if !hasParent {
  170. results = append(results, result)
  171. }
  172. }
  173. sort.Sort(lfsResultSlice(results))
  174. // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
  175. shasToNameReader, shasToNameWriter := io.Pipe()
  176. nameRevStdinReader, nameRevStdinWriter := io.Pipe()
  177. errChan := make(chan error, 1)
  178. wg := sync.WaitGroup{}
  179. wg.Add(3)
  180. go func() {
  181. defer wg.Done()
  182. scanner := bufio.NewScanner(nameRevStdinReader)
  183. i := 0
  184. for scanner.Scan() {
  185. line := scanner.Text()
  186. if len(line) == 0 {
  187. continue
  188. }
  189. result := results[i]
  190. result.FullCommitName = line
  191. result.BranchName = strings.Split(line, "~")[0]
  192. i++
  193. }
  194. }()
  195. go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
  196. go func() {
  197. defer wg.Done()
  198. defer shasToNameWriter.Close()
  199. for _, result := range results {
  200. _, err := shasToNameWriter.Write([]byte(result.SHA))
  201. if err != nil {
  202. errChan <- err
  203. break
  204. }
  205. _, err = shasToNameWriter.Write([]byte{'\n'})
  206. if err != nil {
  207. errChan <- err
  208. break
  209. }
  210. }
  211. }()
  212. wg.Wait()
  213. select {
  214. case err, has := <-errChan:
  215. if has {
  216. return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
  217. }
  218. default:
  219. }
  220. return results, nil
  221. }