Monorepo for Tangled
Something went wrong. Try again.
8.4 kB · 425 lines
Go
at icy/yovxsu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426package git
import ( "archive/tar" "bytes" "errors" "fmt" "io" "io/fs" "path" "strconv" "strings" "time"
"github.com/go-git/go-git/v5" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object")
var ( ErrBinaryFile = errors.New("binary file") ErrNotBinaryFile = errors.New("not binary file") ErrMissingGitModules = errors.New("no .gitmodules file found") ErrInvalidGitModules = errors.New("invalid .gitmodules file") ErrNotSubmodule = errors.New("path is not a submodule"))
type GitRepo struct { path string r *git.Repository h plumbing.Hash}
// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo// to tar WriteHeadertype infoWrapper struct { name string size int64 mode fs.FileMode modTime time.Time isDir bool}
func Open(path string, ref string) (*GitRepo, error) { var err error g := GitRepo{path: path} g.r, err = git.PlainOpen(path) if err != nil { return nil, fmt.Errorf("opening %s: %w", path, err) }
if ref == "" { head, err := g.r.Head() if err != nil { return nil, fmt.Errorf("getting head of %s: %w", path, err) } g.h = head.Hash() } else { hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) if err != nil { return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) } g.h = *hash } return &g, nil}
func PlainOpen(path string) (*GitRepo, error) { var err error g := GitRepo{path: path} g.r, err = git.PlainOpen(path) if err != nil { return nil, fmt.Errorf("opening %s: %w", path, err) } return &g, nil}
func (g *GitRepo) Hash() plumbing.Hash { return g.h}
// re-open a repository and update referencesfunc (g *GitRepo) Refresh() error { refreshed, err := Open(g.path, g.Hash().String()) if err != nil { return err }
*g = *refreshed return nil}
func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) { commits := []*object.Commit{}
output, err := g.revList( g.h.String(), fmt.Sprintf("--skip=%d", offset), fmt.Sprintf("--max-count=%d", limit), ) if err != nil { return nil, fmt.Errorf("commits from ref: %w", err) }
lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) == 1 && lines[0] == "" { return commits, nil }
for _, item := range lines { obj, err := g.r.CommitObject(plumbing.NewHash(item)) if err != nil { continue } commits = append(commits, obj) }
return commits, nil}
func (g *GitRepo) TotalCommits() (int, error) { output, err := g.revList( g.h.String(), "--count", ) if err != nil { return 0, fmt.Errorf("failed to run rev-list: %w", err) }
count, err := strconv.Atoi(strings.TrimSpace(string(output))) if err != nil { return 0, err }
return count, nil}
func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { return g.r.CommitObject(h)}
func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) { c, err := g.r.CommitObject(g.h) if err != nil { return nil, fmt.Errorf("commit object: %w", err) }
tree, err := c.Tree() if err != nil { return nil, fmt.Errorf("file tree: %w", err) }
file, err := tree.File(path) if err != nil { return nil, err }
isbin, _ := file.IsBinary() if isbin { return nil, ErrBinaryFile }
reader, err := file.Reader() if err != nil { return nil, err } defer reader.Close()
buf := new(bytes.Buffer) if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil { return nil, err }
return buf.Bytes(), nil}
func (g *GitRepo) RawContent(path string) ([]byte, error) { c, err := g.r.CommitObject(g.h) if err != nil { return nil, fmt.Errorf("commit object: %w", err) }
tree, err := c.Tree() if err != nil { return nil, fmt.Errorf("file tree: %w", err) }
file, err := tree.File(path) if err != nil { return nil, err }
reader, err := file.Reader() if err != nil { return nil, fmt.Errorf("opening file reader: %w", err) } defer reader.Close()
return io.ReadAll(reader)}
func (g *GitRepo) File(path string) (*object.File, error) { c, err := g.r.CommitObject(g.h) if err != nil { return nil, fmt.Errorf("commit object: %w", err) }
tree, err := c.Tree() if err != nil { return nil, fmt.Errorf("file tree: %w", err) }
return tree.File(path)}
// read and parse .gitmodulesfunc (g *GitRepo) Submodules() (*config.Modules, error) { c, err := g.r.CommitObject(g.h) if err != nil { return nil, fmt.Errorf("commit object: %w", err) }
tree, err := c.Tree() if err != nil { return nil, fmt.Errorf("tree: %w", err) }
// read .gitmodules file modulesEntry, err := tree.FindEntry(".gitmodules") if err != nil { return nil, fmt.Errorf("%w: %w", ErrMissingGitModules, err) }
modulesFile, err := tree.TreeEntryFile(modulesEntry) if err != nil { return nil, fmt.Errorf("%w: failed to read file: %w", ErrInvalidGitModules, err) }
content, err := modulesFile.Contents() if err != nil { return nil, fmt.Errorf("%w: failed to read contents: %w", ErrInvalidGitModules, err) }
// parse .gitmodules modules := config.NewModules() if err = modules.Unmarshal([]byte(content)); err != nil { return nil, fmt.Errorf("%w: failed to parse: %w", ErrInvalidGitModules, err) }
return modules, nil}
func (g *GitRepo) Submodule(path string) (*config.Submodule, error) { modules, err := g.Submodules() if err != nil { return nil, err }
for _, submodule := range modules.Submodules { if submodule.Path == path { return submodule, nil } }
// path is not a submodule return nil, ErrNotSubmodule}
func (g *GitRepo) SetDefaultBranch(branch string) error { ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) return g.r.Storer.SetReference(ref)}
func (g *GitRepo) FindMainBranch() (string, error) { output, err := g.revParse("--abbrev-ref", "HEAD") if err != nil { return "", fmt.Errorf("failed to find main branch: %w", err) }
return strings.TrimSpace(string(output)), nil}
func (g *GitRepo) Remote() (string, error) { remote, err := g.r.Remote("origin") if errors.Is(err, gogit.ErrRemoteNotFound) { return "", nil } if err != nil { return "", err }
if remote == nil { return "", nil }
urls := remote.Config().URLs if len(urls) == 0 { return "", nil }
return urls[0], nil}
// WriteTar writes itself from a tree into a binary tar file format.// prefix is root folder to be appended.func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { tw := tar.NewWriter(w) defer tw.Close()
c, err := g.r.CommitObject(g.h) if err != nil { return fmt.Errorf("commit object: %w", err) }
tree, err := c.Tree() if err != nil { return err }
walker := object.NewTreeWalker(tree, true, nil) defer walker.Close()
name, entry, err := walker.Next() for ; err == nil; name, entry, err = walker.Next() { info, err := newInfoWrapper(name, prefix, &entry, tree) if err != nil { return err }
header, err := tar.FileInfoHeader(info, "") if err != nil { return err }
err = tw.WriteHeader(header) if err != nil { return err }
if !info.IsDir() { file, err := tree.File(name) if err != nil { return err }
reader, err := file.Blob.Reader() if err != nil { return err }
_, err = io.Copy(tw, reader) if err != nil { reader.Close() return err } reader.Close() } }
return nil}
func newInfoWrapper( name string, prefix string, entry *object.TreeEntry, tree *object.Tree,) (*infoWrapper, error) { var ( size int64 mode fs.FileMode isDir bool )
if entry.Mode.IsFile() { file, err := tree.TreeEntryFile(entry) if err != nil { return nil, err } mode = fs.FileMode(file.Mode)
size, err = tree.Size(name) if err != nil { return nil, err } } else { isDir = true mode = fs.ModeDir | fs.ModePerm }
fullname := path.Join(prefix, name) return &infoWrapper{ name: fullname, size: size, mode: mode, modTime: time.Unix(0, 0), isDir: isDir, }, nil}
func (i *infoWrapper) Name() string { return i.name}
func (i *infoWrapper) Size() int64 { return i.size}
func (i *infoWrapper) Mode() fs.FileMode { return i.mode}
func (i *infoWrapper) ModTime() time.Time { return i.modTime}
func (i *infoWrapper) IsDir() bool { return i.isDir}
func (i *infoWrapper) Sys() any { return nil}