From fd98e26575836729b1a8a2d03126b65404ce2174 Mon Sep 17 00:00:00 2001 From: Kirill Zaitsev Date: Sat, 22 Aug 2015 06:32:22 +0300 Subject: [PATCH] GitLab service interface --- Godeps/Godeps.json | 2 +- .../Bugagazavr/go-gitlab-client/README.md | 3 + .../examples/projects/main.go | 84 ++++++++++++-- .../examples/repositories/main.go | 32 ++++- .../Bugagazavr/go-gitlab-client/gitlab.go | 29 +++++ .../go-gitlab-client/hook_payload.go | 1 + .../Bugagazavr/go-gitlab-client/hooks.go | 16 ++- .../Bugagazavr/go-gitlab-client/projects.go | 65 +++++++++-- .../go-gitlab-client/projects_test.go | 15 +++ .../go-gitlab-client/repositories.go | 89 +++++++++++++- .../go-gitlab-client/repositories_test.go | 9 ++ .../Bugagazavr/go-gitlab-client/services.go | 19 +++ .../stubs/commits/comments/index.json | 16 +++ .../projects/merge_requests/notes/index.json | 16 +++ cmd/drone-server/drone.go | 16 +++ pkg/remote/builtin/gitlab/gitlab.go | 109 ++++++++++-------- pkg/server/commits.go | 34 ++++++ pkg/server/redirect.go | 61 ++++++++++ pkg/store/builtin/build.go | 12 ++ pkg/store/builtin/build_sql.go | 73 ++++++++++++ pkg/store/store.go | 8 ++ 21 files changed, 629 insertions(+), 80 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/services.go create mode 100644 Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/commits/comments/index.json create mode 100644 Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/projects/merge_requests/notes/index.json create mode 100644 pkg/server/redirect.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 14f6037e4..089d4c11e 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -12,7 +12,7 @@ }, { "ImportPath": "github.com/Bugagazavr/go-gitlab-client", - "Rev": "912567bb7e65212c910733b3bfa178b11049a70e" + "Rev": "fa361f26087a2ff8fbb267fbe2d82037fc35e51a" }, { "ImportPath": "github.com/BurntSushi/migration", diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/README.md b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/README.md index 4e716ee32..7920c5c4b 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/README.md +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/README.md @@ -18,6 +18,9 @@ go-gitlab-client is a simple client written in golang to consume gitlab API. ###Projects [gitlab api doc](http://doc.gitlab.com/ce/api/projects.html) * list projects * get single project + * list project merge requests + * list notes on merge requests + * add comments to merge requests * ###Repositories [gitlab api doc](http://doc.gitlab.com/ce/api/repositories.html) diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/projects/main.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/projects/main.go index 26325c19d..80a3d478d 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/projects/main.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/projects/main.go @@ -37,15 +37,30 @@ func main() { var method string flag.StringVar(&method, "m", "", "Specify method to retrieve projects infos, available methods:\n"+ " > -m projects\n"+ - " > -m project -id PROJECT_ID\n"+ - " > -m hooks -id PROJECT_ID\n"+ - " > -m branches -id PROJECT_ID\n"+ - " > -m merge_requests -id PROJECT_ID\n"+ - " > -m team -id PROJECT_ID") + " > -m project -id PROJECT_ID\n"+ + " > -m hooks -id PROJECT_ID\n"+ + " > -m branches -id PROJECT_ID\n"+ + " > -m merge_requests -id PROJECT_ID\n"+ + " > -m merge_request_notes -id PROJECT_ID -merge_id MERGE_REQUEST_ID\n"+ + " > -m merge_request_comment -id PROJECT_ID -merge_id MERGE_REQUEST_ID -comment COMMENT_BODY\n"+ + " > -m team -id PROJECT_ID\n"+ + " > -m add_drone -id PROJECT_ID\n -token DRONE_TOKEN -url DRONE_URL") var id string flag.StringVar(&id, "id", "", "Specify repository id") + var merge_id string + flag.StringVar(&merge_id, "merge_id", "", "Specify merge request id") + + var comment string + flag.StringVar(&comment, "comment", "", "The body of the new comment") + + var drone_token string + flag.StringVar(&drone_token, "drone_token", "", "Drone service token") + + var drone_url string + flag.StringVar(&drone_url, "drone_url", "", "Drone service url") + flag.Usage = func() { fmt.Printf("Usage:\n") flag.PrintDefaults() @@ -151,11 +166,54 @@ func main() { if mr.Assignee != nil { assignee = mr.Assignee.Username } - fmt.Printf(" %s -> %s [%s] author[%s] assignee[%s]\n", - mr.SourceBranch, mr.TargetBranch, mr.State, + fmt.Printf(" (#%d) %s -> %s [%s] author[%s] assignee[%s]\n", + mr.Id, mr.SourceBranch, mr.TargetBranch, mr.State, author, assignee) } + case "merge_request_notes": + fmt.Println("Fetching merge_request notes…") + + if id == "" { + flag.Usage() + return + } + + notes, err := gitlab.MergeRequestNotes(id, merge_id, 0, 30) + if err != nil { + fmt.Println(err.Error()) + return + } + + for _, note := range notes { + author := "" + if note.Author != nil { + author = note.Author.Username + } + fmt.Printf(" [%d] author: %s <%s> %s\n", + note.Id, author, note.CreatedAt, note.Body) + } + + case "merge_request_comment": + fmt.Println("Sending new merge_request comment…") + + if id == "" { + flag.Usage() + return + } + + note, err := gitlab.SendMergeRequestComment(id, merge_id, comment) + if err != nil { + fmt.Println(err.Error()) + return + } + author := "" + if note.Author != nil { + author = note.Author.Username + } + fmt.Printf(" [%d] author: %s <%s> %s\n", + note.Id, author, note.CreatedAt, note.Body) + case "hooks": fmt.Println("Fetching project hooks…") @@ -191,5 +249,17 @@ func main() { for _, member := range members { fmt.Printf("> [%d] %s (%s) since %s\n", member.Id, member.Username, member.Name, member.CreatedAt) } + case "add_drone": + fmt.Println("Adding drone service to project") + + if id == "" || drone_token == "" || drone_url == "" { + flag.Usage() + return + } + + if err := gitlab.AddDroneService(id, map[string]string{"token": drone_token, "drone_url": drone_url}); err != nil { + fmt.Println(err) + return + } } } diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/repositories/main.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/repositories/main.go index e6591bb91..8a250204c 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/repositories/main.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/examples/repositories/main.go @@ -36,11 +36,19 @@ func main() { " > branches\n"+ " > branch\n"+ " > tags\n"+ - " > commits") + " > commits\n"+ + " > commit_comments -sha COMMIT_SHA\n"+ + " > comment_a_commit -sha COMMIT_SHA -comment COMMENT_BODY") var id string flag.StringVar(&id, "id", "", "Specify repository id") + var sha string + flag.StringVar(&sha, "sha", "", "Specify commit sha") + + var comment string + flag.StringVar(&comment, "comment", "", "The body of the new comment") + flag.Usage = func() { fmt.Printf("Usage:\n") flag.PrintDefaults() @@ -90,7 +98,27 @@ func main() { } for _, commit := range commits { - fmt.Printf("%s > [%s] %s\n", commit.CreatedAt.Format("Mon 02 Jan 15:04"), commit.Author_Name, commit.Title) + fmt.Printf("(%s) %s > [%s] %s\n", commit.Id, commit.CreatedAt.Format("Mon 02 Jan 15:04"), commit.Author_Name, commit.Title) } + case "commit_comments": + fmt.Println("Fetching comments on a repository commit…") + + comments, err := gitlab.RepoCommitComments(id, sha) + if err != nil { + fmt.Println(err.Error()) + } + + for _, c := range comments { + fmt.Printf("[%s] %s\n", c.Author.Username, c.Note) + } + case "comment_a_commit": + fmt.Println("Sending a new comment on a repository commit…") + + c, err := gitlab.SendRepoCommitComment(id, sha, comment) + if err != nil { + fmt.Println(err.Error()) + } + + fmt.Printf("[%s] %s\n", c.Author.Username, c.Note) } } diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/gitlab.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/gitlab.go index 415dc5ed6..dceb17541 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/gitlab.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/gitlab.go @@ -128,6 +128,35 @@ func (g *Gitlab) ResourceUrlQuery(u string, params, query map[string]string) str } +func (g *Gitlab) ResourceUrlQueryRaw(u string, params, query map[string]string) (string, string) { + if params != nil { + for key, val := range params { + u = strings.Replace(u, key, encodeParameter(val), -1) + } + } + + query_params := url.Values{} + if !g.Bearer { + query_params.Add("private_token", g.Token) + } + + if query != nil { + for key, val := range query { + query_params.Set(key, val) + } + } + + u = g.BaseUrl + g.ApiPath + u + "?" + query_params.Encode() + p, err := url.Parse(u) + if err != nil { + return u, "" + } + + opaque := "//" + p.Host + p.Path + return u, opaque + +} + func (g *Gitlab) ResourceUrlRaw(u string, params map[string]string) (string, string) { if params != nil { diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hook_payload.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hook_payload.go index 8bb7e9add..e4c33c0a2 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hook_payload.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hook_payload.go @@ -27,6 +27,7 @@ type HookObjAttr struct { StDiffs string `json:"st_diffs,omitempty"` MergeStatus string `json:"merge_status,omitempty"` TargetProjectId int `json:"target_project_id,omitempty"` + URL string `json:"url,omitempty"` Source *hProject `json:"source,omitempty"` Target *hProject `json:"target,omitempty"` LastCommit *hCommit `json:"last_commit,omitempty"` diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hooks.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hooks.go index 9dd0fe4bb..3f19de7eb 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hooks.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/hooks.go @@ -88,13 +88,13 @@ Parameters: merge_requests_events Trigger hook on merge_requests events */ -func (g *Gitlab) AddProjectHook(id, hook_url string, push_events, issues_events, merge_requests_events bool) error { +func (g *Gitlab) AddProjectHook(id, hook_url string, push_events, issues_events, merge_requests_events, tag_events bool) error { url, opaque := g.ResourceUrlRaw(project_url_hooks, map[string]string{":id": id}) var err error - body := buildHookQuery(hook_url, push_events, issues_events, merge_requests_events) + body := buildHookQuery(hook_url, push_events, issues_events, merge_requests_events, tag_events) _, err = g.buildAndExecRequestRaw("POST", url, opaque, []byte(body)) return err @@ -115,7 +115,7 @@ Parameters: merge_requests_events Trigger hook on merge_requests events */ -func (g *Gitlab) EditProjectHook(id, hook_id, hook_url string, push_events, issues_events, merge_requests_events bool) error { +func (g *Gitlab) EditProjectHook(id, hook_id, hook_url string, push_events, issues_events, merge_requests_events, tag_events bool) error { url, opaque := g.ResourceUrlRaw(project_url_hook, map[string]string{ ":id": id, @@ -124,7 +124,7 @@ func (g *Gitlab) EditProjectHook(id, hook_id, hook_url string, push_events, issu var err error - body := buildHookQuery(hook_url, push_events, issues_events, merge_requests_events) + body := buildHookQuery(hook_url, push_events, issues_events, merge_requests_events, tag_events) _, err = g.buildAndExecRequestRaw("PUT", url, opaque, []byte(body)) return err @@ -158,7 +158,7 @@ func (g *Gitlab) RemoveProjectHook(id, hook_id string) error { /* Build HTTP query to add or edit hook */ -func buildHookQuery(hook_url string, push_events, issues_events, merge_requests_events bool) string { +func buildHookQuery(hook_url string, push_events, issues_events, merge_requests_events, tag_events bool) string { v := url.Values{} v.Set("url", hook_url) @@ -178,6 +178,10 @@ func buildHookQuery(hook_url string, push_events, issues_events, merge_requests_ } else { v.Set("merge_requests_events", "false") } - + if tag_events { + v.Set("tag_push_events", "true") + } else { + v.Set("tag_push_events", "false") + } return v.Encode() } diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects.go index 9de030ec0..b470f57e3 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects.go @@ -2,19 +2,21 @@ package gogitlab import ( "encoding/json" + "fmt" "strconv" "strings" ) const ( - projects_url = "/projects" // Get a list of projects owned by the authenticated user - 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 - project_url_branches = "/projects/:id/repository/branches" // Lists all branches of a project - project_url_members = "/projects/:id/members" // List project team members - project_url_member = "/projects/:id/members/:user_id" // Get project team member - project_url_merge_requests = "/projects/:id/merge_requests" // List all merge requests of a project + projects_url = "/projects" // Get a list of projects owned by the authenticated user + 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 + project_url_branches = "/projects/:id/repository/branches" // Lists all branches of a project + project_url_members = "/projects/:id/members" // List project team members + project_url_member = "/projects/:id/members/:user_id" // Get project team member + project_url_merge_requests = "/projects/:id/merge_requests" // List all merge requests of a project + merge_request_url_notes = "/projects/:id/merge_requests/:merge_request_id/notes" // Manage comments for a given merge request ) type Member struct { @@ -89,6 +91,14 @@ type MergeRequest struct { Description string `json:"description,omitempty"` } +type MergeRequestNote struct { + Attachment interface{} `json:"attachment"` + Body string `json:"body"` + CreatedAt string `json:"created_at"` + Id int `json:"id"` + Author *Member `json:"author"` +} + /* Get a list of all projects owned by the authenticated user. */ @@ -209,6 +219,43 @@ func (g *Gitlab) ProjectMergeRequests(id string, page int, per_page int, state s return mr, err } +/* +Lists all comments on merge request. +*/ +func (g *Gitlab) MergeRequestNotes(id string, merge_request_id string, page int, per_page int) ([]*MergeRequestNote, error) { + par := map[string]string{":id": id, ":merge_request_id": merge_request_id} + qry := map[string]string{ + "page": strconv.Itoa(page), + "per_page": strconv.Itoa(per_page)} + url := g.ResourceUrlQuery(merge_request_url_notes, par, qry) + + var mr []*MergeRequestNote + + contents, err := g.buildAndExecRequest("GET", url, nil) + if err == nil { + err = json.Unmarshal(contents, &mr) + } + + return mr, err +} + +/* +Creates a new comment on a merge request. +*/ +func (g *Gitlab) SendMergeRequestComment(id string, merge_request_id string, comment string) (*MergeRequestNote, error) { + par := map[string]string{":id": id, ":merge_request_id": merge_request_id} + url := g.ResourceUrlQuery(merge_request_url_notes, par, map[string]string{}) + + var mr *MergeRequestNote + + contents, err := g.buildAndExecRequest("POST", url, []byte(fmt.Sprintf("body=%s", comment))) + if err == nil { + err = json.Unmarshal(contents, &mr) + } + + return mr, err +} + /* Get single project id. @@ -236,7 +283,7 @@ func (g *Gitlab) SearchProjectId(namespace string, name string) (id int, err err } for _, project := range projects { - if project.Namespace.Name == namespace { + if project.Namespace.Name == namespace && strings.ToLower(project.Name) == strings.ToLower(name) { id = project.Id } } diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects_test.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects_test.go index 2484d6f48..6fcbda844 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects_test.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/projects_test.go @@ -57,6 +57,21 @@ func TestProjectMergeRequests(t *testing.T) { } } +func TestMergeRequestNotes(t *testing.T) { + ts, gitlab := Stub("stubs/projects/merge_requests/notes/index.json") + defer ts.Close() + notes, err := gitlab.MergeRequestNotes("1", "1", 0, 30) + + assert.Equal(t, err, nil) + assert.Equal(t, len(notes), 1) + + if len(notes) > 0 { + assert.Equal(t, notes[0].Id, 301) + assert.Equal(t, notes[0].Body, "Comment for MR") + assert.Equal(t, notes[0].Author.Username, "pipin") + } +} + func TestSearchProjectId(t *testing.T) { ts, gitlab := Stub("stubs/projects/index.json") diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories.go index 694100e0e..47d54b706 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories.go @@ -2,17 +2,19 @@ package gogitlab import ( "encoding/json" + "fmt" "net/url" "time" ) const ( - repo_url_branches = "/projects/:id/repository/branches" // List repository branches - repo_url_branch = "/projects/:id/repository/branches/:branch" // Get a specific branch of a project. - repo_url_tags = "/projects/:id/repository/tags" // List project repository tags - repo_url_commits = "/projects/:id/repository/commits" // List repository commits - repo_url_tree = "/projects/:id/repository/tree" // List repository tree - repo_url_raw_file = "/projects/:id/repository/blobs/:sha" // Get raw file content for specific commit/branch + repo_url_branches = "/projects/:id/repository/branches" // List repository branches + repo_url_branch = "/projects/:id/repository/branches/:branch" // Get a specific branch of a project. + repo_url_tags = "/projects/:id/repository/tags" // List project repository tags + repo_url_commits = "/projects/:id/repository/commits" // List repository commits + repo_url_commit_comments = "/projects/:id/repository/commits/:sha/comments" // New comment or list of commit comments + repo_url_tree = "/projects/:id/repository/tree" // List repository tree + repo_url_raw_file = "/projects/:id/repository/blobs/:sha" // Get raw file content for specific commit/branch ) type BranchCommit struct { @@ -62,6 +64,14 @@ type File struct { Children []*File } +type CommitComment struct { + Author *Member `json:"author,omitempty"` + Line int `json:"line,omitempty"` + LineType string `json:"line_type,omitempty"` + Note string `json:"note,omitempty"` + Path string `json:"path,omitempty"` +} + /* Get a list of repository branches from a project, sorted by name alphabetically. @@ -195,6 +205,73 @@ func (g *Gitlab) RepoCommits(id string) ([]*Commit, error) { return commits, err } +/* +Get a list of comments in a repository commit. + + GET /projects/:id/repository/commits/:sha/comments + +Parameters: + + id The ID of a project + sha The sha of the commit + +Usage: + + comments, err := gitlab.RepoCommitComments("your_projet_id", "commit_sha") + if err != nil { + fmt.Println(err.Error()) + } + for _, comment := range comments { + fmt.Printf("%+v\n", comment) + } +*/ +func (g *Gitlab) RepoCommitComments(id string, sha string) ([]*CommitComment, error) { + + url, opaque := g.ResourceUrlRaw(repo_url_commit_comments, map[string]string{":id": id, ":sha": sha}) + + var comments []*CommitComment + + contents, err := g.buildAndExecRequestRaw("GET", url, opaque, nil) + if err == nil { + err = json.Unmarshal(contents, &comments) + } + + return comments, err +} + +/* +Create a comment in a repository commit. + + POST /projects/:id/repository/commits/:sha/comments + +Parameters: + + id The ID of a project + sha The sha of the commit + body The body of the comment + +Usage: + + comment, err := gitlab.SendRepoCommitComment("your_projet_id", "commit_sha", "your comment goes here") + if err != nil { + fmt.Println(err.Error()) + } + fmt.Printf("%+v\n", comment) +*/ +func (g *Gitlab) SendRepoCommitComment(id string, sha string, body string) (*CommitComment, error) { + + url, opaque := g.ResourceUrlRaw(repo_url_commit_comments, map[string]string{":id": id, ":sha": sha}) + + var comment *CommitComment + + contents, err := g.buildAndExecRequestRaw("POST", url, opaque, []byte(fmt.Sprintf("note=%s", body))) + if err == nil { + err = json.Unmarshal(contents, &comment) + } + + return comment, err +} + /* Get Raw file content */ diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories_test.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories_test.go index ab445fbc5..36a4d7bda 100644 --- a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories_test.go +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/repositories_test.go @@ -41,3 +41,12 @@ func TestRepoCommits(t *testing.T) { assert.Equal(t, len(commits), 2) defer ts.Close() } + +func TestRepoCommitComments(t *testing.T) { + ts, gitlab := Stub("stubs/commits/comments/index.json") + comments, err := gitlab.RepoCommitComments("1", "a9e6a5io4e695923c995ed2e836789b50oi77e0b") + + assert.Equal(t, err, nil) + assert.Equal(t, len(comments), 1) + defer ts.Close() +} diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/services.go b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/services.go new file mode 100644 index 000000000..6ef79c283 --- /dev/null +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/services.go @@ -0,0 +1,19 @@ +package gogitlab + +const ( + drone_service_url = "/projects/:id/services/drone-ci" +) + +func (g *Gitlab) AddDroneService(id string, params map[string]string) error { + url, opaque := g.ResourceUrlQueryRaw(drone_service_url, map[string]string{":id": id}, params) + + _, err := g.buildAndExecRequestRaw("PUT", url, opaque, nil) + return err +} + +func (g *Gitlab) DeleteDroneService(id string) error { + url, opaque := g.ResourceUrlQueryRaw(drone_service_url, map[string]string{":id": id}, nil) + + _, err := g.buildAndExecRequestRaw("DELETE", url, opaque, nil) + return err +} diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/commits/comments/index.json b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/commits/comments/index.json new file mode 100644 index 000000000..f3886f5c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/commits/comments/index.json @@ -0,0 +1,16 @@ +[ + { + "author": { + "id": 1, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "blocked": false, + "created_at": "2012-04-29T08:46:00Z" + }, + "note": "text1", + "path": "example.rb", + "line": 5, + "line_type": "new" + } +] \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/projects/merge_requests/notes/index.json b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/projects/merge_requests/notes/index.json new file mode 100644 index 000000000..52f57e809 --- /dev/null +++ b/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client/stubs/projects/merge_requests/notes/index.json @@ -0,0 +1,16 @@ +[ + { + "id": 301, + "body": "Comment for MR", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z" + }, + "created_at": "2013-10-02T08:57:14Z" + } +] \ No newline at end of file diff --git a/cmd/drone-server/drone.go b/cmd/drone-server/drone.go index 737fa78ba..16001ae55 100644 --- a/cmd/drone-server/drone.go +++ b/cmd/drone-server/drone.go @@ -185,6 +185,14 @@ func main() { repo.GET("/logs/:number/:task", server.GetLogs) // repo.POST("/status/:number", server.PostBuildStatus) } + + // Routes for external services + repoExternal := repos.Group("") + { + repoExternal.Use(server.SetRepo()) + + repoExternal.GET("/pr/:number", server.GetPullRequest) + } } badges := api.Group("/badges/:owner/:name") @@ -236,6 +244,14 @@ func main() { auth.POST("", server.GetLogin) } + redirects := r.Group("/redirect") + { + redirects.Use(server.SetDatastore(store)) + redirects.Use(server.SetRepo()) + + redirects.GET("/:owner/:name/commit/:sha", server.RedirectSha) + } + r.SetHTMLTemplate(index()) r.NoRoute(func(c *gin.Context) { c.HTML(200, "index.html", nil) diff --git a/pkg/remote/builtin/gitlab/gitlab.go b/pkg/remote/builtin/gitlab/gitlab.go index a79809bfa..e5c0560dc 100644 --- a/pkg/remote/builtin/gitlab/gitlab.go +++ b/pkg/remote/builtin/gitlab/gitlab.go @@ -6,8 +6,10 @@ import ( "io/ioutil" "net/http" "net/url" + "regexp" "strconv" "strings" + "time" "github.com/drone/drone/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client" "github.com/drone/drone/Godeps/_workspace/src/github.com/hashicorp/golang-lru" @@ -190,25 +192,15 @@ func (r *Gitlab) Activate(user *common.User, repo *common.Repo, k *common.Keypai return err } - title, err := GetKeyTitle(link) + uri, err := url.Parse(link) if err != nil { return err } - // if the repository is private we'll need - // to upload a github key to the repository - if repo.Private { - if err := client.AddProjectDeployKey(id, title, k.Public); err != nil { - return err - } - } + droneUrl := fmt.Sprintf("%s://%s", uri.Scheme, uri.Host) + droneToken := uri.Query().Get("access_token") - // append the repo owner / name to the hook url since gitlab - // doesn't send this detail in the post-commit hook - link += "&owner=" + repo.Owner + "&name=" + repo.Name - - // add the hook - return client.AddProjectHook(id, link, true, false, true) + return client.AddDroneService(id, map[string]string{"token": droneToken, "drone_url": droneUrl}) } // Deactivate removes a repository by removing all the post-commit hooks @@ -220,33 +212,7 @@ func (r *Gitlab) Deactivate(user *common.User, repo *common.Repo, link string) e return err } - keys, err := client.ProjectDeployKeys(id) - if err != nil { - return err - } - var pubkey = strings.TrimSpace(repo.Keys.Public) - for _, k := range keys { - if pubkey == strings.TrimSpace(k.Key) { - if err := client.RemoveProjectDeployKey(id, strconv.Itoa(k.Id)); err != nil { - return err - } - break - } - } - hooks, err := client.ProjectHooks(id) - if err != nil { - return err - } - link += "&owner=" + repo.Owner + "&name=" + repo.Name - for _, h := range hooks { - if link == h.Url { - if err := client.RemoveProjectHook(id, strconv.Itoa(h.Id)); err != nil { - return err - } - break - } - } - return nil + return client.DeleteDroneService(id) } // ParseHook parses the post-commit hook from the Request body @@ -259,20 +225,65 @@ func (r *Gitlab) Hook(req *http.Request) (*common.Hook, error) { return nil, err } - if len(parsed.After) == 0 || parsed.TotalCommitsCount == 0 { + switch parsed.ObjectKind { + case "merge_request": + if (parsed.ObjectAttributes.State != "reopened" || parsed.ObjectAttributes.State != "opened") && parsed.ObjectAttributes.MergeStatus != "unchecked" { + return nil, nil + } + + return mergeRequest(parsed, req) + case "push": + if len(parsed.After) == 0 || parsed.TotalCommitsCount == 0 { + return nil, nil + } + + return push(parsed, req) + default: return nil, nil } +} - if parsed.ObjectKind == "merge_request" { - // NOTE: in gitlab 8.0, gitlab will get same MR models as github - // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/981/diffs - return nil, nil +func mergeRequest(parsed *gogitlab.HookPayload, req *http.Request) (*common.Hook, error) { + var hook = new(common.Hook) + + re := regexp.MustCompile(".git$") + + hook.Repo = &common.Repo{} + hook.Repo.Owner = req.FormValue("owner") + hook.Repo.Name = req.FormValue("name") + hook.Repo.FullName = fmt.Sprintf("%s/%s", hook.Repo.Owner, hook.Repo.Name) + hook.Repo.Link = re.ReplaceAllString(parsed.ObjectAttributes.Target.HttpUrl, "$1") + hook.Repo.Clone = parsed.ObjectAttributes.Target.HttpUrl + hook.Repo.Branch = "master" + + hook.Commit = &common.Commit{} + hook.Commit.Message = parsed.ObjectAttributes.LastCommit.Message + hook.Commit.Sha = parsed.ObjectAttributes.LastCommit.Id + hook.Commit.Remote = parsed.ObjectAttributes.Source.HttpUrl + + if parsed.ObjectAttributes.Source.HttpUrl == parsed.ObjectAttributes.Target.HttpUrl { + hook.Commit.Ref = fmt.Sprintf("refs/heads/%s", parsed.ObjectAttributes.SourceBranch) + hook.Commit.Branch = parsed.ObjectAttributes.SourceBranch + hook.Commit.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST") + } else { + hook.Commit.Ref = fmt.Sprintf("refs/merge-requests/%d/head", parsed.ObjectAttributes.IId) + hook.Commit.Branch = parsed.ObjectAttributes.SourceBranch + hook.Commit.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST") } - if len(parsed.After) == 0 { - return nil, nil - } + hook.Commit.Author = &common.Author{} + hook.Commit.Author.Login = parsed.ObjectAttributes.LastCommit.Author.Name + hook.Commit.Author.Email = parsed.ObjectAttributes.LastCommit.Author.Email + hook.PullRequest = &common.PullRequest{} + hook.PullRequest.Number = parsed.ObjectAttributes.IId + hook.PullRequest.Title = parsed.ObjectAttributes.Description + hook.PullRequest.Link = parsed.ObjectAttributes.URL + + return hook, nil +} + +func push(parsed *gogitlab.HookPayload, req *http.Request) (*common.Hook, error) { var cloneUrl = parsed.Repository.GitHttpUrl var hook = new(common.Hook) diff --git a/pkg/server/commits.go b/pkg/server/commits.go index bf9b647e1..cead75400 100644 --- a/pkg/server/commits.go +++ b/pkg/server/commits.go @@ -41,6 +41,40 @@ func GetCommit(c *gin.Context) { } } +// GetPullRequest accepts a requests to retvie a pull request +// from the datastore for the given repository and +// pull request number +// +// GET /api/repos/:owner/:name/pr/:number +// +func GetPullRequest(c *gin.Context) { + store := ToDatastore(c) + repo := ToRepo(c) + + // get the token and verify the hook is authorized + if c.Request.FormValue("access_token") != hash(repo.FullName, repo.Hash) { + c.AbortWithStatus(403) + return + } + + num, err := strconv.Atoi(c.Params.ByName("number")) + if err != nil { + c.Fail(400, err) + return + } + build, err := store.BuildPullRequestNumber(repo, num) + if err != nil { + c.Fail(404, err) + return + } + build.Jobs, err = store.JobList(build) + if err != nil { + c.Fail(404, err) + } else { + c.JSON(200, build) + } +} + // GetCommits accepts a request to retrieve a list // of commits from the datastore for the given repository. // diff --git a/pkg/server/redirect.go b/pkg/server/redirect.go new file mode 100644 index 000000000..59a0608f4 --- /dev/null +++ b/pkg/server/redirect.go @@ -0,0 +1,61 @@ +package server + +import ( + "fmt" + "strconv" + + "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" +) + +// RedirectSha accepts a request to retvie a redirect +// to job from the datastore for the given repository +// and commit sha +// +// GET /redirect/:owner/:name/commit/:sha +// +func RedirectSha(c *gin.Context) { + var branch string + + store := ToDatastore(c) + repo := ToRepo(c) + sha := c.Params.ByName("sha") + + branch = c.Request.FormValue("branch") + if branch == "" { + branch = repo.Branch + } + + build, err := store.BuildSha(repo, sha, branch) + if err != nil { + c.Redirect(301, "/") + return + } + + c.Redirect(301, fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.ID)) + return +} + +// RedirectSha accepts a request to retvie a redirect +// to job from the datastore for the given repository +// and pull request number +// +// GET /redirect/:owner/:name/pr/:number +// +func RedirectPullRequest(c *gin.Context) { + store := ToDatastore(c) + repo := ToRepo(c) + num, err := strconv.Atoi(c.Params.ByName("number")) + if err != nil { + c.Redirect(301, "/") + return + } + + build, err := store.BuildPullRequestNumber(repo, num) + if err != nil { + c.Redirect(301, "/") + return + } + + c.Redirect(301, fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.ID)) + return +} diff --git a/pkg/store/builtin/build.go b/pkg/store/builtin/build.go index e2b139e53..ba14b19e1 100644 --- a/pkg/store/builtin/build.go +++ b/pkg/store/builtin/build.go @@ -25,6 +25,18 @@ func (db *Buildstore) BuildNumber(repo *types.Repo, seq int) (*types.Build, erro return getBuild(db, rebind(stmtBuildSelectBuildNumber), repo.ID, seq) } +// BuildPullRequest gets the specific build for the +// named repository and pull request number +func (db *Buildstore) BuildPullRequestNumber(repo *types.Repo, seq int) (*types.Build, error) { + return getBuild(db, rebind(stmtBuildSelectPullRequestNumber), repo.ID, seq) +} + +// BuildSha gets the specific build for the +// named repository and sha +func (db *Buildstore) BuildSha(repo *types.Repo, sha, branch string) (*types.Build, error) { + return getBuild(db, rebind(stmtBuildSelectSha), repo.ID, sha, branch) +} + // BuildLast gets the last executed build for the // named repository. func (db *Buildstore) BuildLast(repo *types.Repo, branch string) (*types.Build, error) { diff --git a/pkg/store/builtin/build_sql.go b/pkg/store/builtin/build_sql.go index eabc37285..32b7ce98c 100644 --- a/pkg/store/builtin/build_sql.go +++ b/pkg/store/builtin/build_sql.go @@ -596,6 +596,79 @@ WHERE build_repo_id = ? AND build_number = ? ` +const stmtBuildSelectPullRequestNumber = ` +SELECT + build_id +,build_repo_id +,build_number +,build_status +,build_started +,build_finished +,build_commit_sha +,build_commit_ref +,build_commit_link +,build_commit_branch +,build_commit_message +,build_commit_timestamp +,build_commit_remote +,build_commit_author_login +,build_commit_author_email +,build_pull_request_number +,build_pull_request_title +,build_pull_request_link +,build_pull_request_base_sha +,build_pull_request_base_ref +,build_pull_request_base_link +,build_pull_request_base_branch +,build_pull_request_base_message +,build_pull_request_base_timestamp +,build_pull_request_base_remote +,build_pull_request_base_author_login +,build_pull_request_base_author_email +FROM builds +WHERE build_repo_id = ? +AND build_pull_request_number = ? +ORDER BY build_number DESC +LIMIT 1 +` + +const stmtBuildSelectSha = ` +SELECT + build_id +,build_repo_id +,build_number +,build_status +,build_started +,build_finished +,build_commit_sha +,build_commit_ref +,build_commit_link +,build_commit_branch +,build_commit_message +,build_commit_timestamp +,build_commit_remote +,build_commit_author_login +,build_commit_author_email +,build_pull_request_number +,build_pull_request_title +,build_pull_request_link +,build_pull_request_base_sha +,build_pull_request_base_ref +,build_pull_request_base_link +,build_pull_request_base_branch +,build_pull_request_base_message +,build_pull_request_base_timestamp +,build_pull_request_base_remote +,build_pull_request_base_author_login +,build_pull_request_base_author_email +FROM builds +WHERE build_repo_id = ? +AND build_commit_sha = ? +AND build_commit_branch = ? +ORDER BY build_number DESC +LIMIT 1 +` + const stmtBuildSelectCommitBranch = ` SELECT build_id diff --git a/pkg/store/store.go b/pkg/store/store.go index 92470571f..8b2db4ee2 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -131,6 +131,14 @@ type Store interface { // named repository and build number BuildNumber(*types.Repo, int) (*types.Build, error) + // BuildPullRequestNumber gets the specified build number for the + // named repository and build number + BuildPullRequestNumber(*types.Repo, int) (*types.Build, error) + + // BuildSha gets the specified build number for the + // named repository and sha + BuildSha(*types.Repo, string, string) (*types.Build, error) + // BuildLast gets the last executed build for the // named repository and branch BuildLast(*types.Repo, string) (*types.Build, error)