server/pkg/git/repository.go

Summary

Maintainability
B
4 hrs
Test Coverage
F
9%
package git

import (
    "archive/tar"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "path"
    "time"

    "github.com/go-git/go-billy/v5/memfs"
    git "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
    "github.com/go-git/go-git/v5/plumbing/transport"
    "github.com/go-git/go-git/v5/storage/memory"
)

type CloneOptions struct {
    TagName           string
    BranchName        string
    ReferenceName     string
    RecurseSubmodules git.SubmoduleRescursivity
    Auth              transport.AuthMethod
}

func CloneInMemory(url string, opts CloneOptions) (*git.Repository, error) {
    storage := memory.NewStorage()
    fs := memfs.New()

    cloneOptions := &git.CloneOptions{}
    {
        cloneOptions.URL = url

        switch {
        case opts.TagName != "":
            cloneOptions.ReferenceName = plumbing.ReferenceName(fmt.Sprintf("refs/tags/%s", opts.TagName))
        case opts.BranchName != "":
            cloneOptions.ReferenceName = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", opts.BranchName))
        case opts.ReferenceName != "":
            cloneOptions.ReferenceName = plumbing.ReferenceName(opts.ReferenceName)
        }

        if opts.RecurseSubmodules != 0 {
            cloneOptions.RecurseSubmodules = opts.RecurseSubmodules
        }

        if opts.Auth != nil {
            cloneOptions.Auth = opts.Auth
        }
    }

    return git.Clone(storage, fs, cloneOptions)
}

func AddWorktreeFilesToTar(tw *tar.Writer, gitRepo *git.Repository) error {
    return ForEachWorktreeFile(gitRepo, func(path, link string, fileReader io.Reader, info os.FileInfo) error {
        size := info.Size()

        // The size field is the size of the file in bytes; linked files are archived with this field specified as zero
        if link != "" {
            size = 0
        }

        if err := tw.WriteHeader(&tar.Header{
            Format:     tar.FormatGNU,
            Name:       path,
            Linkname:   link,
            Size:       size,
            Mode:       int64(info.Mode()),
            ModTime:    time.Now(),
            AccessTime: time.Now(),
            ChangeTime: time.Now(),
        }); err != nil {
            return fmt.Errorf("unable to write tar entry %q header: %w", path, err)
        }

        if link == "" {
            _, err := io.Copy(tw, fileReader)
            if err != nil {
                return fmt.Errorf("unable to write tar entry %q data: %w", path, err)
            }
        }

        return nil
    })
}

func ForEachWorktreeFile(gitRepo *git.Repository, fileFunc func(path, link string, fileReader io.Reader, info os.FileInfo) error) error {
    w, err := gitRepo.Worktree()
    if err != nil {
        return fmt.Errorf("unable to get git repository worktree: %w", err)
    }

    fs := w.Filesystem

    var processFilesFunc func(directory string, files []os.FileInfo) error
    processFilesFunc = func(directory string, fileInfoList []os.FileInfo) error {
        for _, fileInfo := range fileInfoList {
            absPath := path.Join(directory, fileInfo.Name())
            if fileInfo.IsDir() {
                fFileInfoList, err := fs.ReadDir(absPath)
                if err != nil {
                    return fmt.Errorf("unable to read dir %q: %w", absPath, err)
                }

                if err := processFilesFunc(absPath, fFileInfoList); err != nil {
                    return err
                }

                continue
            }

            if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
                link, err := fs.Readlink(absPath)
                if err != nil {
                    return fmt.Errorf("unable to read link %q: %w", absPath, err)
                }

                if err := fileFunc(absPath, link, nil, fileInfo); err != nil {
                    return err
                }
            } else {
                billyFile, err := fs.Open(absPath)
                if err != nil {
                    return fmt.Errorf("unable to open file %q: %w", absPath, err)
                }

                if err := fileFunc(absPath, "", billyFile, fileInfo); err != nil {
                    return err
                }

                if err := billyFile.Close(); err != nil {
                    return err
                }
            }
        }

        return nil
    }

    rootDirectory := ""
    files, err := fs.ReadDir(rootDirectory)
    if err != nil {
        return fmt.Errorf("unable to read root directory: %w", err)
    }

    return processFilesFunc(rootDirectory, files)
}

func ReadWorktreeFile(gitRepo *git.Repository, path string) ([]byte, error) {
    w, err := gitRepo.Worktree()
    if err != nil {
        return nil, fmt.Errorf("unable to get git repository worktree: %w", err)
    }

    fs := w.Filesystem

    f, err := fs.Open(path)
    if err != nil {
        return nil, fmt.Errorf("unable to open git repository worktree file %q: %w", path, err)
    }

    data, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, fmt.Errorf("unable to read git repository worktree file %q: %w", path, err)
    }

    return data, nil
}

func IsAncestor(gitRepo *git.Repository, ancestorCommit, descendantCommit string) (bool, error) {
    ancestorCommitObj, err := gitRepo.CommitObject(plumbing.NewHash(ancestorCommit))
    if err != nil {
        return false, fmt.Errorf("unable to get commit %q object: %w", ancestorCommit, err)
    }

    descendantCommitObj, err := gitRepo.CommitObject(plumbing.NewHash(descendantCommit))
    if err != nil {
        return false, fmt.Errorf("unable to get commit %q object: %w", descendantCommitObj, err)
    }

    isAncestor, err := ancestorCommitObj.IsAncestor(descendantCommitObj)
    if err != nil {
        return false, fmt.Errorf("unable to check ancestry of git commit %q to %q: %w", ancestorCommit, descendantCommit, err)
    }

    return isAncestor, nil
}