Files
atlantis/server/events/event_parser.go
2018-02-28 10:42:21 -08:00

240 lines
7.7 KiB
Go

package events
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/google/go-github/github"
"github.com/lkysow/go-gitlab"
"github.com/runatlantis/atlantis/server/events/models"
)
const gitlabPullOpened = "opened"
const usagesCols = 90
// multiLineRegex is used to ignore multi-line comments since those aren't valid
// Atlantis commands.
var multiLineRegex = regexp.MustCompile(`.*\r?\n.+`)
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_event_parsing.go EventParsing
type Command struct {
Name CommandName
Workspace string
Verbose bool
Flags []string
// Dir is the path relative to the repo root to run the command in.
// If empty string then it wasn't specified. "." is the root of the repo.
// Dir will never end in "/".
Dir string
}
type EventParsing interface {
ParseGithubIssueCommentEvent(comment *github.IssueCommentEvent) (baseRepo models.Repo, user models.User, pullNum int, err error)
ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, error)
ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error)
ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo)
ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User)
ParseGitlabMergeRequest(mr *gitlab.MergeRequest) models.PullRequest
}
type EventParser struct {
GithubUser string
GithubToken string
GitlabUser string
GitlabToken string
}
func (e *EventParser) ParseGithubIssueCommentEvent(comment *github.IssueCommentEvent) (baseRepo models.Repo, user models.User, pullNum int, err error) {
baseRepo, err = e.ParseGithubRepo(comment.Repo)
if err != nil {
return
}
if comment.Comment == nil || comment.Comment.User.GetLogin() == "" {
err = errors.New("comment.user.login is null")
return
}
commentorUsername := comment.Comment.User.GetLogin()
user = models.User{
Username: commentorUsername,
}
pullNum = comment.Issue.GetNumber()
if pullNum == 0 {
err = errors.New("issue.number is null")
return
}
return
}
func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, error) {
var pullModel models.PullRequest
var headRepoModel models.Repo
commit := pull.Head.GetSHA()
if commit == "" {
return pullModel, headRepoModel, errors.New("head.sha is null")
}
url := pull.GetHTMLURL()
if url == "" {
return pullModel, headRepoModel, errors.New("html_url is null")
}
branch := pull.Head.GetRef()
if branch == "" {
return pullModel, headRepoModel, errors.New("head.ref is null")
}
authorUsername := pull.User.GetLogin()
if authorUsername == "" {
return pullModel, headRepoModel, errors.New("user.login is null")
}
num := pull.GetNumber()
if num == 0 {
return pullModel, headRepoModel, errors.New("number is null")
}
headRepoModel, err := e.ParseGithubRepo(pull.Head.Repo)
if err != nil {
return pullModel, headRepoModel, err
}
pullState := models.Closed
if pull.GetState() == "open" {
pullState = models.Open
}
return models.PullRequest{
Author: authorUsername,
Branch: branch,
HeadCommit: commit,
URL: url,
Num: num,
State: pullState,
}, headRepoModel, nil
}
func (e *EventParser) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) {
var repo models.Repo
repoFullName := ghRepo.GetFullName()
if repoFullName == "" {
return repo, errors.New("repository.full_name is null")
}
repoOwner := ghRepo.Owner.GetLogin()
if repoOwner == "" {
return repo, errors.New("repository.owner.login is null")
}
repoName := ghRepo.GetName()
if repoName == "" {
return repo, errors.New("repository.name is null")
}
repoSanitizedCloneURL := ghRepo.GetCloneURL()
if repoSanitizedCloneURL == "" {
return repo, errors.New("repository.clone_url is null")
}
// Construct HTTPS repo clone url string with username and password.
repoCloneURL := strings.Replace(repoSanitizedCloneURL, "https://", fmt.Sprintf("https://%s:%s@", e.GithubUser, e.GithubToken), -1)
return models.Repo{
Owner: repoOwner,
FullName: repoFullName,
CloneURL: repoCloneURL,
SanitizedCloneURL: repoSanitizedCloneURL,
Name: repoName,
}, nil
}
func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo) {
modelState := models.Closed
if event.ObjectAttributes.State == gitlabPullOpened {
modelState = models.Open
}
// GitLab also has a "merged" state, but we map that to Closed so we don't
// need to check for it.
pull := models.PullRequest{
URL: event.ObjectAttributes.URL,
Author: event.User.Username,
Num: event.ObjectAttributes.IID,
HeadCommit: event.ObjectAttributes.LastCommit.ID,
Branch: event.ObjectAttributes.SourceBranch,
State: modelState,
}
cloneURL := e.addGitlabAuth(event.Project.GitHTTPURL)
// Get owner and name from PathWithNamespace because the fields
// event.Project.Name and event.Project.Owner can have capitals.
owner, name := e.getOwnerAndName(event.Project.PathWithNamespace)
repo := models.Repo{
FullName: event.Project.PathWithNamespace,
Name: name,
SanitizedCloneURL: event.Project.GitHTTPURL,
Owner: owner,
CloneURL: cloneURL,
}
return pull, repo
}
// addGitlabAuth adds gitlab username/password to the cloneURL.
// We support http and https URLs because GitLab's docs have http:// URLs whereas
// their API responses have https://.
// Ex. https://gitlab.com/owner/repo.git => https://uname:pass@gitlab.com/owner/repo.git
func (e *EventParser) addGitlabAuth(cloneURL string) string {
httpsReplaced := strings.Replace(cloneURL, "https://", fmt.Sprintf("https://%s:%s@", e.GitlabUser, e.GitlabToken), -1)
return strings.Replace(httpsReplaced, "http://", fmt.Sprintf("http://%s:%s@", e.GitlabUser, e.GitlabToken), -1)
}
// getOwnerAndName takes pathWithNamespace that should look like "owner/repo"
// and returns "owner", "repo"
func (e *EventParser) getOwnerAndName(pathWithNamespace string) (string, string) {
pathSplit := strings.Split(pathWithNamespace, "/")
if len(pathSplit) > 1 {
return pathSplit[0], pathSplit[1]
}
return "", ""
}
// ParseGitlabMergeCommentEvent creates Atlantis models out of a GitLab event.
func (e *EventParser) ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User) {
// Get owner and name from PathWithNamespace because the fields
// event.Project.Name and event.Project.Owner can have capitals.
owner, name := e.getOwnerAndName(event.Project.PathWithNamespace)
baseRepo = models.Repo{
FullName: event.Project.PathWithNamespace,
Name: name,
SanitizedCloneURL: event.Project.GitHTTPURL,
Owner: owner,
CloneURL: e.addGitlabAuth(event.Project.GitHTTPURL),
}
user = models.User{
Username: event.User.Username,
}
owner, name = e.getOwnerAndName(event.MergeRequest.Source.PathWithNamespace)
headRepo = models.Repo{
FullName: event.MergeRequest.Source.PathWithNamespace,
Name: name,
SanitizedCloneURL: event.MergeRequest.Source.GitHTTPURL,
Owner: owner,
CloneURL: e.addGitlabAuth(event.MergeRequest.Source.GitHTTPURL),
}
return
}
func (e *EventParser) ParseGitlabMergeRequest(mr *gitlab.MergeRequest) models.PullRequest {
pullState := models.Closed
if mr.State == gitlabPullOpened {
pullState = models.Open
}
// GitLab also has a "merged" state, but we map that to Closed so we don't
// need to check for it.
return models.PullRequest{
URL: mr.WebURL,
Author: mr.Author.Username,
Num: mr.IID,
HeadCommit: mr.SHA,
Branch: mr.SourceBranch,
State: pullState,
}
}