Skip to content
This repository has been archived by the owner on Apr 26, 2021. It is now read-only.

Commit

Permalink
Added ls-tree api support to gandalf.
Browse files Browse the repository at this point in the history
  • Loading branch information
heynemann committed Jul 2, 2014
1 parent dfff88d commit ef2fdd5
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 8 deletions.
40 changes: 39 additions & 1 deletion api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func HealthCheck(w http.ResponseWriter, r *http.Request) {

func GetFileContents(w http.ResponseWriter, r *http.Request) {
repo := r.URL.Query().Get(":name")
path := r.URL.Query().Get(":path")
path := r.URL.Query().Get("path")
ref := r.URL.Query().Get("ref")
if ref == "" {
ref = "master"
Expand Down Expand Up @@ -303,3 +303,41 @@ func GetArchive(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Expires", "Mon, 26 Jul 1997 05:00:00 GMT")
w.Write(contents)
}

func GetTree(w http.ResponseWriter, r *http.Request) {
repo := r.URL.Query().Get(":name")
path := r.URL.Query().Get("path")
ref := r.URL.Query().Get("ref")

if ref == "" {
ref = "master"
}

if path == "" {
path = "."
}

if repo == "" {
err := fmt.Errorf("Error when trying to obtain tree for path %s on ref %s of repository %s (repository is required).", path, ref, repo)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

tree, err := repository.GetTree(repo, ref, path)

if err != nil {
err := fmt.Errorf("Error when trying to obtain tree for path %s on ref %s of repository %s (%s).", path, ref, repo, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

b, err := json.Marshal(tree)

if err != nil {
err := fmt.Errorf("Error when trying to obtain tree for path %s on ref %s of repository %s (%s).", path, ref, repo, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

w.Write(b)
}
138 changes: 133 additions & 5 deletions api/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ func (s *S) TestHealthcheck(c *gocheck.C) {
}

func (s *S) TestGetFileContents(c *gocheck.C) {
url := "/repository/repo/contents/README.txt?:name=repo&:path=README.txt"
url := "/repository/repo/contents?:name=repo&path=README.txt"
expected := "result"
repository.Retriever = &repository.MockContentRetriever{
ResultContents: []byte(expected),
Expand All @@ -699,7 +699,7 @@ func (s *S) TestGetFileContents(c *gocheck.C) {
}

func (s *S) TestGetFileContentsWithoutExtension(c *gocheck.C) {
url := "/repository/repo/contents/README?:name=repo&:path=README"
url := "/repository/repo/contents?:name=repo&path=README"
expected := "result"
repository.Retriever = &repository.MockContentRetriever{
ResultContents: []byte(expected),
Expand All @@ -718,7 +718,7 @@ func (s *S) TestGetFileContentsWithoutExtension(c *gocheck.C) {
}

func (s *S) TestGetFileContentsWithRef(c *gocheck.C) {
url := "/repository/repo/contents/README?:name=repo&:path=README.txt&ref=other"
url := "/repository/repo/contents?:name=repo&path=README.txt&ref=other"
expected := "result"
mockRetriever := repository.MockContentRetriever{
ResultContents: []byte(expected),
Expand All @@ -739,7 +739,7 @@ func (s *S) TestGetFileContentsWithRef(c *gocheck.C) {
}

func (s *S) TestGetFileContentsWhenCommandFails(c *gocheck.C) {
url := "/repository/repo/contents/README?:name=repo&:path=README.txt&ref=other"
url := "/repository/repo/contents?:name=repo&path=README.txt&ref=other"
outputError := fmt.Errorf("command error")
repository.Retriever = &repository.MockContentRetriever{
OutputError: outputError,
Expand All @@ -756,7 +756,7 @@ func (s *S) TestGetFileContentsWhenCommandFails(c *gocheck.C) {
}

func (s *S) TestGetFileContentsWhenNoRepository(c *gocheck.C) {
url := "/repository//contents/README?:name=&:path=README.txt&ref=other"
url := "/repository//contents?:name=&path=README.txt&ref=other"
request, err := http.NewRequest("GET", url, nil)
c.Assert(err, gocheck.IsNil)
recorder := httptest.NewRecorder()
Expand Down Expand Up @@ -843,3 +843,131 @@ func (s *S) TestGetArchive(c *gocheck.C) {
c.Assert(recorder.Header()["Pragma"][0], gocheck.Equals, "private")
c.Assert(recorder.Header()["Expires"][0], gocheck.Equals, "Mon, 26 Jul 1997 05:00:00 GMT")
}

func (s *S) TestGetTreeWithDefaultValues(c *gocheck.C) {
url := "/repository/repo/tree?:name=repo"
tree := make([]map[string]string, 1)
tree[0] = make(map[string]string)
tree[0]["permission"] = "333"
tree[0]["filetype"] = "blob"
tree[0]["hash"] = "123456"
tree[0]["path"] = "filename.txt"
tree[0]["rawPath"] = "raw/filename.txt"
mockRetriever := repository.MockContentRetriever{
Tree: tree,
}
repository.Retriever = &mockRetriever
defer func() {
repository.Retriever = nil
}()
request, err := http.NewRequest("GET", url, nil)
c.Assert(err, gocheck.IsNil)
recorder := httptest.NewRecorder()
GetTree(recorder, request)
c.Assert(recorder.Code, gocheck.Equals, http.StatusOK)
var obj []map[string]string
json.Unmarshal([]byte(recorder.Body.String()), &obj)
c.Assert(len(obj), gocheck.Equals, 1)
c.Assert(obj[0]["permission"], gocheck.Equals, tree[0]["permission"])
c.Assert(obj[0]["filetype"], gocheck.Equals, tree[0]["filetype"])
c.Assert(obj[0]["hash"], gocheck.Equals, tree[0]["hash"])
c.Assert(obj[0]["path"], gocheck.Equals, tree[0]["path"])
c.Assert(obj[0]["rawPath"], gocheck.Equals, tree[0]["rawPath"])
c.Assert(mockRetriever.LastRef, gocheck.Equals, "master")
c.Assert(mockRetriever.LastPath, gocheck.Equals, ".")
}

func (s *S) TestGetTreeWithSpecificPath(c *gocheck.C) {
url := "/repository/repo/tree?:name=repo&path=/test"
tree := make([]map[string]string, 1)
tree[0] = make(map[string]string)
tree[0]["permission"] = "333"
tree[0]["filetype"] = "blob"
tree[0]["hash"] = "123456"
tree[0]["path"] = "/test/filename.txt"
tree[0]["rawPath"] = "/test/raw/filename.txt"
mockRetriever := repository.MockContentRetriever{
Tree: tree,
}
repository.Retriever = &mockRetriever
defer func() {
repository.Retriever = nil
}()
request, err := http.NewRequest("GET", url, nil)
c.Assert(err, gocheck.IsNil)
recorder := httptest.NewRecorder()
GetTree(recorder, request)
c.Assert(recorder.Code, gocheck.Equals, http.StatusOK)
var obj []map[string]string
json.Unmarshal([]byte(recorder.Body.String()), &obj)
c.Assert(len(obj), gocheck.Equals, 1)
c.Assert(obj[0]["permission"], gocheck.Equals, tree[0]["permission"])
c.Assert(obj[0]["filetype"], gocheck.Equals, tree[0]["filetype"])
c.Assert(obj[0]["hash"], gocheck.Equals, tree[0]["hash"])
c.Assert(obj[0]["path"], gocheck.Equals, tree[0]["path"])
c.Assert(obj[0]["rawPath"], gocheck.Equals, tree[0]["rawPath"])
c.Assert(mockRetriever.LastRef, gocheck.Equals, "master")
c.Assert(mockRetriever.LastPath, gocheck.Equals, "/test")
}

func (s *S) TestGetTreeWithSpecificRef(c *gocheck.C) {
url := "/repository/repo/tree?:name=repo&path=/test&ref=1.1.1"
tree := make([]map[string]string, 1)
tree[0] = make(map[string]string)
tree[0]["permission"] = "333"
tree[0]["filetype"] = "blob"
tree[0]["hash"] = "123456"
tree[0]["path"] = "/test/filename.txt"
tree[0]["rawPath"] = "/test/raw/filename.txt"
mockRetriever := repository.MockContentRetriever{
Tree: tree,
}
repository.Retriever = &mockRetriever
defer func() {
repository.Retriever = nil
}()
request, err := http.NewRequest("GET", url, nil)
c.Assert(err, gocheck.IsNil)
recorder := httptest.NewRecorder()
GetTree(recorder, request)
c.Assert(recorder.Code, gocheck.Equals, http.StatusOK)
var obj []map[string]string
json.Unmarshal([]byte(recorder.Body.String()), &obj)
c.Assert(len(obj), gocheck.Equals, 1)
c.Assert(obj[0]["permission"], gocheck.Equals, tree[0]["permission"])
c.Assert(obj[0]["filetype"], gocheck.Equals, tree[0]["filetype"])
c.Assert(obj[0]["hash"], gocheck.Equals, tree[0]["hash"])
c.Assert(obj[0]["path"], gocheck.Equals, tree[0]["path"])
c.Assert(obj[0]["rawPath"], gocheck.Equals, tree[0]["rawPath"])
c.Assert(mockRetriever.LastRef, gocheck.Equals, "1.1.1")
c.Assert(mockRetriever.LastPath, gocheck.Equals, "/test")
}

func (s *S) TestGetTreeWhenNoRepo(c *gocheck.C) {
url := "/repository//tree?:name="
request, err := http.NewRequest("GET", url, nil)
c.Assert(err, gocheck.IsNil)
recorder := httptest.NewRecorder()
GetTree(recorder, request)
c.Assert(recorder.Code, gocheck.Equals, http.StatusBadRequest)
expected := "Error when trying to obtain tree for path . on ref master of repository (repository is required).\n"
c.Assert(recorder.Body.String(), gocheck.Equals, expected)
}

func (s *S) TestGetTreeWhenCommandFails(c *gocheck.C) {
url := "/repository/repo/tree/?:name=repo&ref=master&path=/test"
expected := fmt.Errorf("output error")
mockRetriever := repository.MockContentRetriever{
OutputError: expected,
}
repository.Retriever = &mockRetriever
defer func() {
repository.Retriever = nil
}()
request, err := http.NewRequest("GET", url, nil)
c.Assert(err, gocheck.IsNil)
recorder := httptest.NewRecorder()
GetTree(recorder, request)
c.Assert(recorder.Code, gocheck.Equals, http.StatusBadRequest)
c.Assert(recorder.Body.String(), gocheck.Equals, "Error when trying to obtain tree for path /test on ref master of repository repo (output error).\n")
}
52 changes: 51 additions & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Get file contents
Returns the contents for a `path` in the specified `repository` with the given `ref` (commit, tag or branch).

* Method: GET
* URI: /repository/`:name`/contents/`:path`?ref=:ref
* URI: /repository/`:name`/contents?ref=:ref&path=:path
* Format: binary

Where:
Expand All @@ -65,6 +65,50 @@ Where:
* `:path` is the file path in the repository file system;
* `:ref` is the repository ref (commit, tag or branch). **This is optional**. If not passed this is assumed to be "master".

Example URLs (http://gandalf-server omitted for clarity)::

$ curl /repository/myrepository/contents?ref=0.1.0&path=/some/path/in/the/repo.txt
$ curl /repository/myrepository/contents?path=/some/path/in/the/repo.txt # gets master

Get tree
--------

Returns a list of all the files under a `path` in the specified `repository` with the given `ref` (commit, tag or branch).

* Method: GET
* URI: /repository/`:name`/tree?ref=:ref&path=:path
* Format: JSON

Where:

* `:name` is the name of the repository;
* `:path` is the file path in the repository file system. **This is optional**. If not passed this is assumed to be ".";
* `:ref` is the repository ref (commit, tag or branch). **This is optional**. If not passed this is assumed to be "master".

Example result::

[{
filetype: "blob",
hash: "6767b5de5943632e47cb6f8bf5b2147bc0be5cf8",
path: ".gitignore",
permission: "100644",
rawPath: ".gitignore"
}, {
filetype: "blob",
hash: "fbd8b6db62282a8402a4fc5503e9a886b4fb8b4b",
path: ".travis.yml",
permission: "100644",
rawPath: ".travis.yml"
}]

`rawPath` contains exactly the value returned from git (with escaped characters, quotes, etc), while `path` is somewhat cleaner (spaces removed, quotes removed from the left and right).

Example URLs (http://gandalf-server omitted for clarity)::

$ curl /repository/myrepository/tree # gets master and root path(.)
$ curl /repository/myrepository/tree?ref=0.1.0 # gets 0.1.0 tag and root path(.)
$ curl /repository/myrepository/tree?ref=0.1.0&path=/myrepository # gets 0.1.0 tag and files under /myrepository

Get archive
-----------

Expand All @@ -79,3 +123,9 @@ Where:
* `:name` is the name of the repository;
* `:ref` is the repository ref (commit, tag or branch);
* `:format` is the format to return the archive. This can be zip, tar or tar.gz.

Example URLs (http://gandalf-server omitted for clarity)::

$ curl /repository/myrepository/archive/master.zip # gets master and zip format
$ curl /repository/myrepository/archive/master.tar.gz # gets master and tar.gz format
$ curl /repository/myrepository/archive/0.1.0.zip # gets 0.1.0 tag and zip format
16 changes: 16 additions & 0 deletions repository/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ package repository
type MockContentRetriever struct {
LastFormat ArchiveFormat
LastRef string
LastPath string
ResultContents []byte
Tree []map[string]string
LookPathError error
OutputError error
}
Expand Down Expand Up @@ -38,3 +40,17 @@ func (r *MockContentRetriever) GetArchive(repo, ref string, format ArchiveFormat
r.LastFormat = format
return r.ResultContents, nil
}

func (r *MockContentRetriever) GetTree(repo, ref, path string) ([]map[string]string, error) {
if r.LookPathError != nil {
return nil, r.LookPathError
}

if r.OutputError != nil {
return nil, r.OutputError
}

r.LastRef = ref
r.LastPath = path
return r.Tree, nil
}
48 changes: 48 additions & 0 deletions repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"labix.org/v2/mgo/bson"
"os/exec"
"regexp"
"strings"
)

// Repository represents a Git repository. A Git repository is a record in the
Expand Down Expand Up @@ -231,6 +232,7 @@ const (
type ContentRetriever interface {
GetContents(repo, ref, path string) ([]byte, error)
GetArchive(repo, ref string, format ArchiveFormat) ([]byte, error)
GetTree(repo, ref, path string) ([]map[string]string, error)
}

var Retriever ContentRetriever
Expand Down Expand Up @@ -277,6 +279,48 @@ func (*GitContentRetriever) GetArchive(repo, ref string, format ArchiveFormat) (
return out, nil
}

func (*GitContentRetriever) GetTree(repo, ref, path string) ([]map[string]string, error) {
gitPath, err := exec.LookPath("git")
if err != nil {
return nil, fmt.Errorf("Error when trying to obtain file %s on ref %s of repository %s (%s).", path, ref, repo, err)
}
cwd := barePath(repo)
cmd := exec.Command(gitPath, "ls-tree", "-r", ref, path)
cmd.Dir = cwd
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("Error when trying to obtain tree %s on ref %s of repository %s (%s).", path, ref, repo, err)
}
lines := strings.Split(string(out), "\n")
objectCount := 0
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
objectCount++
}
objects := make([]map[string]string, len(lines)-1)
objectCount = 0
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
tabbed := strings.Split(line, "\t")
meta, filepath := tabbed[0], tabbed[1]
meta_parts := strings.Split(meta, " ")
permission, filetype, hash := meta_parts[0], meta_parts[1], meta_parts[2]
object := make(map[string]string)
object["permission"] = permission
object["filetype"] = filetype
object["hash"] = hash
object["path"] = strings.TrimSpace(strings.Trim(filepath, "\""))
object["rawPath"] = filepath
objects[objectCount] = object
objectCount++
}
return objects, nil
}

func retriever() ContentRetriever {
if Retriever == nil {
Retriever = &GitContentRetriever{}
Expand All @@ -295,3 +339,7 @@ func GetFileContents(repo, ref, path string) ([]byte, error) {
func GetArchive(repo, ref string, format ArchiveFormat) ([]byte, error) {
return retriever().GetArchive(repo, ref, format)
}

func GetTree(repo, ref, path string) ([]map[string]string, error) {
return retriever().GetTree(repo, ref, path)
}
Loading

0 comments on commit ef2fdd5

Please sign in to comment.