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