blob: a073e26eb02176e3e77e1bb0a78dfe424e8e1c14 [file] [log] [blame]
package git
import (
"bytes"
"context"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/vfs"
)
// VFS returns a vfs.FS using Git for the given revision.
func (g GitDir) VFS(ctx context.Context, ref string) (*FS, error) {
hash, err := g.RevParse(ctx, "--verify", ref+"^{commit}")
if err != nil {
return nil, skerr.Wrap(err)
}
return &FS{
g: g,
hash: hash,
}, nil
}
// FS implements vfs.FS using Git for a particular revision.
type FS struct {
g GitDir
hash string
}
// Open implements vfs.FS.
func (fs *FS) Open(_ context.Context, name string) (vfs.File, error) {
repoRoot, err := filepath.Abs(string(fs.g))
if err != nil {
return nil, skerr.Wrap(err)
}
absPath, err := filepath.Abs(filepath.Join(string(fs.g), name))
if err != nil {
return nil, skerr.Wrap(err)
}
return &File{
g: fs.g,
isRoot: repoRoot == absPath,
hash: fs.hash,
path: name,
}, nil
}
// Create implements vfs.FS.
func (fs *FS) Create(_ context.Context, name string) (vfs.File, error) {
return nil, skerr.Fmt("Create not implemented for git.FS. Did you mean to use go/vfs.Local?")
}
// Close implements vfs.FS.
func (fs *FS) Close(_ context.Context) error {
return nil
}
// Ensure that FS implements vfs.FS.
var _ vfs.FS = &FS{}
// File implements vfs.File using Git for a particular revision.
type File struct {
g GitDir
isRoot bool
hash string
path string
// These are cached to avoid repeated calls to Git.
cachedFileInfo fs.FileInfo
cachedContents []byte
// reader is used for repeated calls to Read().
reader io.Reader
}
// Close implements vfs.File.
func (f *File) Close(_ context.Context) error {
return nil
}
// get retrieves the contents of this File if they are not already cached.
func (f *File) get(ctx context.Context) ([]byte, error) {
if f.cachedContents == nil {
path := f.path
if f.isRoot && !strings.HasSuffix(path, "/") {
path = path + "/"
}
contents, err := f.g.CatFile(ctx, f.hash, path)
if err != nil {
return nil, skerr.Wrap(err)
}
f.cachedContents = contents
}
return f.cachedContents, nil
}
// Read implements vfs.File.
func (f *File) Read(ctx context.Context, buf []byte) (int, error) {
if f.reader == nil {
contents, err := f.get(ctx)
if err != nil {
return 0, skerr.Wrap(err)
}
f.reader = bytes.NewReader(contents)
}
n, err := f.reader.Read(buf)
if err == io.EOF {
f.reader = nil
return n, err
}
return n, skerr.Wrap(err)
}
// Stat implements vfs.File.
func (f *File) Stat(ctx context.Context) (fs.FileInfo, error) {
// Special case for the repo root.
if f.isRoot {
return vfs.FileInfo{
Name: f.path,
Size: 0,
Mode: os.ModePerm | os.ModeDir,
// Git doesn't track modification times.
ModTime: time.Time{},
IsDir: true,
Sys: nil,
}.Get(), nil
}
// Find the file in its parent dir.
dir, file := filepath.Split(f.path)
infos, err := f.g.ReadDir(ctx, f.hash, dir)
if err != nil {
return nil, skerr.Wrap(err)
}
for _, fi := range infos {
if fi.Name() == file {
return fi, nil
}
}
return nil, skerr.Fmt("Unable to find %q in %q", file, dir)
}
// ReadDir implements vfs.File.
func (f *File) ReadDir(ctx context.Context, n int) ([]fs.FileInfo, error) {
contents, err := f.get(ctx)
if err != nil {
return nil, skerr.Wrap(err)
}
rv, err := ParseDir(contents)
if err != nil {
return nil, skerr.Wrap(err)
}
if n > 0 {
rv = rv[:n]
}
return rv, nil
}
// Write implements vfs.File.
func (f *File) Write(_ context.Context, _ []byte) (int, error) {
return 0, skerr.Fmt("Write not implemented for git.File. Did you mean to use go/vfs.Local?")
}
// Ensure that File implements vfs.File.
var _ vfs.File = &File{}