From b7839f2cd6fbf544bd67b1657b0afca2d97b4b62 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 22 Sep 2021 14:59:51 +0200 Subject: [PATCH] adds commits service to the stash client CommitsService permits retrieving commits and listing them for a given repository. Signed-off-by: Soule BA --- stash/client.go | 2 + stash/commits.go | 147 ++++++++++++++++++++++++++++++++++++++++++ stash/commits_test.go | 125 +++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 stash/commits.go create mode 100644 stash/commits_test.go diff --git a/stash/client.go b/stash/client.go index 59dc5043..13340014 100644 --- a/stash/client.go +++ b/stash/client.go @@ -110,6 +110,7 @@ type Client struct { Git Git Repositories Repositories Branches Branches + Commits Commits } // RateLimiter is the interface that wraps the basic Wait method. @@ -206,6 +207,7 @@ func NewClient(httpClient *http.Client, host string, header *http.Header, logger c.Git = &GitService{Client: c} c.Repositories = &RepositoriesService{Client: c} c.Branches = &BranchesService{Client: c} + c.Commits = &CommitsService{Client: c} return c, nil } diff --git a/stash/commits.go b/stash/commits.go new file mode 100644 index 00000000..fac02e55 --- /dev/null +++ b/stash/commits.go @@ -0,0 +1,147 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +const ( + commitsURI = "commits" +) + +// Commits interface defines the methods that can be used to +// retrieve commits of a repository. +type Commits interface { + List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*CommitList, error) + Get(ctx context.Context, projectKey, repositorySlug, commitID string) (*CommitObject, error) +} + +// CommitsService is a client for communicating with stash commits endpoint +// bitbucket-server API docs: https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +type CommitsService service + +// CommitObject represents a commit in stash +type CommitObject struct { + // Session is the session object for the branch. + Session `json:"sessionInfo,omitempty"` + // Author is the author of the commit. + Author User `json:"author,omitempty"` + // AuthorTimestamp is the timestamp of the author of the commit. + AuthorTimestamp int64 `json:"authorTimestamp,omitempty"` + // Committer is the committer of the commit. + Committer User `json:"committer,omitempty"` + // CommitterTimestamp is the timestamp of the committer of the commit. + CommitterTimestamp int64 `json:"committerTimestamp,omitempty"` + // DisplayID is the display ID of the commit. + DisplayID string `json:"displayId,omitempty"` + // ID is the ID of the commit i.e the SHA1. + ID string `json:"id,omitempty"` + // Message is the message of the commit. + Message string `json:"message,omitempty"` + // Parents is the list of parents of the commit. + Parents []*Parent `json:"parents,omitempty"` +} + +// Parent represents a parent of a commit. +type Parent struct { + // DisplayID is the display ID of the commit. + DisplayID string `json:"displayId,omitempty"` + // ID is the ID of the commit i.e the SHA1. + ID string `json:"id,omitempty"` +} + +// CommitList represents a list of commits in stash +type CommitList struct { + // Paging is the paging information. + Paging + // Commits is the list of commits. + Commits []*CommitObject `json:"values,omitempty"` +} + +// GetCommits returns the list of commits +func (c *CommitList) GetCommits() []*CommitObject { + return c.Commits +} + +// List returns the list of commits. +// Paging is optional and is enabled by providing a PagingOptions struct. +// A pointer to a CommitList struct is returned to retrieve the next page of results. +// List uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/commits". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *CommitsService) List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*CommitList, error) { + query := addPaging(&url.Values{}, opts) + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, commitsURI), *query, nil, nil) + if err != nil { + return nil, fmt.Errorf("list commits request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("list commits failed: %w", err) + } + + // As nothing is done with the response body, it is safe to close here + // to avoid leaking connections + defer resp.Body.Close() + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + c := &CommitList{} + if err := json.Unmarshal(res, c); err != nil { + return nil, fmt.Errorf("list commits for repository failed, unable to unmarshall repository json: %w", err) + } + + for _, commit := range c.GetCommits() { + commit.Session.set(resp) + } + return c, nil +} + +// Get retrieves a stash commit given it's ID i.e a SHA1. +// Get uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/commits/{commitID}". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *CommitsService) Get(ctx context.Context, projectKey, repositorySlug, commitID string) (*CommitObject, error) { + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, commitsURI, commitID), nil, nil, nil) + if err != nil { + return nil, fmt.Errorf("get commit request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("get commit failed: %w", err) + } + + // As nothing is done with the response body, it is safe to close here + // to avoid leaking connections + defer resp.Body.Close() + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + c := &CommitObject{} + if err := json.Unmarshal(res, c); err != nil { + return nil, fmt.Errorf("get commit failed, unable to unmarshall json: %w", err) + } + + c.Session.set(resp) + + return c, nil +} diff --git a/stash/commits_test.go b/stash/commits_test.go new file mode 100644 index 00000000..1fada27f --- /dev/null +++ b/stash/commits_test.go @@ -0,0 +1,125 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "path" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetCommit(t *testing.T) { + tests := []struct { + name string + commitID string + }{ + { + name: "test a commit", + commitID: "abcdef0123abcdef4567abcdef8987abcdef6543", + }, + { + name: "test commit does not exist", + commitID: "*°0#13jbkjfbvsqbùbjùrdfbgzeo'àtu)éuçt&-y", + }, + } + + validCommitID := []string{"abcdef0123abcdef4567abcdef8987abcdef6543"} + + mux, client := setup(t) + + fmt.Println("commit") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, commitsURI, tt.commitID) + mux.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) { + for _, substr := range validCommitID { + if path.Base(r.URL.String()) == substr { + w.WriteHeader(http.StatusOK) + c := &CommitObject{ + ID: substr, + DisplayID: substr[:10], + } + + json.NewEncoder(w).Encode(c) + return + } + } + + http.Error(w, "The specified commit does not exist", http.StatusNotFound) + + return + + }) + + ctx := context.Background() + c, err := client.Commits.Get(ctx, "prj1", "repo1", tt.commitID) + if err != nil { + if err != ErrNotFound { + t.Fatalf("Commits.Get returned error: %v", err) + } + return + } + + if c.ID != tt.commitID { + t.Fatalf("Commits.Get returned commit %s, want %s", c.ID, tt.commitID) + } + + }) + } +} + +func TestListCommits(t *testing.T) { + cIDs := []*CommitObject{ + {ID: "abcdef0123abcdef4567abcdef8987abcdef6543"}, + {ID: "aerfdef09893abcdef4567abcdef898abcdef652"}, + {ID: "abcdef3456abcdef4567abcdef8987abcdef6657"}, + {ID: "abcdef9876abcdef4567abcdef8987abcdef4357"}} + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s", stashURIprefix, projectsURI, RepositoriesURI, commitsURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + b := struct { + Commits []*CommitObject `json:"values"` + }{[]*CommitObject{ + cIDs[0], + cIDs[1], + cIDs[2], + cIDs[3], + }} + json.NewEncoder(w).Encode(b) + return + + }) + ctx := context.Background() + list, err := client.Commits.List(ctx, "prj1", "repo1", nil) + if err != nil { + t.Fatalf("Commits.List returned error: %v", err) + } + + if diff := cmp.Diff(cIDs, list.Commits); diff != "" { + t.Errorf("Commits.List returned diff (want -> got):\n%s", diff) + } + +}