diff --git a/api/handler.go b/api/handler.go index b7c4ab9..f4084a1 100644 --- a/api/handler.go +++ b/api/handler.go @@ -370,7 +370,7 @@ func GetTree(w http.ResponseWriter, r *http.Request) { 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) + http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(b) @@ -392,7 +392,7 @@ func GetBranches(w http.ResponseWriter, r *http.Request) { b, err := json.Marshal(branches) if err != nil { err := fmt.Errorf("Error when trying to obtain the branches of repository %s (%s).", repo, err) - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(b) @@ -415,7 +415,7 @@ func GetTags(w http.ResponseWriter, r *http.Request) { b, err := json.Marshal(tags) if err != nil { err := fmt.Errorf("Error when trying to obtain tags on ref %s of repository %s (%s).", ref, repo, err) - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(b) @@ -491,8 +491,37 @@ func Commit(w http.ResponseWriter, r *http.Request) { } b, err := json.Marshal(ref) if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(b) +} + +func GetLog(w http.ResponseWriter, r *http.Request) { + repo := r.URL.Query().Get(":name") + ref := r.URL.Query().Get("ref") + total, err := strconv.Atoi(r.URL.Query().Get("total")) + if err != nil { + err := fmt.Errorf("Error when trying to obtain log for ref %s of repository %s (%s).", ref, repo, err) http.Error(w, err.Error(), http.StatusBadRequest) return } + if repo == "" { + err := fmt.Errorf("Error when trying to obtain log for ref %s of repository %s (repository is required).", ref, repo) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + logs, err := repository.GetLog(repo, ref, total) + if err != nil { + err := fmt.Errorf("Error when trying to obtain log for ref %s of repository %s (%s).", ref, repo, err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + b, err := json.Marshal(logs) + if err != nil { + err := fmt.Errorf("Error when trying to obtain log for ref %s of repository %s (%s).", ref, repo, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } w.Write(b) } diff --git a/api/handler_test.go b/api/handler_test.go index 4bf2d57..c32b180 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -1429,3 +1429,45 @@ func (s *S) TestPostNewCommitWithEmptyBranch(c *gocheck.C) { Commit(recorder, request) c.Assert(recorder.Code, gocheck.Equals, http.StatusBadRequest) } + +func (s *S) TestLog(c *gocheck.C) { + url := "/repository/repo/logs?&:name=repo&ref=HEAD&total=1" + objects := repository.GitHistory{} + parent := make([]string, 2) + parent[0] = "a367b5de5943632e47cb6f8bf5b2147bc0be5cf8" + parent[1] = "b267b5de5943632e47cb6f8bf5b2147bc0be5cf2" + commits := make([]repository.GitLog, 1) + commits[0] = repository.GitLog{ + Ref: "a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9", + CreatedAt: "Mon Jul 28 10:13:27 2014 -0300", + Committer: &repository.GitUser{ + Name: "doge", + Email: "much@email.com", + }, + Author: &repository.GitUser{ + Name: "doge", + Email: "much@email.com", + }, + Subject: "will bark", + Parent: parent, + } + objects.Commits = commits + objects.Next = "b231c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9" + mockRetriever := repository.MockContentRetriever{ + History: objects, + } + repository.Retriever = &mockRetriever + defer func() { + repository.Retriever = nil + }() + request, err := http.NewRequest("GET", url, nil) + c.Assert(err, gocheck.IsNil) + recorder := httptest.NewRecorder() + GetLog(recorder, request) + c.Assert(recorder.Code, gocheck.Equals, http.StatusOK) + var obj repository.GitHistory + json.Unmarshal(recorder.Body.Bytes(), &obj) + c.Assert(obj.Next, gocheck.Equals, "b231c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9") + c.Assert(obj.Commits, gocheck.HasLen, 1) + c.Assert(obj.Commits[0], gocheck.DeepEquals, commits[0]) +} diff --git a/docs/source/api.rst b/docs/source/api.rst index f1d28cd..e63ee3c 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -297,3 +297,46 @@ Example result:: zipArchive: "/repository/myrepository/archive?ref=master&format=zip", } } + +Logs +---- + +Returns a list of all commits into `repository`. + +* Method: GET +* URI: /repository/`:name`/log?ref=:ref&total=:total +* Format: JSON + +Where: + +* `:name` is the name of the repository; +* `:ref` is the repository ref (commit, tag or branch); +* `:total` is the maximum number of items to retrieve + +Example URL (http://gandalf-server omitted for clarity):: + + $ curl /repository/myrepository/logs?ref=HEAD&total=1 + +Example result:: + + { + commits: [{ + ref: "6767b5de5943632e47cb6f8bf5b2147bc0be5cf8", + subject: "much WOW", + createdAt: "Mon Jul 28 10:13:27 2014 -0300" + author: { + name: "Author name", + email: "author@email.com", + date: "Mon Jul 28 10:13:27 2014 -0300"" + }, + committer: { + name: "Committer name", + email: "committer@email.com", + date: "Tue Jul 29 13:43:57 2014 -0300" + }, + parent: [ + "a367b5de5943632e47cb6f8bf5b2147bc0be5cf8" + ] + }], + next: "1267b5de5943632e47cb6f8bf5b2147bc0be5cf123" + } diff --git a/repository/mocks.go b/repository/mocks.go index 8f62ba9..dc995d2 100644 --- a/repository/mocks.go +++ b/repository/mocks.go @@ -26,6 +26,7 @@ type MockContentRetriever struct { OutputError error ClonePath string CleanUp func() + History GitHistory } func (r *MockContentRetriever) GetContents(repo, ref, path string) ([]byte, error) { @@ -395,3 +396,13 @@ func (r *MockContentRetriever) CommitZip(repo string, z *multipart.FileHeader, c } return &r.Ref, nil } + +func (r *MockContentRetriever) GetLog(repo, hash string, total int) (*GitHistory, error) { + if r.LookPathError != nil { + return nil, r.LookPathError + } + if r.OutputError != nil { + return nil, r.OutputError + } + return &r.History, nil +} diff --git a/repository/repository.go b/repository/repository.go index 370d17b..9c62e41 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -72,6 +72,20 @@ type Ref struct { CreatedAt string `json:"createdAt"` } +type GitLog struct { + Ref string `json:"ref"` + Author *GitUser `json:"author"` + Committer *GitUser `json:"committer"` + Subject string `json:"subject"` + CreatedAt string `json:"createdAt"` + Parent []string `json:"parent"` +} + +type GitHistory struct { + Commits []GitLog `json:"commits"` + Next string `json:"next"` +} + // exists returns whether the given file or directory exists or not func exists(path string) (bool, error) { _, err := os.Stat(path) @@ -308,6 +322,7 @@ type ContentRetriever interface { Commit(cloneDir, message string, author GitUser) error Push(cloneDir, branch string) error CommitZip(repo string, z *multipart.FileHeader, c GitCommit) (*Ref, error) + GetLog(repo, hash string, total int) (*GitHistory, error) } var Retriever ContentRetriever @@ -680,6 +695,95 @@ func (*GitContentRetriever) CommitZip(repo string, z *multipart.FileHeader, c Gi return nil, fmt.Errorf("Error when trying to commit zip to repository %s, could not check branch: %s", repo, err) } +func (*GitContentRetriever) GetLog(repo, hash string, total int) (*GitHistory, error) { + if total < 1 { + total = 1 + } + totalPagination := total + 1 + var last, ref, committerName, committerEmail, committerDate, authorName, authorEmail, authorDate, subject, parent string + gitPath, err := exec.LookPath("git") + if err != nil { + return nil, fmt.Errorf("Error when trying to obtain the log of repository %s (%s).", repo, err) + } + cwd := barePath(repo) + repoExists, err := exists(cwd) + if err != nil || !repoExists { + return nil, fmt.Errorf("Error when trying to obtain the log of repository %s (Repository does not exist).", repo) + } + format := "%H%x09%an%x09%ae%x09%ad%x09%cn%x09%ce%x09%cd%x09%P%x09%s" + cmd := exec.Command(gitPath, "--no-pager", "log", fmt.Sprintf("-n %d", totalPagination), fmt.Sprintf("--format=%s", format), hash) + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("Error when trying to obtain the log of repository %s (%s).", repo, err) + } + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + objectCount := len(lines) + if len(lines) == 1 && len(lines[0]) == 0 { + objectCount = 0 + } + if objectCount > total { + last = lines[objectCount-1] + lines = lines[0 : objectCount-1] + objectCount -= 1 + } + history := GitHistory{} + commits := make([]GitLog, objectCount) + objectCount = 0 + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + fields := strings.Split(line, "\t") + if len(fields) > 8 { // let there be commits with empty subject + ref = fields[0] + authorName = fields[1] + authorEmail = fields[2] + authorDate = fields[3] + committerName = fields[4] + committerEmail = fields[5] + committerDate = fields[6] + parent = fields[7] + subject = strings.Join(fields[8:], "\t") // let there be subjects with \t + } else { + return nil, fmt.Errorf("Error when trying to obtain the log of repository %s (Invalid git log output [%s]).", repo, out) + } + commit := GitLog{} + commit.Ref = ref + commit.Subject = subject + commit.CreatedAt = authorDate + commit.Committer = &GitUser{ + Name: committerName, + Email: committerEmail, + Date: committerDate, + } + commit.Author = &GitUser{ + Name: authorName, + Email: authorEmail, + Date: authorDate, + } + parents := strings.Split(parent, " ") + parentCount := len(parents) + aux := make([]string, parentCount) + parentCount = 0 + for _, item := range parents { + aux[parentCount] = item + parentCount++ + } + commit.Parent = aux + commits[objectCount] = commit + objectCount++ + } + history.Commits = commits + if last != "" { + fields := strings.Split(last, "\t") + history.Next = fields[0] + } else { + history.Next = "" + } + return &history, nil +} + func retriever() ContentRetriever { if Retriever == nil { Retriever = &GitContentRetriever{} @@ -746,3 +850,7 @@ func Push(cloneDir, branch string) error { func CommitZip(repo string, z *multipart.FileHeader, c GitCommit) (*Ref, error) { return retriever().CommitZip(repo, z, c) } + +func GetLog(repo, hash string, total int) (*GitHistory, error) { + return retriever().GetLog(repo, hash, total) +} diff --git a/repository/repository_test.go b/repository/repository_test.go index 4bce1ed..f7e926d 100644 --- a/repository/repository_test.go +++ b/repository/repository_test.go @@ -1726,3 +1726,64 @@ func (s *S) TestCommitZipIntegrationWhenFileEmpty(c *gocheck.C) { _, err = CommitZip(repo, file, commit) c.Assert(err.Error(), gocheck.Equals, expectedErr) } + +func (s *S) TestGetLog(c *gocheck.C) { + oldBare := bare + bare = "/tmp" + repo := "gandalf-test-repo" + file := "README" + content := "will\tbark" + object1 := "You should read this README" + object2 := "Seriously, read this file!" + cleanUp, errCreate := CreateTestRepository(bare, repo, file, content) + defer func() { + cleanUp() + bare = oldBare + }() + c.Assert(errCreate, gocheck.IsNil) + errCreateCommit := CreateCommit(bare, repo, file, object1) + c.Assert(errCreateCommit, gocheck.IsNil) + errCreateCommit = CreateCommit(bare, repo, file, object2) + c.Assert(errCreateCommit, gocheck.IsNil) + history, err := GetLog(repo, "HEAD", 1) + c.Assert(err, gocheck.IsNil) + c.Assert(history.Commits, gocheck.HasLen, 1) + c.Assert(history.Commits[0].Ref, gocheck.Matches, "[a-f0-9]{40}") + c.Assert(history.Commits[0].Parent, gocheck.HasLen, 1) + c.Assert(history.Commits[0].Parent[0], gocheck.Matches, "[a-f0-9]{40}") + c.Assert(history.Commits[0].Committer.Name, gocheck.Equals, "doge") + c.Assert(history.Commits[0].Committer.Email, gocheck.Equals, "much@email.com") + c.Assert(history.Commits[0].Author.Name, gocheck.Equals, "doge") + c.Assert(history.Commits[0].Author.Email, gocheck.Equals, "much@email.com") + c.Assert(history.Commits[0].Subject, gocheck.Equals, "Seriously, read this file!") + c.Assert(history.Commits[0].CreatedAt, gocheck.Equals, history.Commits[0].Author.Date) + c.Assert(history.Next, gocheck.Matches, "[a-f0-9]{40}") + // Next + history, err = GetLog(repo, history.Next, 1) + c.Assert(err, gocheck.IsNil) + c.Assert(history.Commits, gocheck.HasLen, 1) + c.Assert(history.Commits[0].Ref, gocheck.Matches, "[a-f0-9]{40}") + c.Assert(history.Commits[0].Parent, gocheck.HasLen, 1) + c.Assert(history.Commits[0].Parent[0], gocheck.Matches, "[a-f0-9]{40}") + c.Assert(history.Commits[0].Committer.Name, gocheck.Equals, "doge") + c.Assert(history.Commits[0].Committer.Email, gocheck.Equals, "much@email.com") + c.Assert(history.Commits[0].Author.Name, gocheck.Equals, "doge") + c.Assert(history.Commits[0].Author.Email, gocheck.Equals, "much@email.com") + c.Assert(history.Commits[0].Subject, gocheck.Equals, "You should read this README") + c.Assert(history.Commits[0].CreatedAt, gocheck.Equals, history.Commits[0].Author.Date) + c.Assert(history.Next, gocheck.Matches, "[a-f0-9]{40}") + // Next + history, err = GetLog(repo, history.Next, 1) + c.Assert(err, gocheck.IsNil) + c.Assert(history.Commits, gocheck.HasLen, 1) + c.Assert(history.Commits[0].Ref, gocheck.Matches, "[a-f0-9]{40}") + c.Assert(history.Commits[0].Parent, gocheck.HasLen, 1) + c.Assert(history.Commits[0].Parent[0], gocheck.Equals, "") + c.Assert(history.Commits[0].Committer.Name, gocheck.Equals, "doge") + c.Assert(history.Commits[0].Committer.Email, gocheck.Equals, "much@email.com") + c.Assert(history.Commits[0].Author.Name, gocheck.Equals, "doge") + c.Assert(history.Commits[0].Author.Email, gocheck.Equals, "much@email.com") + c.Assert(history.Commits[0].Subject, gocheck.Equals, "will\tbark") + c.Assert(history.Commits[0].CreatedAt, gocheck.Equals, history.Commits[0].Author.Date) + c.Assert(history.Next, gocheck.Equals, "") +} diff --git a/webserver/main.go b/webserver/main.go index 095a24b..bd84a61 100644 --- a/webserver/main.go +++ b/webserver/main.go @@ -53,6 +53,7 @@ For an example conf check gandalf/etc/gandalf.conf file.\n %s` router.Get("/repository/:name/tags", http.HandlerFunc(api.GetTags)) router.Get("/repository/:name/diff/commits", http.HandlerFunc(api.GetDiff)) router.Post("/repository/:name/commit", http.HandlerFunc(api.Commit)) + router.Get("/repository/:name/logs", http.HandlerFunc(api.GetLog)) router.Get("/healthcheck/", http.HandlerFunc(api.HealthCheck)) router.Post("/hook/:name", http.HandlerFunc(api.AddHook))