aboutsummaryrefslogtreecommitdiffstats
path: root/storage/filesystem/internal/dotgit
diff options
context:
space:
mode:
Diffstat (limited to 'storage/filesystem/internal/dotgit')
-rw-r--r--storage/filesystem/internal/dotgit/dotgit.go105
-rw-r--r--storage/filesystem/internal/dotgit/dotgit_test.go224
-rw-r--r--storage/filesystem/internal/dotgit/refs.go144
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
+}