From fae9fab62cbdda733fddbf6b46c52e5ca2a8cbe7 Mon Sep 17 00:00:00 2001 From: Stephan Peijnik Date: Thu, 3 Dec 2015 16:28:49 +0100 Subject: [PATCH 1/4] Implement buildAndExecRequestEx which allows following Gitlab's "next page" links when multiple result pages are returned Signed-off-by: Stephan Peijnik --- gitlab.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/gitlab.go b/gitlab.go index fe6b281..a53b336 100644 --- a/gitlab.go +++ b/gitlab.go @@ -4,6 +4,7 @@ package gogitlab import ( "bytes" "crypto/tls" + "errors" "flag" "fmt" "io/ioutil" @@ -69,32 +70,112 @@ func (g *Gitlab) ResourceUrl(url string, params map[string]string) string { } func (g *Gitlab) buildAndExecRequest(method, url string, body []byte) ([]byte, error) { + return g.buildAndExecRequestEx(method, url, "", body, false) +} + +func (g *Gitlab) buildAndExecRequestEx(method, rawurl, opaque string, body []byte, followNextLink bool) ([]byte, error) { var req *http.Request var err error - if body != nil { - reader := bytes.NewReader(body) - req, err = http.NewRequest(method, url, reader) - } else { - req, err = http.NewRequest(method, url, nil) - } + nextUrl, err := url.Parse(rawurl) if err != nil { - panic("Error while building gitlab request") + return nil, err } - resp, err := g.Client.Do(req) - if err != nil { - return nil, fmt.Errorf("Client.Do error: %q", err) + if len(opaque) > 0 { + nextUrl.Opaque = opaque } - defer resp.Body.Close() - contents, err := ioutil.ReadAll(resp.Body) - if err != nil { - fmt.Printf("%s", err) + + // Check if both body and followNextLink are set + if body != nil && followNextLink { + return nil, errors.New("Cannot body and followNextLink are mutually exclusive") } - if resp.StatusCode >= 400 { - err = fmt.Errorf("*Gitlab.buildAndExecRequest failed: <%d> %s", resp.StatusCode, req.URL) + baseRequestPath := nextUrl.EscapedPath() + privateToken := nextUrl.Query().Get("private_token") + contentsBuffer := &bytes.Buffer{} + for nextUrl != nil { + if body != nil { + reader := bytes.NewReader(body) + req, err = http.NewRequest(method, nextUrl.String(), reader) + } else { + req, err = http.NewRequest(method, nextUrl.String(), nil) + } + if err != nil { + panic("Error while building gitlab request") + } + + resp, err := g.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("Client.Do error: %q", err) + } + defer resp.Body.Close() + partialContents, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Printf("%s", err) + } + + if resp.StatusCode >= 400 { + err = fmt.Errorf("*Gitlab.buildAndExecRequest failed: <%d> %s", resp.StatusCode, req.URL) + } + + if err != nil { + return nil, err + } + + // Clear nextUrl + nextUrl = nil + + // Check if we need to continue + linkHeader := resp.Header.Get("Link") + if followNextLink && linkHeader != "" { + linkHeaders := strings.Split(linkHeader, ",") + for _, link := range linkHeaders { + // Find next link + if strings.HasSuffix(link, "; rel=\"next\"") { + nextRawUrl := strings.Trim(strings.TrimSuffix(link, "; rel=\"next\""), " <>") + next, err := url.Parse(nextRawUrl) + if err != nil { + return nil, err + } + + // Make sure we are targeting the same path + if next.EscapedPath() != baseRequestPath { + return nil, fmt.Errorf("Invalid next URL '%s' - path different (original path: %s)", + nextRawUrl, baseRequestPath) + } + + // Re-set private_token in next... + queryValues := next.Query() + queryValues.Set("private_token", privateToken) + next.RawQuery = queryValues.Encode() + nextUrl = next + break + } + } + + // At this point nextUrl might be set. If this is the case, check for a trailing closing bracket + // in partialContents and replace it with a comma. + if partialContents[len(partialContents)-1] != byte(']') { + return nil, errors.New("Cannot follow next URL: partial contents do not seem to be an array.") + } + + // Remove leading bracket + partialContents = bytes.TrimPrefix(partialContents, []byte("[")) + // Replace trailing closing bracket with a comma + partialContents[len(partialContents)-1] = byte(',') + } + contentsBuffer.Write(partialContents) + } + + contents := contentsBuffer.Bytes() + if contents[0] != byte('[') { + contents = append([]byte{'['}, contents...) + } + + if contents[len(contents)-1] == byte(',') { + contents[len(contents)-1] = byte(']') } return contents, err From bf816eaf6d0bc132c2c7e68da06c1d8108c6ce19 Mon Sep 17 00:00:00 2001 From: Stephan Peijnik Date: Thu, 3 Dec 2015 16:29:42 +0100 Subject: [PATCH 2/4] Add AllProjects function and implement access_level property of Member struct Signed-off-by: Stephan Peijnik --- projects.go | 59 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/projects.go b/projects.go index 4410dbb..6301b57 100644 --- a/projects.go +++ b/projects.go @@ -2,11 +2,13 @@ package gogitlab import ( "encoding/json" + "fmt" "strconv" ) const ( projects_url = "/projects" // Get a list of projects owned by the authenticated user + projects_all_url = "/projects/all" // Get a list of all projects (admin-only) projects_search_url = "/projects/search/:query" // Search for projects by name project_url = "/projects/:id" // Get a specific project, identified by project ID or NAME project_url_events = "/projects/:id/events" // Get project events @@ -15,14 +17,39 @@ const ( project_url_member = "/projects/:id/members/:user_id" // Get project team member ) +type AccessLevel int + +func (al AccessLevel) Name() string { + if name, ok := accessLevelNameMap[al]; ok { + return name + } + return fmt.Sprintf("Unknown(%d)", al) +} + +const ( + AccessLevelGuest AccessLevel = 10 + AccessLevelReporter = 20 + AccessLevelDeveloper = 30 + AccessLevelMaster = 40 + AccessLevelOwner = 50 +) + +var accessLevelNameMap = map[AccessLevel]string{ + AccessLevelGuest: "guest", + AccessLevelReporter: "reporter", + AccessLevelDeveloper: "developer", + AccessLevelMaster: "master", + AccessLevelOwner: "owner", +} + type Member struct { - Id int - Username string - Email string - Name string - State string - CreatedAt string `json:"created_at,omitempty"` - // AccessLevel int + Id int + Username string + Email string + Name string + State string + CreatedAt string `json:"created_at,omitempty"` + AccessLevel AccessLevel `json:"access_level,omitempty"` } type Namespace struct { @@ -72,6 +99,22 @@ func (g *Gitlab) Projects() ([]*Project, error) { return projects, err } +/* +Get a list of all projects (admin-only). +*/ +func (g *Gitlab) AllProjects() ([]*Project, error) { + url := g.ResourceUrl(projects_all_url, nil) + + var projects []*Project + + contents, err := g.buildAndExecRequestEx("GET", url, "", nil, true) + if err == nil { + err = json.Unmarshal(contents, &projects) + } + + return projects, err +} + /* Remove a project. */ @@ -133,7 +176,7 @@ func (g *Gitlab) ProjectMembers(id string) ([]*Member, error) { var members []*Member - contents, err := g.buildAndExecRequestRaw("GET", url, opaque, nil) + contents, err := g.buildAndExecRequestEx("GET", url, opaque, nil, true) if err == nil { err = json.Unmarshal(contents, &members) } From c58c5ced10cdb73e591a5dea94002bdcbd446133 Mon Sep 17 00:00:00 2001 From: Stephan Peijnik Date: Thu, 3 Dec 2015 16:30:56 +0100 Subject: [PATCH 3/4] Implement AllUsers function which fetches all users known to Gitlab Signed-off-by: Stephan Peijnik --- users.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/users.go b/users.go index a9e160d..0630026 100644 --- a/users.go +++ b/users.go @@ -6,9 +6,10 @@ import ( ) const ( - users_url = "/users?page=:page&per_page=:per_page" // Get users list - user_url = "/users/:id" // Get a single user. - current_user_url = "/user" // Get current user + users_url = "/users?page=:page&per_page=:per_page" // Get users list + users_all_url = "/users" // Get all users + user_url = "/users/:id" // Get a single user. + current_user_url = "/user" // Get current user ) type User struct { @@ -28,6 +29,19 @@ type User struct { ColorSchemeId int `json:"color_scheme_id,color_scheme_id"` } +func (g *Gitlab) AllUsers() ([]*User, error) { + url := g.ResourceUrl(users_all_url, nil) + + var users []*User + + contents, err := g.buildAndExecRequestEx("GET", url, "", nil, true) + if err == nil { + err = json.Unmarshal(contents, &users) + } + + return users, err +} + func (g *Gitlab) Users(page, per_page int) ([]*User, error) { url := g.ResourceUrl(users_url, map[string]string{":page": strconv.Itoa(page), ":per_page": strconv.Itoa(per_page)}) From c06ab10a729cbf5a6cae508af3aa20d9b3219b5d Mon Sep 17 00:00:00 2001 From: Stephan Peijnik Date: Thu, 3 Dec 2015 16:31:31 +0100 Subject: [PATCH 4/4] Implement GroupMembers function which allows fetching members of a group - like ProjectMembers does for projects Signed-off-by: Stephan Peijnik --- groups.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 groups.go diff --git a/groups.go b/groups.go new file mode 100644 index 0000000..f22e67b --- /dev/null +++ b/groups.go @@ -0,0 +1,20 @@ +package gogitlab + +import "encoding/json" + +const ( + group_members_url = "/groups/:id/members" +) + +func (g *Gitlab) GroupMembers(id string) ([]*Member, error) { + url, opaque := g.ResourceUrlRaw(group_members_url, map[string]string{":id": id}) + + var members []*Member + + contents, err := g.buildAndExecRequestEx("GET", url, opaque, nil, true) + if err == nil { + err = json.Unmarshal(contents, &members) + } + + return members, err +}