diff --git a/collector/collector.go b/collector/collector.go index ed4573e..a5cf5ed 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -4,6 +4,7 @@ package main import ( + "fmt" "sync" "time" @@ -11,6 +12,8 @@ import ( "github.com/spf13/viper" "github.com/xanzy/go-gitlab" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" _ "github.com/globocom/gitlab-lint/config" "github.com/globocom/gitlab-lint/db" @@ -79,6 +82,67 @@ func processRules(rulesList []rules.Rule) error { return nil } +func processIssues(registry *rules.Registry, git *gitlab.Client) error { + dbInstance, err := db.NewMongoSession() + if err != nil { + log.Errorf("[Collector] Error on create mongo session: %v", err) + return err + } + // iterating over matched rules + for _, r := range registry.Rules { + // searching for opened issue for project and rule + pipeline := bson.M{"$and": bson.A{bson.M{"projectId": r.ProjectID}, bson.M{"ruleId": r.RuleID}}} + + issue := &rules.Issue{} + err := dbInstance.Get(issue, pipeline, &options.FindOneOptions{Sort: bson.M{"issueId": -1}}) + + // if any error different than noDocumentFound + if err != nil && err != mongo.ErrNoDocuments { + return err + } + + // if no opened issue found -> create new issue and add to DB + if err != nil && err == mongo.ErrNoDocuments { + title := fmt.Sprintf("[Gitlab-Lint] %s", registry.RulesFn[r.RuleID].GetName()) + description := registry.RulesFn[r.RuleID].GetDescription() + if err := createIssue(r, git, dbInstance, title, description); err != nil { + return err + } + // Opened issue found + } else { + // fetch issue info from gitlab + gitIssue, _, err := git.Issues.GetIssue(issue.ProjectID, issue.IssueID) + if err != nil { + return err + } + // if issue was closed but rule still matched + if gitIssue.State == "closed" { + // Open new issue with Reopened title + title := fmt.Sprintf("[Gitlab-Lint][Reopened] %s", registry.RulesFn[r.RuleID].GetName()) + description := registry.RulesFn[r.RuleID].GetDescription() + // create new issue + if err := createIssue(r, git, dbInstance, title, description); err != nil { + return err + } + } + } + } + + return nil +} + +func createIssue(r rules.Rule, git *gitlab.Client, dbInstance db.DB, title string, description string) error { + createdIssue, _, err := git.Issues.CreateIssue(r.ProjectID, &gitlab.CreateIssueOptions{Title: &title, Description: &description}) + if err != nil { + return err + } + if _, err := dbInstance.Insert(&rules.Issue{ProjectID: r.ProjectID, RuleID: r.RuleID, IssueID: createdIssue.IID, + WebURL: r.WebURL, Title: title, Description: r.Description}); err != nil { + return err + } + return nil +} + func insertStats(r *rules.Registry) error { dbInstance, err := db.NewMongoSession() if err != nil { @@ -191,4 +255,7 @@ func main() { if err := insertStats(rules.MyRegistry); err != nil { log.Errorf("[Collector] Error on insert statistics data: %v", err) } + if err := processIssues(rules.MyRegistry, git); err != nil { + log.Errorf("[Collector] Error on processing issue: %v", err) + } } diff --git a/db/db.go b/db/db.go index a6cd1a1..d6bedb4 100644 --- a/db/db.go +++ b/db/db.go @@ -135,7 +135,7 @@ func (m *mongoCollection) Get(d rules.Queryable, q bson.M, o *options.FindOneOpt log.Debug("[DB] Get...") collection := m.session.Database(m.dbName).Collection(d.GetCollectionName()) ctx, _ := newDBContext() - return collection.FindOne(ctx, q).Decode(d) + return collection.FindOne(ctx, q, o).Decode(d) } func (m mongoCollection) GetAll(d rules.Queryable, filter FindFilter) ([]rules.Queryable, error) { diff --git a/rules/empty_repository.go b/rules/empty_repository.go index 0537716..d1064a9 100644 --- a/rules/empty_repository.go +++ b/rules/empty_repository.go @@ -24,6 +24,14 @@ func (e *EmptyRepository) GetLevel() string { return LevelError } +func (e *EmptyRepository) GetName() string { + return e.Name +} + +func (e *EmptyRepository) GetDescription() string { + return e.Description +} + func NewEmptyRepository() Ruler { e := &EmptyRepository{ Name: "Empty Repository", diff --git a/rules/fast-forward-merge.go b/rules/fast-forward-merge.go index d9eb816..abbd47a 100644 --- a/rules/fast-forward-merge.go +++ b/rules/fast-forward-merge.go @@ -23,6 +23,14 @@ func (w *NonFastForwardMerge) GetLevel() string { return LevelPedantic } +func (e *NonFastForwardMerge) GetName() string { + return e.Name +} + +func (e *NonFastForwardMerge) GetDescription() string { + return e.Description +} + func NewNonFastForwardMerge() Ruler { w := &NonFastForwardMerge{ Name: "Non Fast-forward Merge", diff --git a/rules/go_vendor_folder.go b/rules/go_vendor_folder.go new file mode 100644 index 0000000..1289bb3 --- /dev/null +++ b/rules/go_vendor_folder.go @@ -0,0 +1,127 @@ +// Copyright (c) 2021, Pablo Aguilar +// Licensed under the BSD 3-Clause License + +package rules + +import ( + log "github.com/sirupsen/logrus" + "github.com/xanzy/go-gitlab" +) + +// GoVendorFolder is a rule to verify if a repository has or not the go vendor folder. +// It look at the repository root searching for the `go.mod` file first since a project without +// that file means that it doesn't use go modules. If there's a `go.mod` it'll search for a +// file called `modules.txt` inside a `vendor` folder. That's the pattern the projects that use +// go modules and vendor its dependencies follows. +// If "go.mod" and "vendor/modules.txt" exist this rule will return `true`. +type GoVendorFolder struct { + Description string `json:"description"` + ID string `json:"ruleId"` + Level string `json:"level"` + Name string `json:"name"` +} + +// NewGoVendorFolder returns an instance of GoVendorFolder with its attributes filled +func NewGoVendorFolder() Ruler { + v := &GoVendorFolder{ + Name: "Go Vendor Folder", + Description: "This rule identifies if a repo has the vendor folder for a project that uses go modules", + } + v.ID = v.GetSlug() + v.Level = v.GetLevel() + return v +} + +func (f *GoVendorFolder) Run(c *gitlab.Client, p *gitlab.Project) bool { + if p.EmptyRepo { + return false + } + + hasGoMod, err := f.searchForGoModFile(p.ID, c) + if err != nil { + log.Errorf(`[%s] error searching for "go.mod" file: %s`, f.GetSlug(), err) + return false + } + + if !hasGoMod { + return false + } + + hasGoVendor, err := f.searchGoVendorModulesFile(p.ID, c) + if err != nil { + log.Errorf(`[%s] error searching for "vendor/modules.txt" file: %s`, f.GetSlug(), err) + return false + } + + return hasGoVendor +} + +func (f *GoVendorFolder) searchForGoModFile(projectID int, c *gitlab.Client) (bool, error) { + return f.searchForFile( + projectID, + "go.mod", + "", + c, + ) +} + +func (f *GoVendorFolder) searchGoVendorModulesFile(projectID int, c *gitlab.Client) (bool, error) { + return f.searchForFile( + projectID, + "modules.txt", + "vendor", + c, + ) +} + +func (f *GoVendorFolder) searchForFile( + projectID int, + fileName string, + path string, + c *gitlab.Client, +) (bool, error) { + listOpts := gitlab.ListTreeOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + }, + Recursive: gitlab.Bool(false), + Path: gitlab.String(path), + } + + for { + nodes, resp, err := c.Repositories.ListTree(projectID, &listOpts) + if err != nil { + return false, err + } + + for _, node := range nodes { + if node.Type == "blob" && node.Name == fileName { + return true, nil + } + } + + if resp.CurrentPage >= resp.TotalPages { + break + } + + listOpts.Page = resp.NextPage + } + + return false, nil +} + +func (f *GoVendorFolder) GetSlug() string { + return "go-vendor-folder" +} + +func (f *GoVendorFolder) GetLevel() string { + return LevelWarning +} + +func (e *GoVendorFolder) GetName() string { + return e.Name +} + +func (e *GoVendorFolder) GetDescription() string { + return e.Description +} diff --git a/rules/has_open_issues.go b/rules/has_open_issues.go index 8f8742b..3b4e3c2 100644 --- a/rules/has_open_issues.go +++ b/rules/has_open_issues.go @@ -26,6 +26,14 @@ func (h *HasOpenIssues) GetLevel() string { return LevelPedantic } +func (e *HasOpenIssues) GetName() string { + return e.Name +} + +func (e *HasOpenIssues) GetDescription() string { + return e.Description +} + func NewHasOpenIssues() Ruler { h := &HasOpenIssues{ Name: "Has Open Issues", diff --git a/rules/issue.go b/rules/issue.go new file mode 100644 index 0000000..ce8c107 --- /dev/null +++ b/rules/issue.go @@ -0,0 +1,27 @@ +package rules + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type Issue struct { + ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` + ProjectID int `json:"projectId" bson:"projectId"` + RuleID string `json:"ruleId" bson:"ruleId"` + IssueID int `json:"issueId" bson:"issueId"` + WebURL string `json:"webUrl" bson:"webUrl"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` +} + +type Issues []Issue + +func (i Issue) Cast() Queryable { + return &i +} + +func (i Issue) GetCollectionName() string { + return "issues" +} + +func (i Issue) GetSearchableFields() []string { + return []string{"title", "projectid", "ruleid", "state"} +} diff --git a/rules/last_activity.go b/rules/last_activity.go index fefbae2..c2801d8 100644 --- a/rules/last_activity.go +++ b/rules/last_activity.go @@ -30,6 +30,14 @@ func (l *LastActivity) GetLevel() string { return LevelWarning } +func (e *LastActivity) GetName() string { + return e.Name +} + +func (e *LastActivity) GetDescription() string { + return e.Description +} + func NewLastActivity() Ruler { l := &LastActivity{ Name: "Last Activity > 1 year", diff --git a/rules/my_registry.go b/rules/my_registry.go index a239002..08393da 100644 --- a/rules/my_registry.go +++ b/rules/my_registry.go @@ -11,6 +11,7 @@ var MyRegistry = &Registry{ func init() { MyRegistry.AddRule(NewEmptyRepository()) + MyRegistry.AddRule(NewGoVendorFolder()) MyRegistry.AddRule(NewHasOpenIssues()) MyRegistry.AddRule(NewLastActivity()) MyRegistry.AddRule(NewNonFastForwardMerge()) diff --git a/rules/ruler.go b/rules/ruler.go index 8cdd5f1..51afd4e 100644 --- a/rules/ruler.go +++ b/rules/ruler.go @@ -7,6 +7,8 @@ import "github.com/xanzy/go-gitlab" type Ruler interface { Run(client *gitlab.Client, p *gitlab.Project) bool + GetName() string + GetDescription() string GetSlug() string GetLevel() string } diff --git a/rules/without_gitlab_ci.go b/rules/without_gitlab_ci.go index db0ffd2..a87446c 100644 --- a/rules/without_gitlab_ci.go +++ b/rules/without_gitlab_ci.go @@ -31,6 +31,14 @@ func (w *WithoutGitlabCI) GetLevel() string { return LevelInfo } +func (e *WithoutGitlabCI) GetName() string { + return e.Name +} + +func (e *WithoutGitlabCI) GetDescription() string { + return e.Description +} + func NewWithoutGitlabCI() Ruler { w := &WithoutGitlabCI{ Name: "Without Gitlab CI", diff --git a/rules/without_readme.go b/rules/without_readme.go index 90096b9..426bbe9 100644 --- a/rules/without_readme.go +++ b/rules/without_readme.go @@ -23,6 +23,14 @@ func (w *WithoutReadme) GetLevel() string { return LevelError } +func (e *WithoutReadme) GetName() string { + return e.Name +} + +func (e *WithoutReadme) GetDescription() string { + return e.Description +} + func NewWithoutReadme() Ruler { w := &WithoutReadme{ Name: "Without Readme",