diff --git a/cmd/dads/dads.go b/cmd/dads/dads.go index 59a4a76..7cb4c56 100644 --- a/cmd/dads/dads.go +++ b/cmd/dads/dads.go @@ -24,6 +24,7 @@ import ( "github.com/LF-Engineering/da-ds/pipermail" "github.com/LF-Engineering/da-ds/bugzilla" + "github.com/LF-Engineering/da-ds/gitlab" jsoniter "github.com/json-iterator/go" @@ -90,6 +91,13 @@ func runDS(ctx *lib.Ctx) (err error) { return err } return manager.Sync() + case gitlab.Gitlab: + manager, err := buildGitlabManager(ctx) + if err != nil { + fmt.Println(err) + return err + } + return manager.Sync() default: err = fmt.Errorf("unknown data source type: " + ctx.DS) return @@ -445,6 +453,51 @@ func buildGoogleGroupsManager(ctx *lib.Ctx) (*googlegroups.Manager, error) { return mgr, err } +func buildGitlabManager(ctx *lib.Ctx) (*gitlab.Manager, error) { + + params := &gitlab.MgrParams{} + authData, err := getAuthData() + if err != nil { + fmt.Println(err) + } + + params.Fetch = true + params.Enrich = ctx.BoolEnv("ENRICH") + params.ESBulkSize, _ = strconv.Atoi(ctx.Env("ES_BULK_SIZE")) + params.ESIndex = ctx.Env("RICH_INDEX") + params.ESUrl = ctx.ESURL + params.ESPassword = "" + params.ESUsername = "" + params.ProjectSlug = ctx.Env("PROJECT_SLUG") + params.Project = ctx.Env("PROJECT") + params.ESCacheURL = authData["es_url"] + params.ESCachePassword = authData["es_pass"] + params.ESCacheUsername = authData["es_user"] + params.AuthGrantType = authData["grant_type"] + params.AuthClientID = authData["client_id"] + params.AuthClientSecret = authData["client_secret"] + params.AuthAudience = authData["audience"] + params.Auth0URL = authData["url"] + params.Environment = authData["env"] + params.AffBaseURL = ctx.Env("AFFILIATION_API_URL") + "/v1" + params.Repo = ctx.Env("URL") + params.Token = ctx.Env("TOKEN") + + timeout, err := time.ParseDuration("60s") + if err != nil { + fmt.Println(err) + } + params.HTTPTimeout = timeout + + mgr, err := gitlab.NewManager(params) + if err != nil { + fmt.Println("manager error:", mgr) + } + + return mgr, err + +} + func getAuthData() (map[string]string, error) { var data map[string]string diff --git a/gitlab/const.go b/gitlab/const.go new file mode 100644 index 0000000..4cf3d67 --- /dev/null +++ b/gitlab/const.go @@ -0,0 +1,52 @@ +package gitlab + +import "time" + +const ( + // GitlabAPIVersion ... + GitlabAPIVersion = "v4" + //GitlabAPIBase ... + GitlabAPIBase = "https://gitlab.com/api" + //Gitlab datasource name + Gitlab = "gitlab" + //Unknown ... + Unknown = "Unknown" +) + +var ( + // DefaultDateTime ... + DefaultDateTime = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) + // GitlabRawMapping ... + GitlabRawMapping = []byte(`{"mappings":{"dynamic":true,"properties":{"metadata__updated_on":{"type":"date"},"data":{"properties":{"body":{"dynamic":false,"properties":{}}}}}}}`) + // GitlabRichMapping ... + GitlabRichMapping = []byte(`{ + "mappings": { + "dynamic_templates": [ + { + "notanalyzed": { + "match": "*", + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + }, + { + "formatdate": { + "match": "*", + "match_mapping_type": "date", + "mapping": { + "format": "strict_date_optional_time||epoch_millis", + "type": "date" + } + } + } + ], + "properties": { + "metadata__updated_on": { + "type": "date" + } + } + } + }`) +) diff --git a/gitlab/enricher.go b/gitlab/enricher.go new file mode 100644 index 0000000..dc9a111 --- /dev/null +++ b/gitlab/enricher.go @@ -0,0 +1,366 @@ +package gitlab + +import ( + "fmt" + "log" + "net/url" + "strconv" + "strings" + "time" + + "github.com/LF-Engineering/dev-analytics-libraries/affiliation" + "github.com/LF-Engineering/dev-analytics-libraries/elastic" + "github.com/LF-Engineering/dev-analytics-libraries/uuid" +) + +// AffiliationClient ... +type AffiliationClient interface { + GetIdentityByUser(key string, value string) (*affiliation.AffIdentity, error) + AddIdentity(identity *affiliation.Identity) bool + GetOrganizations(uuid string, projectSlug string) *[]affiliation.Enrollment +} + +// Enricher .. +type Enricher struct { + DSName string + ElasticSearchProvider *elastic.ClientProvider + affiliationsClientProvider AffiliationClient +} + +// NewEnricher initiates a new Enricher +func NewEnricher(esClientProvider *elastic.ClientProvider, affiliationsClientProvider *affiliation.Affiliation) *Enricher { + return &Enricher{ + DSName: Gitlab, + ElasticSearchProvider: esClientProvider, + affiliationsClientProvider: affiliationsClientProvider, + } +} + +// EnrichIssue ... +func (e *Enricher) EnrichIssue(rawItem IssueRaw, now time.Time) (*IssueEnrich, error) { + + enrichedIssue := IssueEnrich{ + BackendName: fmt.Sprintf("%sEnrich", strings.Title(e.DSName)), + BackendVersion: rawItem.BackendVersion, + Title: rawItem.Data.Title, + UUID: rawItem.UUID, + ProjectSlug: rawItem.ProjectSlug, + Project: rawItem.Project, + MetadataUpdatedOn: rawItem.MetadataUpdatedOn, + MetadataTimestamp: rawItem.MetadataTimestamp, + MetadataEnrichedOn: now, + Body: rawItem.Data.Description, + BodyAnalyzed: rawItem.Data.Description, + CreatedAt: rawItem.Data.CreatedAt, + ClosedAt: rawItem.Data.ClosedAt, + UpdatedAt: rawItem.Data.UpdatedAt, + Origin: rawItem.Repo, + AuthorAvatarURL: rawItem.Data.Author.AvatarURL, + AuthorMultiOrgNames: []string{Unknown}, + AuthorOrgName: Unknown, + AuthorLogin: rawItem.Data.Author.Username, + Type: rawItem.Data.Type, + URL: rawItem.Data.WebURL, + URLID: getIssueURLID(rawItem.Repo, rawItem.Data.IssueID), + Repository: rawItem.Repo, + State: rawItem.Data.State, + Tag: rawItem.Repo, + Category: rawItem.Data.Type, + ItemType: rawItem.Data.Type, + IssueID: rawItem.Data.ID, + IsGitlabIssue: 1, + IDInRepo: rawItem.Data.IssueID, + ID: getIssueURLID(rawItem.Repo, rawItem.Data.IssueID), + GitlabRepo: getIssueRepoShort(rawItem.Repo), + Reponame: rawItem.Repo, + RepoShortname: getProjectShortname(rawItem.Repo), + NoOfAssignees: len(rawItem.Data.Assignees), + NoOfComments: rawItem.Data.UserNotesCount, + NoOfReactions: rawItem.Data.Upvotes + rawItem.Data.Downvotes, + NoOfTotalComments: rawItem.Data.UserNotesCount, + UserAvatarURL: rawItem.Data.Author.AvatarURL, + UserLogin: rawItem.Data.Author.Username, + UserDataOrgName: Unknown, + } + + enrichedIssue.Labels = append(enrichedIssue.Labels, rawItem.Data.Labels...) + + source := Gitlab + authorUsername := rawItem.Data.Author.Username + authorName := rawItem.Data.Author.Name + authorUUID, err := uuid.GenerateIdentity(&source, nil, &authorName, &authorUsername) + if err != nil { + return nil, err + } + + userData, err := e.affiliationsClientProvider.GetIdentityByUser("id", authorUUID) + if err != nil { + errMessage := fmt.Sprintf("%+v : %+v", authorUUID, err) + log.Println(errMessage) + } + + if userData != nil { + if userData.ID != nil { + enrichedIssue.AuthorID = *userData.ID + enrichedIssue.UserDataID = *userData.ID + } + if userData.Name != "" { + enrichedIssue.AuthorName = userData.Name + enrichedIssue.Username = userData.Name + enrichedIssue.UserDataName = userData.Name + } + + if userData.UUID != nil { + enrichedIssue.AuthorUUID = *userData.UUID + enrichedIssue.UserDataUUID = *userData.UUID + } + + if userData.Domain != "" { + enrichedIssue.AuthorDomain = userData.Domain + enrichedIssue.UserDataDomain = userData.Domain + } + + if userData.UUID != nil { + slug := rawItem.ProjectSlug + enrollments := e.affiliationsClientProvider.GetOrganizations(*userData.UUID, slug) + if enrollments != nil { + metaDataEpochMills := enrichedIssue.MetadataUpdatedOn.UnixNano() / 1000000 + organizations := make([]string, 0) + for _, enrollment := range *enrollments { + organizations = append(organizations, enrollment.Organization.Name) + } + + foundEnrollment := false + for _, enrollment := range *enrollments { + affStartEpoch := enrollment.Start.UnixNano() / 1000000 + affEndEpoch := enrollment.End.UnixNano() / 1000000 + if affStartEpoch <= metaDataEpochMills && affEndEpoch >= metaDataEpochMills { + enrichedIssue.AuthorOrgName = enrollment.Organization.Name + enrichedIssue.UserDataOrgName = enrollment.Organization.Name + foundEnrollment = true + break + } + } + + if len(organizations) != 0 { + enrichedIssue.AuthorMultiOrgNames = organizations + enrichedIssue.UserDataMultiOrgNames = organizations + } + + if !foundEnrollment && len(organizations) >= 1 { + enrichedIssue.AuthorOrgName = organizations[0] + enrichedIssue.UserDataOrgName = organizations[0] + } + } + + } + + if userData.IsBot != nil { + if *userData.IsBot == 1 { + enrichedIssue.AuthorBot = true + enrichedIssue.UserDataBot = true + } + } + } else { + userIdentity := affiliation.Identity{ + LastModified: now, + Name: authorName, + Source: source, + Username: authorUsername, + ID: authorUUID, + } + + if ok := e.affiliationsClientProvider.AddIdentity(&userIdentity); !ok { + log.Printf("failed to add identity for [%+v]", authorUsername) + } + + enrichedIssue.AuthorID = authorUUID + enrichedIssue.AuthorUUID = authorUUID + enrichedIssue.AuthorName = authorName + + enrichedIssue.UserDataID = authorUUID + enrichedIssue.UserDataUUID = authorUUID + enrichedIssue.UserDataName = authorName + } + + return &enrichedIssue, nil + +} + +// EnrichMergeRequest ... +func (e *Enricher) EnrichMergeRequest(rawItem MergeRequestRaw, now time.Time) (*MergeReqestEnrich, error) { + + enrichedMergeRequest := MergeReqestEnrich{ + BackendName: fmt.Sprintf("%sEnrich", strings.Title(e.DSName)), + BackendVersion: rawItem.BackendVersion, + Title: rawItem.Data.Title, + UUID: rawItem.UUID, + ProjectSlug: rawItem.ProjectSlug, + Project: rawItem.Project, + MetadataUpdatedOn: rawItem.MetadataUpdatedOn, + MetadataTimestamp: rawItem.MetadataTimestamp, + MetadataEnrichedOn: now, + Body: rawItem.Data.Description, + BodyAnalyzed: rawItem.Data.Description, + CreatedAt: rawItem.Data.CreatedAt, + ClosedAt: rawItem.Data.ClosedAt, + UpdatedAt: rawItem.Data.UpdatedAt, + Origin: rawItem.Repo, + AuthorAvatarURL: rawItem.Data.Author.AvatarURL, + AuthorMultiOrgNames: []string{Unknown}, + AuthorOrgName: Unknown, + AuthorLogin: rawItem.Data.Author.Username, + Type: rawItem.Data.Type, + URL: rawItem.Data.WebURL, + URLID: getIssueURLID(rawItem.Repo, rawItem.Data.MergeRequestID), + Repository: rawItem.Repo, + State: rawItem.Data.State, + Tag: rawItem.Repo, + Category: rawItem.Data.Type, + ItemType: rawItem.Data.Type, + MergeRequestID: rawItem.Data.ID, + IsGitlabMergeRequest: 1, + IDInRepo: rawItem.Data.MergeRequestID, + ID: getIssueURLID(rawItem.Repo, rawItem.Data.MergeRequestID), + GitlabRepo: getIssueRepoShort(rawItem.Repo), + Reponame: rawItem.Repo, + RepoShortname: getProjectShortname(rawItem.Repo), + NoOfAssignees: len(rawItem.Data.Assignees), + NoOfRequestedReviewers: len(rawItem.Data.Reviewers), + NoOfComments: rawItem.Data.UserNotesCount, + NoOfReactions: rawItem.Data.Upvotes + rawItem.Data.Downvotes, + NoOfTotalComments: rawItem.Data.UserNotesCount, + UserAvatarURL: rawItem.Data.Author.AvatarURL, + UserLogin: rawItem.Data.Author.Username, + UserDataOrgName: Unknown, + } + + enrichedMergeRequest.Labels = append(enrichedMergeRequest.Labels, rawItem.Data.Labels...) + + source := Gitlab + authorUsername := rawItem.Data.Author.Username + authorName := rawItem.Data.Author.Name + authorUUID, err := uuid.GenerateIdentity(&source, nil, &authorName, &authorUsername) + if err != nil { + return nil, err + } + + userData, err := e.affiliationsClientProvider.GetIdentityByUser("id", authorUUID) + if err != nil { + errMessage := fmt.Sprintf("BOOM: %+v : %+v", authorUUID, err) + log.Println(errMessage) + } + + if userData != nil { + if userData.ID != nil { + enrichedMergeRequest.AuthorID = *userData.ID + enrichedMergeRequest.UserDataID = *userData.ID + } + if userData.Name != "" { + enrichedMergeRequest.AuthorName = userData.Name + enrichedMergeRequest.Username = userData.Name + enrichedMergeRequest.UserDataName = userData.Name + } + + if userData.UUID != nil { + enrichedMergeRequest.AuthorUUID = *userData.UUID + enrichedMergeRequest.UserDataUUID = *userData.UUID + } + + if userData.Domain != "" { + enrichedMergeRequest.AuthorDomain = userData.Domain + enrichedMergeRequest.UserDataDomain = userData.Domain + } + + if userData.UUID != nil { + slug := rawItem.ProjectSlug + enrollments := e.affiliationsClientProvider.GetOrganizations(*userData.UUID, slug) + if enrollments != nil { + metaDataEpochMills := enrichedMergeRequest.MetadataUpdatedOn.UnixNano() / 1000000 + organizations := make([]string, 0) + for _, enrollment := range *enrollments { + organizations = append(organizations, enrollment.Organization.Name) + } + + foundEnrollment := false + for _, enrollment := range *enrollments { + affStartEpoch := enrollment.Start.UnixNano() / 1000000 + affEndEpoch := enrollment.End.UnixNano() / 1000000 + if affStartEpoch <= metaDataEpochMills && affEndEpoch >= metaDataEpochMills { + enrichedMergeRequest.AuthorOrgName = enrollment.Organization.Name + enrichedMergeRequest.UserDataOrgName = enrollment.Organization.Name + foundEnrollment = true + break + } + } + + if len(organizations) != 0 { + enrichedMergeRequest.AuthorMultiOrgNames = organizations + enrichedMergeRequest.UserDataMultiOrgNames = organizations + } + + if !foundEnrollment && len(organizations) >= 1 { + enrichedMergeRequest.AuthorOrgName = organizations[0] + enrichedMergeRequest.UserDataOrgName = organizations[0] + } + } + + } + + if userData.IsBot != nil { + if *userData.IsBot == 1 { + enrichedMergeRequest.AuthorBot = true + enrichedMergeRequest.UserDataBot = true + } + } + } else { + userIdentity := affiliation.Identity{ + LastModified: now, + Name: authorName, + Source: source, + Username: authorUsername, + ID: authorUUID, + } + + if ok := e.affiliationsClientProvider.AddIdentity(&userIdentity); !ok { + log.Printf("failed to add identity for [%+v]", authorUsername) + } + + enrichedMergeRequest.AuthorID = authorUUID + enrichedMergeRequest.AuthorUUID = authorUUID + enrichedMergeRequest.AuthorName = authorName + + enrichedMergeRequest.UserDataID = authorUUID + enrichedMergeRequest.UserDataUUID = authorUUID + enrichedMergeRequest.UserDataName = authorName + } + + return &enrichedMergeRequest, nil +} + +func getProjectShortname(repoURL string) (projectURL string) { + repoInChunks := strings.Split(repoURL, "/") + + return repoInChunks[len(repoInChunks)-1] +} + +func getIssueURLID(repo string, issueID int) (urlID string) { + u, err := url.Parse(repo) + if err != nil { + fmt.Println("URL Parsing Error:", err) + } + + path := strings.TrimLeft(u.Path, "/") + urlID = fmt.Sprintf("%s/%s", path, strconv.Itoa(issueID)) + + return urlID +} + +func getIssueRepoShort(repo string) (projectURL string) { + u, err := url.Parse(repo) + if err != nil { + fmt.Println("URL Parsing Error:", err) + } + + return strings.TrimLeft(u.Path, "/") +} diff --git a/gitlab/fetcher.go b/gitlab/fetcher.go new file mode 100644 index 0000000..d82a23c --- /dev/null +++ b/gitlab/fetcher.go @@ -0,0 +1,279 @@ +package gitlab + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/LF-Engineering/dev-analytics-libraries/elastic" + timeLib "github.com/LF-Engineering/dev-analytics-libraries/time" + "github.com/LF-Engineering/dev-analytics-libraries/uuid" + jsoniter "github.com/json-iterator/go" +) + +// ESClientProvider ... +type ESClientProvider interface { + Add(index string, documentID string, body []byte) ([]byte, error) + CreateIndex(index string, body []byte) ([]byte, error) + Bulk(body []byte) ([]byte, error) + Get(index string, query map[string]interface{}, result interface{}) (err error) + GetStat(index string, field string, aggType string, mustConditions []map[string]interface{}, mustNotConditions []map[string]interface{}) (result time.Time, err error) + ReadWithScroll(index string, query map[string]interface{}, result interface{}, scrollID string) (err error) + BulkInsert(data []elastic.BulkData) ([]byte, error) +} + +// HTTPClientProvider ... +type HTTPClientProvider interface { + Request(url string, method string, header map[string]string, body []byte, params map[string]string) (statusCode int, resBody []byte, err error) + RequestWithHeaders(url string, method string, header map[string]string, body []byte, params map[string]string) (statusCode int, resBody []byte, resHeaders map[string][]string, err error) +} + +// FetcherParams ... +type FetcherParams struct { + BackendVersion string + Project string + ProjectSlug string + Origin string + Repo string + Token string +} + +// Fetcher ... +type Fetcher struct { + HTTPClientProvider HTTPClientProvider + ElasticSearchProvider ESClientProvider + BackendVersion string + DSName string + Project string + ProjectSlug string + Origin string + Repo string + Token string +} + +// NewFetcher ... +func NewFetcher(params *FetcherParams, httpClientProvider HTTPClientProvider, esClientProvider ESClientProvider) *Fetcher { + return &Fetcher{ + HTTPClientProvider: httpClientProvider, + ElasticSearchProvider: esClientProvider, + BackendVersion: params.BackendVersion, + Project: params.Project, + ProjectSlug: params.ProjectSlug, + Origin: params.Origin, + DSName: Gitlab, + Repo: params.Repo, + Token: params.Token, + } +} + +// FetchMergeRequests ... +func (f *Fetcher) FetchMergeRequests(projectID string, lastDate time.Time) ([]MergeRequestRaw, error) { + var ( + rawAry = make([]MergeRequestRaw, 0) + mergeRequestResponses []MergeRequestData + ) + lastDateISO := lastDate.Format(time.RFC3339) + + gitlabURL := fmt.Sprintf("%s/%s/projects/%s/merge_requests?per_page=100&updated_after=%s", GitlabAPIBase, GitlabAPIVersion, projectID, lastDateISO) + + headers := map[string]string{ + "PRIVATE-TOKEN": f.Token, + } + resStatus, resBody, resHeaders, err := f.HTTPClientProvider.RequestWithHeaders(gitlabURL, "GET", headers, nil, nil) + if err != nil { + return nil, err + } + + if resStatus == http.StatusOK { + err = jsoniter.Unmarshal(resBody, &mergeRequestResponses) + if err != nil { + return nil, err + } + } + + linkData := resHeaders["Link"] + next := getNextLink(linkData) + + for next != "" { + resStatus, resBody, resHeaders, err = f.HTTPClientProvider.RequestWithHeaders(next, "GET", headers, nil, nil) + + if err != nil { + return nil, err + } + + var nextMergeRequestResponses []MergeRequestData + if resStatus == http.StatusOK { + err = jsoniter.Unmarshal(resBody, &nextMergeRequestResponses) + if err != nil { + return nil, err + } + mergeRequestResponses = append(mergeRequestResponses, nextMergeRequestResponses...) + } + + linkData = resHeaders["Link"] + next = getNextLink(linkData) + + if next == "" { + break + } + } + + for _, mergeRequest := range mergeRequestResponses { + var raw MergeRequestRaw + raw.Data = mergeRequest + raw.Data.Type = "merge_request" + raw.MetadataUpdatedOn = mergeRequest.UpdatedAt + raw.MetadataTimestamp = time.Now() + raw.Timestamp = timeLib.ConvertTimeToFloat(raw.MetadataTimestamp) + raw.BackendVersion = f.BackendVersion + raw.BackendName = f.DSName + raw.Project = f.Project + raw.ProjectSlug = f.ProjectSlug + raw.Repo = f.Repo + + mergeRequestURL := fmt.Sprintf("%s/projects/merge_request/%s", GitlabAPIBase, projectID) + uuid, err := uuid.Generate(mergeRequestURL, strconv.Itoa(mergeRequest.MergeRequestID)) + if err != nil { + return nil, err + } + raw.UUID = uuid + + rawAry = append(rawAry, raw) + } + + return rawAry, nil +} + +// FetchIssues ... +func (f *Fetcher) FetchIssues(projectID string, lastDate time.Time) ([]IssueRaw, error) { + var ( + rawAry = make([]IssueRaw, 0) + issueResponses []IssueData + ) + + lastDateISO := lastDate.Format(time.RFC3339) + gitlabURL := fmt.Sprintf("%s/%s/projects/%s/issues?per_page=100&updated_after=%s", GitlabAPIBase, GitlabAPIVersion, projectID, lastDateISO) + + headers := map[string]string{ + "PRIVATE-TOKEN": f.Token, + } + resStatus, resBody, resHeaders, err := f.HTTPClientProvider.RequestWithHeaders(gitlabURL, "GET", headers, nil, nil) + if err != nil { + return nil, err + } + + if resStatus == http.StatusOK { + err = jsoniter.Unmarshal(resBody, &issueResponses) + if err != nil { + return nil, err + } + } + + linkData := resHeaders["Link"] + next := getNextLink(linkData) + + for next != "" { + resStatus, resBody, resHeaders, err = f.HTTPClientProvider.RequestWithHeaders(next, "GET", headers, nil, nil) + + if err != nil { + return nil, err + } + + var nextIssueResponses []IssueData + if resStatus == http.StatusOK { + err = jsoniter.Unmarshal(resBody, &nextIssueResponses) + if err != nil { + return nil, err + } + issueResponses = append(issueResponses, nextIssueResponses...) + } + + linkData = resHeaders["Link"] + next = getNextLink(linkData) + + if next == "" { + break + } + } + + for _, issue := range issueResponses { + var raw IssueRaw + raw.Data = issue + raw.MetadataUpdatedOn = issue.UpdatedAt + raw.MetadataTimestamp = time.Now() + raw.Timestamp = timeLib.ConvertTimeToFloat(raw.MetadataTimestamp) + raw.BackendVersion = f.BackendVersion + raw.BackendName = f.DSName + raw.Project = f.Project + raw.ProjectSlug = f.ProjectSlug + raw.Repo = f.Repo + + issueURL := fmt.Sprintf("%s/projects/issue/%s", GitlabAPIBase, projectID) + uuid, err := uuid.Generate(issueURL, strconv.Itoa(issue.IssueID)) + if err != nil { + return nil, err + } + + raw.UUID = uuid + rawAry = append(rawAry, raw) + } + + return rawAry, nil +} + +func getNextLink(link []string) (next string) { + linkString := link[0] + allLinks := strings.Split(linkString, ",") + + for _, i := range allLinks { + linkAry := strings.Split(strings.TrimSpace(i), " ") + desc := strings.TrimSpace(linkAry[1]) + + if desc == "rel=\"next\"" { + next = strings.Trim(linkAry[0], "<>;") + } + } + return +} + +func (f *Fetcher) getProjectID(repo string) (projectID string, err error) { + u, err := url.Parse(repo) + if err != nil { + return "", err + } + + encodedPath := url.QueryEscape(strings.TrimLeft(u.Path, "/")) + projectURL := fmt.Sprintf("%s/%s/projects/%s", GitlabAPIBase, GitlabAPIVersion, encodedPath) + + headers := map[string]string{ + "PRIVATE-TOKEN": f.Token, + } + + resStatus, resBody, err := f.HTTPClientProvider.Request(projectURL, "GET", headers, nil, nil) + if err != nil { + return "", err + } + + var projectResponse Project + if resStatus == http.StatusOK { + err = jsoniter.Unmarshal(resBody, &projectResponse) + if err != nil { + return "", err + } + } + + return strconv.Itoa(projectResponse.ID), nil +} + +// GetLastFetchDate ... +func (f *Fetcher) GetLastFetchDate(index string) time.Time { + lastDate, err := f.ElasticSearchProvider.GetStat(index, "metadata__updated_on", "max", nil, nil) + if err != nil { + return DefaultDateTime + } + + return lastDate +} diff --git a/gitlab/manager.go b/gitlab/manager.go new file mode 100644 index 0000000..7fa33f6 --- /dev/null +++ b/gitlab/manager.go @@ -0,0 +1,393 @@ +package gitlab + +import ( + "fmt" + "strconv" + "time" + + "github.com/labstack/gommon/log" + + "github.com/LF-Engineering/da-ds/build" + "github.com/LF-Engineering/dev-analytics-libraries/affiliation" + "github.com/LF-Engineering/dev-analytics-libraries/auth0" + "github.com/LF-Engineering/dev-analytics-libraries/elastic" + "github.com/LF-Engineering/dev-analytics-libraries/http" + "github.com/LF-Engineering/dev-analytics-libraries/slack" +) + +// Manager ... +type Manager struct { + HTTPClientProvider HTTPClientProvider + HTTPTimeout time.Duration + ESUrl string + ESUsername string + ESPassword string + ESCacheURL string + ESCacheUsername string + ESCachePassword string + ESIndex string + ESBulkSize int + AuthGrantType string + AuthClientID string + AuthClientSecret string + AuthAudience string + Auth0URL string + Environment string + WebHookURL string + AffBaseURL string + ProjectSlug string + Project string + Repo string + Fetch bool + Enrich bool + Token string +} + +// MgrParams ... +type MgrParams struct { + HTTPClientProvider HTTPClientProvider + HTTPTimeout time.Duration + ESUrl string + ESUsername string + ESPassword string + ESCacheURL string + ESCacheUsername string + ESCachePassword string + ESIndex string + ESBulkSize int + AuthGrantType string + AuthClientID string + AuthClientSecret string + AuthAudience string + Auth0URL string + Environment string + AffBaseURL string + ProjectSlug string + Project string + Repo string + Fetch bool + Enrich bool + Token string +} + +// NewManager initiates bugzilla manager instance +func NewManager(param *MgrParams) (*Manager, error) { + mgr := &Manager{ + HTTPTimeout: param.HTTPTimeout, + ESUrl: param.ESUrl, + ESUsername: param.ESUsername, + ESPassword: param.ESPassword, + ESCacheURL: param.ESCacheURL, + ESCacheUsername: param.ESCacheUsername, + ESCachePassword: param.ESCachePassword, + ESIndex: param.ESIndex, + ESBulkSize: param.ESBulkSize, + AuthGrantType: param.AuthGrantType, + AuthClientID: param.AuthClientID, + AuthClientSecret: param.AuthClientSecret, + AuthAudience: param.AuthAudience, + Auth0URL: param.Auth0URL, + Environment: param.Environment, + AffBaseURL: param.AffBaseURL, + ProjectSlug: param.ProjectSlug, + Project: param.Project, + Repo: param.Repo, + Fetch: param.Fetch, + Enrich: param.Enrich, + Token: param.Token, + } + + return mgr, nil +} + +func buildServices(m *Manager) (*Fetcher, *Enricher, ESClientProvider, error) { + httpClientProvider := http.NewClientProvider(m.HTTPTimeout) + params := &FetcherParams{ + BackendVersion: "0.0.1", + Project: m.Project, + ProjectSlug: m.ProjectSlug, + Repo: m.Repo, + Token: m.Token, + } + + esClientProvider, err := elastic.NewClientProvider(&elastic.Params{ + URL: m.ESUrl, + Username: m.ESUsername, + Password: m.ESPassword, + }) + + if err != nil { + return nil, nil, nil, err + } + + esCacheClientProvider, err := elastic.NewClientProvider(&elastic.Params{ + URL: m.ESCacheURL, + Username: m.ESCacheUsername, + Password: m.ESCachePassword, + }) + + if err != nil { + return nil, nil, nil, err + } + + slackProvider := slack.New(m.WebHookURL) + appNameVersion := fmt.Sprintf("%s-%v", build.AppName, strconv.FormatInt(time.Now().Unix(), 10)) + auth0Client, err := auth0.NewAuth0Client( + m.Environment, + m.AuthGrantType, + m.AuthClientID, + m.AuthClientSecret, + m.AuthAudience, + m.Auth0URL, + httpClientProvider, + esCacheClientProvider, + &slackProvider, + appNameVersion) + + if err != nil { + return nil, nil, nil, err + } + + affiliationsClientProvider, err := affiliation.NewAffiliationsClient(m.AffBaseURL, m.ProjectSlug, httpClientProvider, esCacheClientProvider, auth0Client, &slackProvider) + if err != nil { + return nil, nil, nil, err + } + + fetcher := NewFetcher(params, httpClientProvider, esClientProvider) + enricher := NewEnricher(esClientProvider, affiliationsClientProvider) + + return fetcher, enricher, esClientProvider, nil +} + +// Sync ... +func (m *Manager) Sync() error { + issueIndex := fmt.Sprintf("%s-issue", m.ESIndex) + mergeRequestIndex := fmt.Sprintf("%s-pull_request", m.ESIndex) + fetcher, enricher, esClientProvider, err := buildServices(m) + if err != nil { + return err + } + + var ( + rawIssues []IssueRaw + rawMergeRequests []MergeRequestRaw + ) + + if m.Fetch { + issueData := make([]elastic.BulkData, 0) + mergeRequestData := make([]elastic.BulkData, 0) + + if m.Repo == "" { + return fmt.Errorf("GITLAB: Repo URL cannot be empty") + } + + // Get gitlab project id from the repo url + projID, err := fetcher.getProjectID(m.Repo) + if err != nil { + return fmt.Errorf("GITLAB: Failed to get projectID for %s", m.Repo) + } + + // ISSUES + // Get last fetch date + lastFetchDateIssue := fetcher.GetLastFetchDate(fmt.Sprintf("%s-raw", issueIndex)) + log.Printf("GITLAB: Fetching Issue Data for %+v since %+v", m.Repo, lastFetchDateIssue) + + rawIssues, err = fetcher.FetchIssues(projID, lastFetchDateIssue) + if err != nil { + return fmt.Errorf("GITLAB: Could not fetch issue data from project: %s", m.Repo) + } + + for _, issueRaw := range rawIssues { + issueData = append(issueData, elastic.BulkData{IndexName: fmt.Sprintf("%s-raw", issueIndex), ID: issueRaw.UUID, Data: issueRaw}) + } + + // Merge Requests + // Get last fetch date + lastFetchDatePR := fetcher.GetLastFetchDate(fmt.Sprintf("%s-raw", mergeRequestIndex)) + log.Printf("GITLAB: Fetching Pull Request Data for %+v since %+v", m.Repo, lastFetchDatePR) + + rawMergeRequests, err = fetcher.FetchMergeRequests(projID, lastFetchDatePR) + if err != nil { + return fmt.Errorf("GITLAB: Could not fetch Pull Request data from project: %s", m.Repo) + } + + for _, mergeRequestRaw := range rawMergeRequests { + mergeRequestData = append(mergeRequestData, elastic.BulkData{IndexName: fmt.Sprintf("%s-raw", mergeRequestIndex), ID: mergeRequestRaw.UUID, Data: mergeRequestRaw}) + } + + // Push to Index + _, err = fetcher.ElasticSearchProvider.CreateIndex(fmt.Sprintf("%s-raw", issueIndex), GitlabRawMapping) + if err != nil { + return fmt.Errorf("GITLAB: Could not create raw index - %s-raw", m.ESIndex) + } + + _, err = fetcher.ElasticSearchProvider.CreateIndex(fmt.Sprintf("%s-raw", mergeRequestIndex), GitlabRawMapping) + if err != nil { + return fmt.Errorf("GITLAB: Could not create raw index - %s-raw", m.ESIndex) + } + + if len(issueData) > 0 { + log.Infof("GITLAB: Exporting fetched issues to raw indices") + for start := 0; start < len(issueData); start += m.ESBulkSize { + end := start + m.ESBulkSize + if end > len(issueData) { + end = len(issueData) + } + batch := issueData[start:end] + _, err := esClientProvider.BulkInsert(batch) + if err != nil { + return fmt.Errorf("GITLAB: Error while BulkInsert to %s-raw", issueIndex) + } + log.Printf("GITLAB: Exported %d documents to raw index\n", len(batch)) + } + log.Infof("GITLAB: Done exporting issues to raw indices") + } + + if len(mergeRequestData) > 0 { + log.Infof("GITLAB: Exporting fetched merge requests to raw indices") + for start := 0; start < len(mergeRequestData); start += m.ESBulkSize { + end := start + m.ESBulkSize + if end > len(mergeRequestData) { + end = len(mergeRequestData) + } + batch := mergeRequestData[start:end] + _, err := esClientProvider.BulkInsert(batch) + if err != nil { + return fmt.Errorf("GITLAB: Error while BulkInsert to %s-raw", mergeRequestIndex) + } + log.Printf("GITLAB: Exported %d documents to raw index\n", len(batch)) + } + log.Infof("GITLAB: Done exporting merge requests to raw indices") + } + } + + if m.Enrich { + log.Printf("GITLAB: Enriching issues data for %+v", m.Repo) + issueData := make([]elastic.BulkData, 0) + mergeRequestData := make([]elastic.BulkData, 0) + + for _, issueRaw := range rawIssues { + issueEnriched, err := enricher.EnrichIssue(issueRaw, time.Now().UTC()) + if err != nil { + return fmt.Errorf("GITLAB: Could not fetch data from project: %s", m.Repo) + } + issueData = append(issueData, elastic.BulkData{IndexName: issueIndex, ID: issueEnriched.ID, Data: issueEnriched}) + } + + log.Printf("GITLAB: Enriching merge requests data for %+v", m.Repo) + for _, mergeRequestRaw := range rawMergeRequests { + mergeRequestEnriched, err := enricher.EnrichMergeRequest(mergeRequestRaw, time.Now().UTC()) + if err != nil { + return fmt.Errorf("GITLAB: Could not fetch data from project: %s", m.Repo) + } + mergeRequestData = append(mergeRequestData, elastic.BulkData{IndexName: mergeRequestIndex, ID: mergeRequestEnriched.ID, Data: mergeRequestEnriched}) + } + + // Push to Index + _, err = enricher.ElasticSearchProvider.CreateIndex(m.ESIndex, GitlabRichMapping) + if err != nil { + return fmt.Errorf("GITLAB: Could not create rich index - %s", m.ESIndex) + } + + if len(issueData) > 0 { + log.Infof("GITLAB: Exporting enriched issues to rich index") + for start := 0; start < len(issueData); start += m.ESBulkSize { + end := start + m.ESBulkSize + if end > len(issueData) { + end = len(issueData) + } + batch := issueData[start:end] + _, err := esClientProvider.BulkInsert(batch) + if err != nil { + return fmt.Errorf("GITLAB: Error while BulkInsert to %s", m.ESIndex) + } + log.Printf("GITLAB: Exported %d documents to rich index\n", len(batch)) + } + log.Infof("GITLAB: Done exporting issues to rich indices") + } + + if len(mergeRequestData) > 0 { + log.Infof("GITLAB: Exporting enriched merge requests to rich index") + for start := 0; start < len(mergeRequestData); start += m.ESBulkSize { + end := start + m.ESBulkSize + if end > len(mergeRequestData) { + end = len(mergeRequestData) + } + batch := mergeRequestData[start:end] + _, err := esClientProvider.BulkInsert(batch) + if err != nil { + return fmt.Errorf("GITLAB: Error while BulkInsert to %s", m.ESIndex) + } + log.Printf("GITLAB: Exported %d documents to rich index\n", len(batch)) + } + log.Infof("GITLAB: Done exporting merge requests to rich indices") + } + + } + + return nil +} + +// func (m *Manager) Runit() { +// fmt.Println("let's do this") +// fetcher, enricher, esClientProvider, err := buildServices(m) + +// //projID, err := fetcher.getProjectID("https://gitlab.com/neho-systems/test") +// projID, err := fetcher.getProjectID(m.Repo) +// fmt.Println("Got the project ID", projID) + +// rawData, _ := fetcher.FetchIssues(projID, time.Now()) +// //rawData, _ := fetcher.FetchMergeRequests("https://gitlab.com/api") + +// rawdata := make([]elastic.BulkData, 0) +// enricheddata := make([]elastic.BulkData, 0) +// for _, issueRaw := range rawData { +// rawdata = append(rawdata, elastic.BulkData{IndexName: fmt.Sprintf("%s-raw", m.ESIndex), ID: issueRaw.UUID, Data: issueRaw}) + +// // enrich data +// issueEnriched, err := enricher.EnrichIssue(issueRaw, time.Now().UTC()) +// if err != nil { +// fmt.Println("enrichment do yawa") +// } +// enricheddata = append(enricheddata, elastic.BulkData{IndexName: m.ESIndex, ID: issueEnriched.ID, Data: issueEnriched}) +// } + +// _, err = fetcher.ElasticSearchProvider.CreateIndex(fmt.Sprintf("%s-raw", m.ESIndex), GitlabRawMapping) +// if err != nil { +// fmt.Println("fetcher index creating", err) +// } +// _, err = enricher.ElasticSearchProvider.CreateIndex(m.ESIndex, GitlabRichMapping) +// if err != nil { +// fmt.Println("enricher index creating", err) +// } + +// for start := 0; start < len(rawdata); start += m.ESBulkSize { +// end := start + m.ESBulkSize +// if end > len(rawdata) { +// end = len(rawdata) +// } +// batch := rawdata[start:end] +// _, err := esClientProvider.BulkInsert(batch) +// if err != nil { +// //log.Println("Error while BulkInsert in Gitlab enrich index: ", err) + +// } +// log.Printf("Exported %d documents", len(batch)) +// } + +// for start := 0; start < len(enricheddata); start += m.ESBulkSize { +// end := start + m.ESBulkSize +// if end > len(enricheddata) { +// end = len(enricheddata) +// } +// batch := enricheddata[start:end] +// _, err := esClientProvider.BulkInsert(batch) +// if err != nil { +// //log.Println("Error while BulkInsert in Gitlab enrich index: ", err) + +// } +// log.Printf("Exported %d documents", len(batch)) +// } + +// fmt.Println(len(rawData)) +// } diff --git a/gitlab/models.go b/gitlab/models.go new file mode 100644 index 0000000..2a4ad05 --- /dev/null +++ b/gitlab/models.go @@ -0,0 +1,229 @@ +package gitlab + +import "time" + +// IssueData ... +type IssueData struct { + ID int `json:"id"` + IssueID int `json:"iid"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ClosedAt *time.Time `json:"closed_at"` + ClosedBy *Author `json:"closed_by"` + Labels []string `json:"labels"` + Assignees []Author `json:"assignees"` + Author Author `json:"author"` + Type string `json:"type"` + UserNotesCount int `json:"user_notes_count"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + WebURL string `json:"web_url"` + ProjectID int `json:"project_id"` +} + +// MergeRequestData ... +type MergeRequestData struct { + ID int `json:"id"` + MergeRequestID int `json:"iid"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ClosedAt *time.Time `json:"closed_at"` + ClosedBy *Author `json:"closed_by"` + MergedBy *Author `json:"merged_by"` + MergedAt *time.Time `json:"merged_at"` + TargetBranch string `json:"target_branch"` + SourceBranch string `json:"source_branch"` + Labels []string `json:"labels"` + Assignees []Author `json:"assignees"` + Reviewers []Author `json:"reviewers"` + Author Author `json:"author"` + UserNotesCount int `json:"user_notes_count"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + WebURL string `json:"web_url"` + Type string `json:"type"` +} + +// Author ... +type Author struct { + ID int `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` +} + +// Project ... +type Project struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// IssueRaw ... +type IssueRaw struct { + BackendName string `json:"backend_name"` + BackendVersion string `json:"backend_version"` + Timestamp float64 `json:"timestamp"` + Origin string `json:"origin"` + UUID string `json:"uuid"` + Project string `json:"project"` + ProjectSlug string `json:"project_slug"` + MetadataUpdatedOn time.Time `json:"metadata__updated_on"` + MetadataTimestamp time.Time `json:"metadata__timestamp"` + Data IssueData `json:"data"` + Repo string `json:"repo"` +} + +// MergeRequestRaw ... +type MergeRequestRaw struct { + BackendName string `json:"backend_name"` + BackendVersion string `json:"backend_version"` + Timestamp float64 `json:"timestamp"` + Origin string `json:"origin"` + UUID string `json:"uuid"` + Project string `json:"project"` + ProjectSlug string `json:"project_slug"` + MetadataUpdatedOn time.Time `json:"metadata__updated_on"` + MetadataTimestamp time.Time `json:"metadata__timestamp"` + Data MergeRequestData `json:"data"` + Repo string `json:"repo"` +} + +// IssueEnrich ... +type IssueEnrich struct { + AuthorName string `json:"author_name"` + AuthorAvatarURL string `json:"author_avatar_url"` + AuthorID string `json:"author_id"` + AuthorUUID string `json:"author_uuid"` + AuthorOrgName string `json:"author_org_name"` + AuthorUserName string `json:"author_user_name"` + AuthorBot bool `json:"author_bot"` + AuthorDomain string `json:"author_domain"` + AuthorLogin string `json:"author_login"` + AuthorMultiOrgNames []string `json:"author_multi_org_names"` + BackendName string `json:"backend_name"` + BackendVersion string `json:"backend_version"` + Title string `json:"title"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + ClosedAt *time.Time `json:"closed_at"` + UpdatedAt time.Time `json:"updated_at"` + URL string `json:"url"` + URLID string `json:"url_id"` + Repository string `json:"repository"` + State string `json:"state"` + Tag string `json:"tag"` + Category string `json:"category"` + Body string `json:"body"` + BodyAnalyzed string `json:"body_analyzed"` + UUID string `json:"uuid"` + NoOfAssignees int `json:"n_assignees"` + NoOfComments int `json:"n_comments"` + NoOfReactions int `json:"n_reactions"` + NoOfTotalComments int `json:"n_total_comments"` + Origin string `json:"origin"` + Project string `json:"project"` + ProjectSlug string `json:"project_slug"` + Labels []string `json:"labels"` + ItemType string `json:"item_type"` + IssueID int `json:"issue_id"` + IsGitlabIssue int `json:"is_gitlab_issue"` + IDInRepo int `json:"id_in_repo"` + ID string `json:"id"` + GitlabRepo string `json:"gitlab_repo"` + Reponame string `json:"repo_name"` + RepoID string `json:"repo_id"` + RepoShortname string `json:"repo_short_name"` + MetadataTimestamp time.Time `json:"metadata__timestamp"` + MetadataEnrichedOn time.Time `json:"metadata__enriched_on"` + MetadataUpdatedOn time.Time `json:"metadata__updated_on"` + UserAvatarURL string `json:"user_avatar_url"` + UserDataBot bool `json:"user_data_bot"` + UserDataDomain string `json:"user_data_domain"` + UserDataID string `json:"user_data_id"` + UserDataMultiOrgNames []string `json:"user_data_multi_org_names"` + UserDataName string `json:"user_data_name"` + UserDataOrgName string `json:"user_data_org_name"` + UserDataUsername string `json:"user_data_user_name"` + UserDataUUID string `json:"user_data_uuid"` + UserDomain string `json:"user_domain"` + UserLocation string `json:"user_location"` + UserLogin string `json:"user_login"` + Username string `json:"user_name"` + UserOrg string `json:"user_org"` +} + +// MergeReqestEnrich ... +type MergeReqestEnrich struct { + AuthorName string `json:"author_name"` + AuthorAvatarURL string `json:"author_avatar_url"` + AuthorID string `json:"author_id"` + AuthorUUID string `json:"author_uuid"` + AuthorOrgName string `json:"author_org_name"` + AuthorUserName string `json:"author_user_name"` + AuthorBot bool `json:"author_bot"` + AuthorDomain string `json:"author_domain"` + AuthorLogin string `json:"author_login"` + AuthorMultiOrgNames []string `json:"author_multi_org_names"` + BackendName string `json:"backend_name"` + BackendVersion string `json:"backend_version"` + Title string `json:"title"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + ClosedAt *time.Time `json:"closed_at"` + MergedAt time.Time `json:"merged_at"` + UpdatedAt time.Time `json:"updated_at"` + Merged bool `json:"merged"` + URL string `json:"url"` + URLID string `json:"url_id"` + Repository string `json:"repository"` + State string `json:"state"` + Tag string `json:"tag"` + Category string `json:"category"` + Body string `json:"body"` + BodyAnalyzed string `json:"body_analyzed"` + UUID string `json:"uuid"` + NoOfAssignees int `json:"n_assignees"` + NoOfComments int `json:"n_comments"` + NoOfReactions int `json:"n_reactions"` + NoOfTotalComments int `json:"n_total_comments"` + NoOfRequestedReviewers int `json:"n_requested_reviewers"` + Origin string `json:"origin"` + Project string `json:"project"` + ProjectSlug string `json:"project_slug"` + Labels []string `json:"labels"` + ItemType string `json:"item_type"` + MergeRequestID int `json:"merge_request_id"` + IsGitlabMergeRequest int `json:"is_gitlab_merge_request"` + MergeRequest bool `json:"merge_request"` + IDInRepo int `json:"id_in_repo"` + ID string `json:"id"` + GitlabRepo string `json:"gitlab_repo"` + Reponame string `json:"repo_name"` + RepoID string `json:"repo_id"` + RepoShortname string `json:"repo_short_name"` + MetadataTimestamp time.Time `json:"metadata__timestamp"` + MetadataEnrichedOn time.Time `json:"metadata__enriched_on"` + MetadataUpdatedOn time.Time `json:"metadata__updated_on"` + UserAvatarURL string `json:"user_avatar_url"` + UserDataBot bool `json:"user_data_bot"` + UserDataDomain string `json:"user_data_domain"` + UserDataID string `json:"user_data_id"` + UserDataMultiOrgNames []string `json:"user_data_multi_org_names"` + UserDataName string `json:"user_data_name"` + UserDataOrgName string `json:"user_data_org_name"` + UserDataUsername string `json:"user_data_user_name"` + UserDataUUID string `json:"user_data_uuid"` + UserDomain string `json:"user_domain"` + UserLocation string `json:"user_location"` + UserLogin string `json:"user_login"` + Username string `json:"user_name"` + UserOrg string `json:"user_org"` +} diff --git a/go.mod b/go.mod index 8dbc4f9..e371423 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,29 @@ module github.com/LF-Engineering/da-ds go 1.15 require ( - github.com/LF-Engineering/dev-analytics-libraries v1.1.17 + github.com/LF-Engineering/dev-analytics-libraries v1.1.22 github.com/PuerkitoBio/goquery v1.6.0 github.com/andybalholm/cascadia v1.2.0 // indirect github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e github.com/go-sql-driver/mysql v1.5.0 github.com/google/go-github/v37 v37.0.0 github.com/google/go-github/v38 v38.1.0 + github.com/jgautheron/usedexports v0.0.0-20151229230600-69abd624f77e // indirect github.com/jmoiron/sqlx v1.2.0 github.com/json-iterator/go v1.1.10 + github.com/kisielk/errcheck v1.6.0 // indirect + github.com/labstack/gommon v0.3.0 github.com/lib/pq v1.9.0 // indirect github.com/mattn/go-sqlite3 v1.14.5 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 - golang.org/x/net v0.0.0-20201216054612-986b41b23924 + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 + golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect + golang.org/x/tools v0.1.8 // indirect google.golang.org/api v0.30.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/h2non/gock.v1 v1.0.16 diff --git a/go.sum b/go.sum index b2ec6c6..57184bb 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/LF-Engineering/dev-analytics-libraries v1.1.17 h1:dlYKoZZQCi/SQGFz2xxRCjUDBZwNQ3we2A8I+fhv9SM= github.com/LF-Engineering/dev-analytics-libraries v1.1.17/go.mod h1:O+9mOX1nf6qGKrZne33F6speSzrGj6+Y1tPF6jh/mcw= +github.com/LF-Engineering/dev-analytics-libraries v1.1.22 h1:2VE3PN/243orhqjadsiJ7olyNSoa+nVzNqjbL5rU6uI= +github.com/LF-Engineering/dev-analytics-libraries v1.1.22/go.mod h1:O+9mOX1nf6qGKrZne33F6speSzrGj6+Y1tPF6jh/mcw= github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94= github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= @@ -201,6 +203,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jgautheron/usedexports v0.0.0-20151229230600-69abd624f77e h1:Mhiko1XY9NKWHKSx0QldXI+9bDia/tpGliSfUvCmmMM= +github.com/jgautheron/usedexports v0.0.0-20151229230600-69abd624f77e/go.mod h1:E+3F4lwcqnFuZ0dhasREi0H1OZOno/5J6jTBWnZH/YE= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -212,6 +216,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.6.0 h1:YTDO4pNy7AUN/021p+JGHycQyYNIyMoenM1YDVK6RlY= +github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -220,12 +226,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -262,12 +271,15 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -304,7 +316,10 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -313,6 +328,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -347,6 +364,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -362,6 +381,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -394,6 +414,11 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -403,6 +428,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -443,9 +471,13 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=