From d2cf3d35258c854d0fd280530e66470f3cdfbf2a Mon Sep 17 00:00:00 2001 From: shawnps Date: Sun, 8 Feb 2015 05:59:35 -0800 Subject: [PATCH] move handlers into separate package, clean up main --- handlers/assets.go | 11 ++ handlers/badge.go | 69 +++++++++++ handlers/check.go | 45 +++++++ handlers/checks.go | 166 ++++++++++++++++++++++++++ handlers/home.go | 15 +++ handlers/report.go | 7 ++ main.go | 285 +-------------------------------------------- 7 files changed, 319 insertions(+), 279 deletions(-) create mode 100644 handlers/assets.go create mode 100644 handlers/badge.go create mode 100644 handlers/check.go create mode 100644 handlers/checks.go create mode 100644 handlers/home.go create mode 100644 handlers/report.go diff --git a/handlers/assets.go b/handlers/assets.go new file mode 100644 index 0000000..2bee464 --- /dev/null +++ b/handlers/assets.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "log" + "net/http" +) + +func AssetsHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Serving " + r.URL.Path[1:]) + http.ServeFile(w, r, r.URL.Path[1:]) +} diff --git a/handlers/badge.go b/handlers/badge.go new file mode 100644 index 0000000..8939fde --- /dev/null +++ b/handlers/badge.go @@ -0,0 +1,69 @@ +package handlers + +import ( + "fmt" + "log" + "net/http" +) + +// Grade represents a grade returned by the server, which is normally +// somewhere between A+ (highest) and F (lowest). +type Grade string + +// The Grade constants below indicate the current available +// grades. +const ( + GradeAPlus Grade = "A+" + GradeA = "A" + GradeB = "B" + GradeC = "C" + GradeD = "D" + GradeE = "E" + GradeF = "F" +) + +// getGrade is a helper for getting the grade for a percentage +func getGrade(percentage float64) Grade { + switch true { + case percentage > 90: + return GradeAPlus + case percentage > 80: + return GradeA + case percentage > 70: + return GradeB + case percentage > 60: + return GradeC + case percentage > 50: + return GradeD + case percentage > 40: + return GradeE + default: + return GradeF + } +} + +func badgeURL(grade Grade) string { + colorMap := map[Grade]string{ + GradeAPlus: "brightgreen", + GradeA: "brightgreen", + GradeB: "yellowgreen", + GradeC: "yellow", + GradeD: "orange", + GradeE: "red", + GradeF: "red", + } + url := fmt.Sprintf("https://img.shields.io/badge/go_report-%s-%s.svg", grade, colorMap[grade]) + return url +} + +func BadgeHandler(w http.ResponseWriter, r *http.Request, org, repo string) { + name := fmt.Sprintf("%s/%s", org, repo) + resp, err := newChecksResp(name, false) + if err != nil { + log.Printf("ERROR: fetching badge for %s: %v", name, err) + http.Redirect(w, r, "https://img.shields.io/badge/go%20report-error-lightgrey.svg", http.StatusTemporaryRedirect) + return + } + + http.Redirect(w, r, badgeURL(resp.Grade), http.StatusTemporaryRedirect) +} diff --git a/handlers/check.go b/handlers/check.go new file mode 100644 index 0000000..1c0242f --- /dev/null +++ b/handlers/check.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gojp/goreportcard/db" + "gopkg.in/mgo.v2/bson" +) + +func CheckHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + repo := r.FormValue("repo") + forceRefresh := r.Method != "GET" // if this is a GET request, try fetch from cached version in mongo first + resp, err := newChecksResp(repo, forceRefresh) + if err != nil { + log.Println("ERROR: ", err) + b, _ := json.Marshal(err) + w.WriteHeader(http.StatusInternalServerError) + w.Write(b) + } + + b, err := json.Marshal(resp) + if err != nil { + log.Println("ERROR: could not marshal json:", err) + http.Error(w, err.Error(), 500) + return + } + w.Write(b) + + // write to mongo + db := db.Mongo{URL: mongoURL, Database: mongoDatabase, CollectionName: mongoCollection} + coll, err := db.Collection() + if err != nil { + log.Println("Failed to get mongo collection: ", err) + } else { + log.Println("Writing to mongo...") + _, err := coll.Upsert(bson.M{"repo": repo}, resp) + if err != nil { + log.Println("Mongo writing error:", err) + } + } +} diff --git a/handlers/checks.go b/handlers/checks.go new file mode 100644 index 0000000..decfefa --- /dev/null +++ b/handlers/checks.go @@ -0,0 +1,166 @@ +package handlers + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" + + "github.com/gojp/goreportcard/check" + "github.com/gojp/goreportcard/db" + "gopkg.in/mgo.v2/bson" +) + +var ( + mongoURL = "mongodb://localhost:27017" + mongoDatabase = "goreportcard" + mongoCollection = "reports" +) + +func getFromCache(repo string) (checksResp, error) { + // try and fetch from mongo + db := db.Mongo{URL: mongoURL, Database: mongoDatabase, CollectionName: mongoCollection} + coll, err := db.Collection() + if err != nil { + return checksResp{}, fmt.Errorf("Failed to get mongo collection during GET: %v", err) + } + resp := checksResp{} + err = coll.Find(bson.M{"repo": repo}).One(&resp) + if err != nil { + return checksResp{}, fmt.Errorf("Failed to fetch %q from mongo: %v", repo, err) + } + + resp.LastRefresh = resp.LastRefresh.UTC() + + return resp, nil +} + +type score struct { + Name string `json:"name"` + Description string `json:"description"` + FileSummaries []check.FileSummary `json:"file_summaries"` + Percentage float64 `json:"percentage"` +} + +type checksResp struct { + Checks []score `json:"checks"` + Average float64 `json:"average"` + Grade Grade `json:"grade"` + Files int `json:"files"` + Issues int `json:"issues"` + Repo string `json:"repo"` + LastRefresh time.Time `json:"last_refresh"` +} + +func orgRepoNames(url string) (string, string) { + dir := strings.TrimSuffix(url, ".git") + split := strings.Split(dir, "/") + org := split[len(split)-2] + repoName := split[len(split)-1] + + return org, repoName +} + +func dirName(url string) string { + org, repoName := orgRepoNames(url) + + return fmt.Sprintf("repos/src/github.com/%s/%s", org, repoName) +} + +func clone(url string) error { + org, _ := orgRepoNames(url) + if err := os.Mkdir(fmt.Sprintf("repos/src/github.com/%s", org), 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("could not create dir: %v", err) + } + dir := dirName(url) + _, err := os.Stat(dir) + if os.IsNotExist(err) { + cmd := exec.Command("timeout", "120", "git", "clone", "--depth", "1", "--single-branch", url, dir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("could not run git clone: %v", err) + } + } else if err != nil { + return fmt.Errorf("could not stat dir: %v", err) + } else { + cmd := exec.Command("git", "-C", dir, "pull") + if err := cmd.Run(); err != nil { + return fmt.Errorf("could not pull repo: %v", err) + } + } + + return nil +} + +func newChecksResp(repo string, forceRefresh bool) (checksResp, error) { + url := repo + if !strings.HasPrefix(url, "https://gojp:gojp@github.com/") { + url = "https://gojp:gojp@github.com/" + url + } + + if !forceRefresh { + resp, err := getFromCache(repo) + if err != nil { + // just log the error and continue + log.Println(err) + } else { + resp.Grade = getGrade(resp.Average * 100) // grade is not stored for some repos, yet + return resp, nil + } + } + + // fetch the repo and grade it + err := clone(url) + if err != nil { + return checksResp{}, fmt.Errorf("Could not clone repo: %v", err) + } + + dir := dirName(url) + filenames, err := check.GoFiles(dir) + if err != nil { + return checksResp{}, fmt.Errorf("Could not get filenames: %v", err) + } + checks := []check.Check{check.GoFmt{Dir: dir, Filenames: filenames}, + check.GoVet{Dir: dir, Filenames: filenames}, + check.GoLint{Dir: dir, Filenames: filenames}, + check.GoCyclo{Dir: dir, Filenames: filenames}, + } + + ch := make(chan score) + for _, c := range checks { + go func(c check.Check) { + p, summaries, err := c.Percentage() + if err != nil { + log.Printf("ERROR: (%s) %v", c.Name(), err) + } + s := score{ + Name: c.Name(), + Description: c.Description(), + FileSummaries: summaries, + Percentage: p, + } + ch <- s + }(c) + } + + resp := checksResp{Repo: repo, + Files: len(filenames), + LastRefresh: time.Now().UTC()} + var avg float64 + var issues = make(map[string]bool) + for i := 0; i < len(checks); i++ { + s := <-ch + resp.Checks = append(resp.Checks, s) + avg += s.Percentage + for _, fs := range s.FileSummaries { + issues[fs.Filename] = true + } + } + + resp.Average = avg / float64(len(checks)) + resp.Issues = len(issues) + resp.Grade = getGrade(resp.Average * 100) + + return resp, nil +} diff --git a/handlers/home.go b/handlers/home.go new file mode 100644 index 0000000..42852dc --- /dev/null +++ b/handlers/home.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "log" + "net/http" +) + +func HomeHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Serving home page") + if r.URL.Path[1:] == "" { + http.ServeFile(w, r, "templates/home.html") + } else { + http.NotFound(w, r) + } +} diff --git a/handlers/report.go b/handlers/report.go new file mode 100644 index 0000000..bea923d --- /dev/null +++ b/handlers/report.go @@ -0,0 +1,7 @@ +package handlers + +import "net/http" + +func ReportHandler(w http.ResponseWriter, r *http.Request, org, repo string) { + http.ServeFile(w, r, "templates/home.html") +} diff --git a/main.go b/main.go index 77b5e5e..873d7de 100644 --- a/main.go +++ b/main.go @@ -1,288 +1,15 @@ package main import ( - "encoding/json" "fmt" "log" "net/http" "os" - "os/exec" "regexp" - "strings" - "time" - "github.com/gojp/goreportcard/check" - "github.com/gojp/goreportcard/db" - "labix.org/v2/mgo/bson" + "github.com/gojp/goreportcard/handlers" ) -var ( - mongoURL = "mongodb://localhost:27017" - mongoDatabase = "goreportcard" - mongoCollection = "reports" -) - -func homeHandler(w http.ResponseWriter, r *http.Request) { - log.Println("Serving home page") - if r.URL.Path[1:] == "" { - http.ServeFile(w, r, "templates/home.html") - } else { - http.NotFound(w, r) - } -} - -func assetsHandler(w http.ResponseWriter, r *http.Request) { - log.Println("Serving " + r.URL.Path[1:]) - http.ServeFile(w, r, r.URL.Path[1:]) -} - -func orgRepoNames(url string) (string, string) { - dir := strings.TrimSuffix(url, ".git") - split := strings.Split(dir, "/") - org := split[len(split)-2] - repoName := split[len(split)-1] - - return org, repoName -} - -func dirName(url string) string { - org, repoName := orgRepoNames(url) - - return fmt.Sprintf("repos/src/github.com/%s/%s", org, repoName) -} - -func clone(url string) error { - org, _ := orgRepoNames(url) - if err := os.Mkdir(fmt.Sprintf("repos/src/github.com/%s", org), 0755); err != nil && !os.IsExist(err) { - return fmt.Errorf("could not create dir: %v", err) - } - dir := dirName(url) - _, err := os.Stat(dir) - if os.IsNotExist(err) { - cmd := exec.Command("timeout", "120", "git", "clone", "--depth", "1", "--single-branch", url, dir) - if err := cmd.Run(); err != nil { - return fmt.Errorf("could not run git clone: %v", err) - } - } else if err != nil { - return fmt.Errorf("could not stat dir: %v", err) - } else { - cmd := exec.Command("git", "-C", dir, "pull") - if err := cmd.Run(); err != nil { - return fmt.Errorf("could not pull repo: %v", err) - } - } - - return nil -} - -type score struct { - Name string `json:"name"` - Description string `json:"description"` - FileSummaries []check.FileSummary `json:"file_summaries"` - Percentage float64 `json:"percentage"` -} - -type checksResp struct { - Checks []score `json:"checks"` - Average float64 `json:"average"` - Grade Grade `json:"grade"` - Files int `json:"files"` - Issues int `json:"issues"` - Repo string `json:"repo"` - LastRefresh time.Time `json:"last_refresh"` -} - -func getFromCache(repo string) (checksResp, error) { - // try and fetch from mongo - db := db.Mongo{URL: mongoURL, Database: mongoDatabase, CollectionName: mongoCollection} - coll, err := db.Collection() - if err != nil { - return checksResp{}, fmt.Errorf("Failed to get mongo collection during GET: %v", err) - } - resp := checksResp{} - err = coll.Find(bson.M{"repo": repo}).One(&resp) - if err != nil { - return checksResp{}, fmt.Errorf("Failed to fetch %q from mongo: %v", repo, err) - } - - resp.LastRefresh = resp.LastRefresh.UTC() - - return resp, nil -} - -// Grade represents a grade returned by the server, which is normally -// somewhere between A+ (highest) and F (lowest). -type Grade string - -// The Grade constants below indicate the current available -// grades. -const ( - GradeAPlus Grade = "A+" - GradeA = "A" - GradeB = "B" - GradeC = "C" - GradeD = "D" - GradeE = "E" - GradeF = "F" -) - -// getGrade is a helper for getting the grade for a percentage -func getGrade(percentage float64) Grade { - switch true { - case percentage > 90: - return GradeAPlus - case percentage > 80: - return GradeA - case percentage > 70: - return GradeB - case percentage > 60: - return GradeC - case percentage > 50: - return GradeD - case percentage > 40: - return GradeE - default: - return GradeF - } -} - -func newChecksResp(repo string, forceRefresh bool) (checksResp, error) { - url := repo - if !strings.HasPrefix(url, "https://gojp:gojp@github.com/") { - url = "https://gojp:gojp@github.com/" + url - } - - if !forceRefresh { - resp, err := getFromCache(repo) - if err != nil { - // just log the error and continue - log.Println(err) - } else { - resp.Grade = getGrade(resp.Average * 100) // grade is not stored for some repos, yet - return resp, nil - } - } - - // fetch the repo and grade it - err := clone(url) - if err != nil { - return checksResp{}, fmt.Errorf("Could not clone repo: %v", err) - } - - dir := dirName(url) - filenames, err := check.GoFiles(dir) - if err != nil { - return checksResp{}, fmt.Errorf("Could not get filenames: %v", err) - } - checks := []check.Check{check.GoFmt{Dir: dir, Filenames: filenames}, - check.GoVet{Dir: dir, Filenames: filenames}, - check.GoLint{Dir: dir, Filenames: filenames}, - check.GoCyclo{Dir: dir, Filenames: filenames}, - } - - ch := make(chan score) - for _, c := range checks { - go func(c check.Check) { - p, summaries, err := c.Percentage() - if err != nil { - log.Printf("ERROR: (%s) %v", c.Name(), err) - } - s := score{ - Name: c.Name(), - Description: c.Description(), - FileSummaries: summaries, - Percentage: p, - } - ch <- s - }(c) - } - - resp := checksResp{Repo: repo, - Files: len(filenames), - LastRefresh: time.Now().UTC()} - var avg float64 - var issues = make(map[string]bool) - for i := 0; i < len(checks); i++ { - s := <-ch - resp.Checks = append(resp.Checks, s) - avg += s.Percentage - for _, fs := range s.FileSummaries { - issues[fs.Filename] = true - } - } - - resp.Average = avg / float64(len(checks)) - resp.Issues = len(issues) - resp.Grade = getGrade(resp.Average * 100) - - return resp, nil -} - -func checkHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - repo := r.FormValue("repo") - forceRefresh := r.Method != "GET" // if this is a GET request, try fetch from cached version in mongo first - resp, err := newChecksResp(repo, forceRefresh) - if err != nil { - log.Println("ERROR: ", err) - b, _ := json.Marshal(err) - w.WriteHeader(http.StatusInternalServerError) - w.Write(b) - } - - b, err := json.Marshal(resp) - if err != nil { - log.Println("ERROR: could not marshal json:", err) - http.Error(w, err.Error(), 500) - return - } - w.Write(b) - - // write to mongo - db := db.Mongo{URL: mongoURL, Database: mongoDatabase, CollectionName: mongoCollection} - coll, err := db.Collection() - if err != nil { - log.Println("Failed to get mongo collection: ", err) - } else { - log.Println("Writing to mongo...") - _, err := coll.Upsert(bson.M{"repo": repo}, resp) - if err != nil { - log.Println("Mongo writing error:", err) - } - } -} - -func reportHandler(w http.ResponseWriter, r *http.Request, org, repo string) { - http.ServeFile(w, r, "templates/home.html") -} - -func badgeURL(grade Grade) string { - colorMap := map[Grade]string{ - GradeAPlus: "brightgreen", - GradeA: "brightgreen", - GradeB: "yellowgreen", - GradeC: "yellow", - GradeD: "orange", - GradeE: "red", - GradeF: "red", - } - url := fmt.Sprintf("https://img.shields.io/badge/go_report-%s-%s.svg", grade, colorMap[grade]) - return url -} - -func badgeHandler(w http.ResponseWriter, r *http.Request, org, repo string) { - name := fmt.Sprintf("%s/%s", org, repo) - resp, err := newChecksResp(name, false) - if err != nil { - log.Printf("ERROR: fetching badge for %s: %v", name, err) - http.Redirect(w, r, "https://img.shields.io/badge/go%20report-error-lightgrey.svg", http.StatusTemporaryRedirect) - return - } - - http.Redirect(w, r, badgeURL(resp.Grade), http.StatusTemporaryRedirect) -} - func makeHandler(name string, fn func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { validPath := regexp.MustCompile(fmt.Sprintf(`^/%s/([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`, name)) @@ -301,11 +28,11 @@ func main() { log.Fatal("ERROR: could not create repos dir: ", err) } - http.HandleFunc("/assets/", assetsHandler) - http.HandleFunc("/checks", checkHandler) - http.HandleFunc("/report/", makeHandler("report", reportHandler)) - http.HandleFunc("/badge/", makeHandler("badge", badgeHandler)) - http.HandleFunc("/", homeHandler) + http.HandleFunc("/assets/", handlers.AssetsHandler) + http.HandleFunc("/checks", handlers.CheckHandler) + http.HandleFunc("/report/", makeHandler("report", handlers.ReportHandler)) + http.HandleFunc("/badge/", makeHandler("badge", handlers.BadgeHandler)) + http.HandleFunc("/", handlers.HomeHandler) fmt.Println("Running on 127.0.01:8080...") log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))