diff options
Diffstat (limited to 'storage/filesystem/internal/dotgit')
-rw-r--r-- | storage/filesystem/internal/dotgit/dotgit.go | 105 | ||||
-rw-r--r-- | storage/filesystem/internal/dotgit/dotgit_test.go | 224 | ||||
-rw-r--r-- | storage/filesystem/internal/dotgit/refs.go | 144 |
3 files changed, 473 insertions, 0 deletions
diff --git a/storage/filesystem/internal/dotgit/dotgit.go b/storage/filesystem/internal/dotgit/dotgit.go new file mode 100644 index 0000000..f365f13 --- /dev/null +++ b/storage/filesystem/internal/dotgit/dotgit.go @@ -0,0 +1,105 @@ +package dotgit + +import ( + "errors" + "os" + "strings" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/utils/fs" +) + +const ( + suffix = ".git" + packedRefsPath = "packed-refs" +) + +var ( + // ErrNotFound is returned by New when the path is not found. + ErrNotFound = errors.New("path not found") + // ErrIdxNotFound is returned by Idxfile when the idx file is not found on the + // repository. + ErrIdxNotFound = errors.New("idx file not found") + // ErrPackfileNotFound is returned by Packfile when the packfile is not found + // on the repository. + ErrPackfileNotFound = errors.New("packfile not found") +) + +// The DotGit type represents a local git repository on disk. This +// type is not zero-value-safe, use the New function to initialize it. +type DotGit struct { + fs fs.FS + path string +} + +// New returns a DotGit value ready to be used. The path argument must +// be the absolute path of a git repository directory (e.g. +// "/foo/bar/.git"). +func New(fs fs.FS, path string) (*DotGit, error) { + d := &DotGit{fs: fs, path: path} + if _, err := fs.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, ErrNotFound + } + return nil, err + } + + return d, nil +} + +// Refs scans the git directory collecting references, which it returns. +// Symbolic references are resolved and included in the output. +func (d *DotGit) Refs() ([]*core.Reference, error) { + var refs []*core.Reference + if err := d.addRefsFromPackedRefs(&refs); err != nil { + return nil, err + } + + if err := d.addRefsFromRefDir(&refs); err != nil { + return nil, err + } + + if err := d.addRefFromHEAD(&refs); err != nil { + return nil, err + } + + return refs, nil +} + +// Packfile returns the path of the packfile (really, it returns the +// path of the first file in the "objects/pack/" directory with a +// ".pack" extension. +func (d *DotGit) Packfile() (fs.FS, string, error) { + packDir := d.fs.Join(d.path, "objects", "pack") + files, err := d.fs.ReadDir(packDir) + if err != nil { + return nil, "", err + } + + for _, f := range files { + if strings.HasSuffix(f.Name(), ".pack") { + return d.fs, d.fs.Join(packDir, f.Name()), nil + } + } + + return nil, "", ErrPackfileNotFound +} + +// Idxfile returns the path of the idx file (really, it returns the +// path of the first file in the "objects/pack/" directory with an +// ".idx" extension. +func (d *DotGit) Idxfile() (fs.FS, string, error) { + packDir := d.fs.Join(d.path, "objects", "pack") + files, err := d.fs.ReadDir(packDir) + if err != nil { + return nil, "", err + } + + for _, f := range files { + if strings.HasSuffix(f.Name(), ".idx") { + return d.fs, d.fs.Join(packDir, f.Name()), nil + } + } + + return nil, "", ErrIdxNotFound +} diff --git a/storage/filesystem/internal/dotgit/dotgit_test.go b/storage/filesystem/internal/dotgit/dotgit_test.go new file mode 100644 index 0000000..6125114 --- /dev/null +++ b/storage/filesystem/internal/dotgit/dotgit_test.go @@ -0,0 +1,224 @@ +package dotgit + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "gopkg.in/src-d/go-git.v4/clients/common" + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/utils/fs" + + "github.com/alcortesm/tgz" + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +var initFixtures = [...]struct { + name string + tgz string + capabilities [][2]string + packfile string + idxfile string +}{ + { + name: "spinnaker", + tgz: "fixtures/spinnaker-gc.tgz", + capabilities: [][2]string{ + {"symref", "HEAD:refs/heads/master"}, + }, + packfile: "objects/pack/pack-584416f86235cac0d54bfabbdc399fb2b09a5269.pack", + idxfile: "objects/pack/pack-584416f86235cac0d54bfabbdc399fb2b09a5269.idx", + }, { + name: "no-packfile-no-idx", + tgz: "fixtures/no-packfile-no-idx.tgz", + }, { + name: "empty", + tgz: "fixtures/empty-gitdir.tgz", + }, +} + +type fixture struct { + installDir string + fs fs.FS + path string // repo names to paths of the extracted tgz + capabilities *common.Capabilities // expected capabilities + packfile string // path of the packfile + idxfile string // path of the idxfile +} + +type SuiteDotGit struct { + fixtures map[string]fixture +} + +var _ = Suite(&SuiteDotGit{}) + +func (s *SuiteDotGit) SetUpSuite(c *C) { + s.fixtures = make(map[string]fixture, len(initFixtures)) + + for _, init := range initFixtures { + com := Commentf("fixture name = %s\n", init.name) + + path, err := tgz.Extract(init.tgz) + c.Assert(err, IsNil, com) + + f := fixture{} + + f.installDir = path + f.fs = fs.NewOS() + f.path = f.fs.Join(path, ".git") + + f.capabilities = common.NewCapabilities() + for _, pair := range init.capabilities { + f.capabilities.Add(pair[0], pair[1]) + } + + f.packfile = init.packfile + f.idxfile = init.idxfile + + s.fixtures[init.name] = f + } +} + +func (s *SuiteDotGit) TearDownSuite(c *C) { + for n, f := range s.fixtures { + err := os.RemoveAll(f.installDir) + c.Assert(err, IsNil, Commentf("cannot delete tmp dir for fixture %s: %s\n", + n, f.installDir)) + } +} + +func (s *SuiteDotGit) TestNewErrors(c *C) { + for i, test := range [...]struct { + input string + err error + }{ + { + input: "./tmp/foo", + err: ErrNotFound, + }, { + input: "./tmp/foo/.git", + err: ErrNotFound, + }, + } { + com := Commentf("subtest %d", i) + + _, err := New(fs.NewOS(), test.input) + c.Assert(err, Equals, test.err, com) + } +} + +func (s *SuiteDotGit) TestRefsFromPackedRefs(c *C) { + _, d := s.newFixtureDir(c, "spinnaker") + refs, err := d.Refs() + c.Assert(err, IsNil) + + ref := findReference(refs, "refs/tags/v0.37.0") + c.Assert(ref, NotNil) + c.Assert(ref.Hash().String(), Equals, "85ec60477681933961c9b64c18ada93220650ac5") + +} +func (s *SuiteDotGit) TestRefsFromReferenceFile(c *C) { + _, d := s.newFixtureDir(c, "spinnaker") + refs, err := d.Refs() + c.Assert(err, IsNil) + + ref := findReference(refs, "refs/remotes/origin/HEAD") + c.Assert(ref, NotNil) + c.Assert(ref.Type(), Equals, core.SymbolicReference) + c.Assert(string(ref.Target()), Equals, "refs/remotes/origin/master") + +} + +func (s *SuiteDotGit) TestRefsFromHEADFile(c *C) { + _, d := s.newFixtureDir(c, "spinnaker") + refs, err := d.Refs() + c.Assert(err, IsNil) + + ref := findReference(refs, "HEAD") + c.Assert(ref, NotNil) + c.Assert(ref.Type(), Equals, core.SymbolicReference) + c.Assert(string(ref.Target()), Equals, "refs/heads/master") +} + +func findReference(refs []*core.Reference, name string) *core.Reference { + n := core.ReferenceName(name) + for _, ref := range refs { + if ref.Name() == n { + return ref + } + } + + return nil +} + +func (s *SuiteDotGit) newFixtureDir(c *C, fixName string) (*fixture, *DotGit) { + f, ok := s.fixtures[fixName] + c.Assert(ok, Equals, true) + + d, err := New(fs.NewOS(), f.path) + c.Assert(err, IsNil) + + return &f, d +} + +func (s *SuiteDotGit) TestPackfile(c *C) { + packfile := func(d *DotGit) (fs.FS, string, error) { + return d.Packfile() + } + idxfile := func(d *DotGit) (fs.FS, string, error) { + return d.Idxfile() + } + for _, test := range [...]struct { + fixture string + fn getPathFn + err string // error regexp + }{ + { + fixture: "spinnaker", + fn: packfile, + }, { + fixture: "spinnaker", + fn: idxfile, + }, { + fixture: "empty", + fn: packfile, + err: ".* no such file or directory", + }, { + fixture: "empty", + fn: idxfile, + err: ".* no such file or directory", + }, { + fixture: "no-packfile-no-idx", + fn: packfile, + err: "packfile not found", + }, { + fixture: "no-packfile-no-idx", + fn: idxfile, + err: "idx file not found", + }, + } { + com := Commentf("fixture = %s", test.fixture) + + fix, dir := s.newFixtureDir(c, test.fixture) + + _, path, err := test.fn(dir) + + if test.err != "" { + c.Assert(err, ErrorMatches, test.err, com) + } else { + c.Assert(err, IsNil, com) + c.Assert(strings.HasSuffix(noExt(path), noExt(fix.packfile)), + Equals, true, com) + } + } +} + +type getPathFn func(*DotGit) (fs.FS, string, error) + +func noExt(path string) string { + ext := filepath.Ext(path) + return path[0 : len(path)-len(ext)] +} diff --git a/storage/filesystem/internal/dotgit/refs.go b/storage/filesystem/internal/dotgit/refs.go new file mode 100644 index 0000000..894732f --- /dev/null +++ b/storage/filesystem/internal/dotgit/refs.go @@ -0,0 +1,144 @@ +package dotgit + +import ( + "bufio" + "errors" + "io/ioutil" + "os" + "strings" + + "gopkg.in/src-d/go-git.v4/core" +) + +var ( + // ErrPackedRefsDuplicatedRef is returned when a duplicated + // reference is found in the packed-ref file. This is usually the + // case for corrupted git repositories. + ErrPackedRefsDuplicatedRef = errors.New("duplicated ref found in packed-ref file") + // ErrPackedRefsBadFormat is returned when the packed-ref file + // corrupt. + ErrPackedRefsBadFormat = errors.New("malformed packed-ref") + // ErrSymRefTargetNotFound is returned when a symbolic reference is + // targeting a non-existing object. This usually means the + // repository is corrupt. + ErrSymRefTargetNotFound = errors.New("symbolic reference target not found") +) + +const ( + refsPath = "refs" +) + +func (d *DotGit) addRefsFromPackedRefs(refs *[]*core.Reference) (err error) { + path := d.fs.Join(d.path, packedRefsPath) + f, err := d.fs.Open(path) + if err != nil { + if err == os.ErrNotExist { + return nil + } + + return err + } + + defer func() { + if errClose := f.Close(); err == nil { + err = errClose + } + }() + + s := bufio.NewScanner(f) + for s.Scan() { + ref, err := d.processLine(s.Text()) + if err != nil { + return err + } + + if ref != nil { + *refs = append(*refs, ref) + } + } + + return s.Err() +} + +// process lines from a packed-refs file +func (d *DotGit) processLine(line string) (*core.Reference, error) { + switch line[0] { + case '#': // comment - ignore + return nil, nil + case '^': // annotated tag commit of the previous line - ignore + return nil, nil + default: + ws := strings.Split(line, " ") // hash then ref + if len(ws) != 2 { + return nil, ErrPackedRefsBadFormat + } + + return core.NewReferenceFromStrings(ws[1], ws[0]), nil + } +} + +func (d *DotGit) addRefsFromRefDir(refs *[]*core.Reference) error { + return d.walkReferencesTree(refs, refsPath) +} + +func (d *DotGit) walkReferencesTree(refs *[]*core.Reference, relPath string) error { + files, err := d.fs.ReadDir(d.fs.Join(d.path, relPath)) + if err != nil { + return err + } + + for _, f := range files { + newRelPath := d.fs.Join(relPath, f.Name()) + if f.IsDir() { + if err = d.walkReferencesTree(refs, newRelPath); err != nil { + return err + } + + continue + } + + ref, err := d.readReferenceFile(d.path, newRelPath) + if err != nil { + return err + } + + if ref != nil { + *refs = append(*refs, ref) + } + } + + return nil +} + +func (d *DotGit) addRefFromHEAD(refs *[]*core.Reference) error { + ref, err := d.readReferenceFile(d.path, "HEAD") + if err != nil { + return err + } + + *refs = append(*refs, ref) + return nil +} + +func (d *DotGit) readReferenceFile(refsPath, refFile string) (ref *core.Reference, err error) { + path := d.fs.Join(refsPath, refFile) + + f, err := d.fs.Open(path) + if err != nil { + return nil, err + } + + defer func() { + if errClose := f.Close(); err == nil { + err = errClose + } + }() + + b, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + + line := strings.TrimSpace(string(b)) + return core.NewReferenceFromStrings(refFile, line), nil +} |