github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/pipeline/lfs.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  //go:build gogit
     7  
     8  package pipeline
     9  
    10  import (
    11  	"bufio"
    12  	"fmt"
    13  	"io"
    14  	"sort"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/gitbundle/modules/git"
    20  
    21  	gogit "github.com/go-git/go-git/v5"
    22  	"github.com/go-git/go-git/v5/plumbing/object"
    23  )
    24  
    25  // LFSResult represents commits found using a provided pointer file hash
    26  type LFSResult struct {
    27  	Name           string
    28  	SHA            string
    29  	Summary        string
    30  	When           time.Time
    31  	ParentHashes   []git.SHA1
    32  	BranchName     string
    33  	FullCommitName string
    34  }
    35  
    36  type lfsResultSlice []*LFSResult
    37  
    38  func (a lfsResultSlice) Len() int           { return len(a) }
    39  func (a lfsResultSlice) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    40  func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
    41  
    42  // FindLFSFile finds commits that contain a provided pointer file hash
    43  func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
    44  	resultsMap := map[string]*LFSResult{}
    45  	results := make([]*LFSResult, 0)
    46  
    47  	basePath := repo.Path
    48  	gogitRepo := repo.GoGitRepo()
    49  
    50  	commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
    51  		Order: gogit.LogOrderCommitterTime,
    52  		All:   true,
    53  	})
    54  	if err != nil {
    55  		return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err)
    56  	}
    57  
    58  	err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
    59  		tree, err := gitCommit.Tree()
    60  		if err != nil {
    61  			return err
    62  		}
    63  		treeWalker := object.NewTreeWalker(tree, true, nil)
    64  		defer treeWalker.Close()
    65  		for {
    66  			name, entry, err := treeWalker.Next()
    67  			if err == io.EOF {
    68  				break
    69  			}
    70  			if entry.Hash == hash {
    71  				result := LFSResult{
    72  					Name:         name,
    73  					SHA:          gitCommit.Hash.String(),
    74  					Summary:      strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
    75  					When:         gitCommit.Author.When,
    76  					ParentHashes: gitCommit.ParentHashes,
    77  				}
    78  				resultsMap[gitCommit.Hash.String()+":"+name] = &result
    79  			}
    80  		}
    81  		return nil
    82  	})
    83  	if err != nil && err != io.EOF {
    84  		return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err)
    85  	}
    86  
    87  	for _, result := range resultsMap {
    88  		hasParent := false
    89  		for _, parentHash := range result.ParentHashes {
    90  			if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
    91  				break
    92  			}
    93  		}
    94  		if !hasParent {
    95  			results = append(results, result)
    96  		}
    97  	}
    98  
    99  	sort.Sort(lfsResultSlice(results))
   100  
   101  	// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
   102  	shasToNameReader, shasToNameWriter := io.Pipe()
   103  	nameRevStdinReader, nameRevStdinWriter := io.Pipe()
   104  	errChan := make(chan error, 1)
   105  	wg := sync.WaitGroup{}
   106  	wg.Add(3)
   107  
   108  	go func() {
   109  		defer wg.Done()
   110  		scanner := bufio.NewScanner(nameRevStdinReader)
   111  		i := 0
   112  		for scanner.Scan() {
   113  			line := scanner.Text()
   114  			if len(line) == 0 {
   115  				continue
   116  			}
   117  			result := results[i]
   118  			result.FullCommitName = line
   119  			result.BranchName = strings.Split(line, "~")[0]
   120  			i++
   121  		}
   122  	}()
   123  	go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
   124  	go func() {
   125  		defer wg.Done()
   126  		defer shasToNameWriter.Close()
   127  		for _, result := range results {
   128  			i := 0
   129  			if i < len(result.SHA) {
   130  				n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
   131  				if err != nil {
   132  					errChan <- err
   133  					break
   134  				}
   135  				i += n
   136  			}
   137  			n := 0
   138  			for n < 1 {
   139  				n, err = shasToNameWriter.Write([]byte{'\n'})
   140  				if err != nil {
   141  					errChan <- err
   142  					break
   143  				}
   144  
   145  			}
   146  
   147  		}
   148  	}()
   149  
   150  	wg.Wait()
   151  
   152  	select {
   153  	case err, has := <-errChan:
   154  		if has {
   155  			return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
   156  		}
   157  	default:
   158  	}
   159  
   160  	return results, nil
   161  }