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