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  }