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  }