123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- // Copyright 2020 The Gitea Authors. All rights reserved.
- // Use of this source code is governed by a MIT-style
- // license that can be found in the LICENSE file.
-
- //go:build !gogit
-
- package pipeline
-
- import (
- "bufio"
- "bytes"
- "fmt"
- "io"
- "sort"
- "strings"
- "sync"
- "time"
-
- "code.gitea.io/gitea/modules/git"
- )
-
- // LFSResult represents commits found using a provided pointer file hash
- type LFSResult struct {
- Name string
- SHA string
- Summary string
- When time.Time
- ParentHashes []git.SHA1
- BranchName string
- FullCommitName string
- }
-
- type lfsResultSlice []*LFSResult
-
- func (a lfsResultSlice) Len() int { return len(a) }
- func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
- func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
-
- // FindLFSFile finds commits that contain a provided pointer file hash
- func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
- resultsMap := map[string]*LFSResult{}
- results := make([]*LFSResult, 0)
-
- basePath := repo.Path
-
- // Use rev-list to provide us with all commits in order
- revListReader, revListWriter := io.Pipe()
- defer func() {
- _ = revListWriter.Close()
- _ = revListReader.Close()
- }()
-
- go func() {
- stderr := strings.Builder{}
- err := git.NewCommand(repo.Ctx, "rev-list", "--all").Run(&git.RunOpts{
- Dir: repo.Path,
- Stdout: revListWriter,
- Stderr: &stderr,
- })
- if err != nil {
- _ = revListWriter.CloseWithError(git.ConcatenateError(err, (&stderr).String()))
- } else {
- _ = revListWriter.Close()
- }
- }()
-
- // Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
- // so let's create a batch stdin and stdout
- batchStdinWriter, batchReader, cancel := repo.CatFileBatch(repo.Ctx)
- defer cancel()
-
- // We'll use a scanner for the revList because it's simpler than a bufio.Reader
- scan := bufio.NewScanner(revListReader)
- trees := [][]byte{}
- paths := []string{}
-
- fnameBuf := make([]byte, 4096)
- modeBuf := make([]byte, 40)
- workingShaBuf := make([]byte, 20)
-
- for scan.Scan() {
- // Get the next commit ID
- commitID := scan.Bytes()
-
- // push the commit to the cat-file --batch process
- _, err := batchStdinWriter.Write(commitID)
- if err != nil {
- return nil, err
- }
- _, err = batchStdinWriter.Write([]byte{'\n'})
- if err != nil {
- return nil, err
- }
-
- var curCommit *git.Commit
- curPath := ""
-
- commitReadingLoop:
- for {
- _, typ, size, err := git.ReadBatchLine(batchReader)
- if err != nil {
- return nil, err
- }
-
- switch typ {
- case "tag":
- // This shouldn't happen but if it does well just get the commit and try again
- id, err := git.ReadTagObjectID(batchReader, size)
- if err != nil {
- return nil, err
- }
- _, err = batchStdinWriter.Write([]byte(id + "\n"))
- if err != nil {
- return nil, err
- }
- continue
- case "commit":
- // Read in the commit to get its tree and in case this is one of the last used commits
- curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(string(commitID)), io.LimitReader(batchReader, int64(size)))
- if err != nil {
- return nil, err
- }
- if _, err := batchReader.Discard(1); err != nil {
- return nil, err
- }
-
- _, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n"))
- if err != nil {
- return nil, err
- }
- curPath = ""
- case "tree":
- var n int64
- for n < size {
- mode, fname, sha20byte, count, err := git.ParseTreeLine(batchReader, modeBuf, fnameBuf, workingShaBuf)
- if err != nil {
- return nil, err
- }
- n += int64(count)
- if bytes.Equal(sha20byte, hash[:]) {
- result := LFSResult{
- Name: curPath + string(fname),
- SHA: curCommit.ID.String(),
- Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
- When: curCommit.Author.When,
- ParentHashes: curCommit.Parents,
- }
- resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
- } else if string(mode) == git.EntryModeTree.String() {
- sha40Byte := make([]byte, 40)
- git.To40ByteSHA(sha20byte, sha40Byte)
- trees = append(trees, sha40Byte)
- paths = append(paths, curPath+string(fname)+"/")
- }
- }
- if _, err := batchReader.Discard(1); err != nil {
- return nil, err
- }
- if len(trees) > 0 {
- _, err := batchStdinWriter.Write(trees[len(trees)-1])
- if err != nil {
- return nil, err
- }
- _, err = batchStdinWriter.Write([]byte("\n"))
- if err != nil {
- return nil, err
- }
- curPath = paths[len(paths)-1]
- trees = trees[:len(trees)-1]
- paths = paths[:len(paths)-1]
- } else {
- break commitReadingLoop
- }
- }
- }
- }
-
- if err := scan.Err(); err != nil {
- return nil, err
- }
-
- for _, result := range resultsMap {
- hasParent := false
- for _, parentHash := range result.ParentHashes {
- if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
- break
- }
- }
- if !hasParent {
- results = append(results, result)
- }
- }
-
- sort.Sort(lfsResultSlice(results))
-
- // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
- shasToNameReader, shasToNameWriter := io.Pipe()
- nameRevStdinReader, nameRevStdinWriter := io.Pipe()
- errChan := make(chan error, 1)
- wg := sync.WaitGroup{}
- wg.Add(3)
-
- go func() {
- defer wg.Done()
- scanner := bufio.NewScanner(nameRevStdinReader)
- i := 0
- for scanner.Scan() {
- line := scanner.Text()
- if len(line) == 0 {
- continue
- }
- result := results[i]
- result.FullCommitName = line
- result.BranchName = strings.Split(line, "~")[0]
- i++
- }
- }()
- go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
- go func() {
- defer wg.Done()
- defer shasToNameWriter.Close()
- for _, result := range results {
- _, err := shasToNameWriter.Write([]byte(result.SHA))
- if err != nil {
- errChan <- err
- break
- }
- _, err = shasToNameWriter.Write([]byte{'\n'})
- if err != nil {
- errChan <- err
- break
- }
-
- }
- }()
-
- wg.Wait()
-
- select {
- case err, has := <-errChan:
- if has {
- return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
- }
- default:
- }
-
- return results, nil
- }
|