Skip to content
67 changes: 67 additions & 0 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
package main

import (
"fmt"
"sync"
"time"

log "github.com/sirupsen/logrus"
"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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions rules/empty_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions rules/fast-forward-merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
127 changes: 127 additions & 0 deletions rules/go_vendor_folder.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions rules/has_open_issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions rules/issue.go
Original file line number Diff line number Diff line change
@@ -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"}
}
8 changes: 8 additions & 0 deletions rules/last_activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions rules/my_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var MyRegistry = &Registry{

func init() {
MyRegistry.AddRule(NewEmptyRepository())
MyRegistry.AddRule(NewGoVendorFolder())
MyRegistry.AddRule(NewHasOpenIssues())
MyRegistry.AddRule(NewLastActivity())
MyRegistry.AddRule(NewNonFastForwardMerge())
Expand Down
2 changes: 2 additions & 0 deletions rules/ruler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
8 changes: 8 additions & 0 deletions rules/without_gitlab_ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions rules/without_readme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down