From ef2fdd51edd1f39b23e2d3222dfeffbd5e448d91 Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Wed, 2 Jul 2014 15:36:06 -0300 Subject: [PATCH] Added ls-tree api support to gandalf. --- api/handler.go | 40 +++++++++++- api/handler_test.go | 138 +++++++++++++++++++++++++++++++++++++-- docs/source/api.rst | 52 ++++++++++++++- repository/mocks.go | 16 +++++ repository/repository.go | 48 ++++++++++++++ webserver/main.go | 4 +- 6 files changed, 290 insertions(+), 8 deletions(-) diff --git a/api/handler.go b/api/handler.go index a3b5857..bc602b9 100644 --- a/api/handler.go +++ b/api/handler.go @@ -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" @@ -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) +} diff --git a/api/handler_test.go b/api/handler_test.go index 6ce7051..62e742b 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -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), @@ -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), @@ -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), @@ -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, @@ -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() @@ -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") +} diff --git a/docs/source/api.rst b/docs/source/api.rst index 87396ab..7914c30 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -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: @@ -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 ----------- @@ -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 diff --git a/repository/mocks.go b/repository/mocks.go index bcdf9f3..4863fb4 100644 --- a/repository/mocks.go +++ b/repository/mocks.go @@ -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 } @@ -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 +} diff --git a/repository/repository.go b/repository/repository.go index 9c9644e..7aeaa10 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -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 @@ -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 @@ -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{} @@ -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) +} diff --git a/webserver/main.go b/webserver/main.go index 0c77cf6..ff4aa7e 100644 --- a/webserver/main.go +++ b/webserver/main.go @@ -46,7 +46,9 @@ For an example conf check gandalf/etc/gandalf.conf file.\n %s` router.Get("/repository/:name", http.HandlerFunc(api.GetRepository)) router.Put("/repository/:name", http.HandlerFunc(api.RenameRepository)) router.Get("/repository/:name/archive/:ref.:format", http.HandlerFunc(api.GetArchive)) - router.Get("/repository/:name/contents/:path", http.HandlerFunc(api.GetFileContents)) + router.Get("/repository/:name/contents", http.HandlerFunc(api.GetFileContents)) + router.Get("/repository/:name/tree/:path", http.HandlerFunc(api.GetTree)) + router.Get("/repository/:name/tree", http.HandlerFunc(api.GetTree)) router.Get("/healthcheck/", http.HandlerFunc(api.HealthCheck)) router.Post("/hook/:name", http.HandlerFunc(api.AddHook))