diff --git a/assets/biggopher.png b/assets/biggopher.png new file mode 100644 index 0000000..35df82e Binary files /dev/null and b/assets/biggopher.png differ diff --git a/handlers/about.go b/handlers/about.go new file mode 100644 index 0000000..5460ad7 --- /dev/null +++ b/handlers/about.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "log" + "net/http" +) + +// AboutHandler handles the about page +func AboutHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Serving about page") + http.ServeFile(w, r, "templates/about.html") +} diff --git a/handlers/badge.go b/handlers/badge.go index be332c0..338e000 100644 --- a/handlers/badge.go +++ b/handlers/badge.go @@ -57,8 +57,8 @@ func badgeURL(grade Grade) string { } // BadgeHandler handles fetching the badge images -func BadgeHandler(w http.ResponseWriter, r *http.Request, org, repo string) { - name := fmt.Sprintf("%s/%s", org, repo) +func BadgeHandler(w http.ResponseWriter, r *http.Request, repo string) { + name := fmt.Sprintf("%s", repo) resp, err := newChecksResp(name, false) if err != nil { log.Printf("ERROR: fetching badge for %s: %v", name, err) diff --git a/handlers/check.go b/handlers/check.go index 71a85b0..7419758 100644 --- a/handlers/check.go +++ b/handlers/check.go @@ -6,9 +6,10 @@ import ( "fmt" "log" "net/http" - "strings" "time" + "golang.org/x/tools/go/vcs" + "github.com/boltdb/bolt" ) @@ -29,18 +30,30 @@ func CheckHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") repo := r.FormValue("repo") - log.Printf("Checking repo %s...", repo) - if strings.ToLower(repo) == "golang/go" { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("We've decided to omit results for the Go repository because it has lots of test files that (purposely) don't pass our checks. Go gets an A+ in our books though!")) + + repoRoot, err := vcs.RepoRootForImportPath(repo, true) + if err != nil || repoRoot.Root == "" || repoRoot.Repo == "" { + log.Println("Failed to create repoRoot:", repoRoot, err) + b, marshalErr := json.Marshal("Please enter a valid 'go get'-able package name") + if marshalErr != nil { + log.Println("JSON marshal error:", marshalErr) + } + w.WriteHeader(http.StatusBadRequest) + w.Write(b) return } + + log.Printf("Checking repo %q...", repo) + forceRefresh := r.Method != "GET" // if this is a GET request, try to fetch from cached version in boltdb first resp, err := newChecksResp(repo, forceRefresh) if err != nil { - log.Println("ERROR: ", err) - b, _ := json.Marshal(err) - w.WriteHeader(http.StatusInternalServerError) + log.Println("ERROR: from newChecksResp:", err) + b, marshalErr := json.Marshal("Could not go get the repository.") + if marshalErr != nil { + log.Println("JSON marshal error:", marshalErr) + } + w.WriteHeader(http.StatusBadRequest) w.Write(b) return } diff --git a/handlers/checks.go b/handlers/checks.go index 2f6c14b..c4083e1 100644 --- a/handlers/checks.go +++ b/handlers/checks.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" "log" "os" "os/exec" + "path/filepath" "strings" "time" @@ -14,6 +16,10 @@ import ( "github.com/gojp/goreportcard/check" ) +func dirName(repo string) string { + return fmt.Sprintf("repos/src/%s", repo) +} + func getFromCache(repo string) (checksResp, error) { // try and fetch from boltdb db, err := bolt.Open(DBPath, 0600, &bolt.Options{Timeout: 1 * time.Second}) @@ -66,58 +72,51 @@ type checksResp struct { 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) { +func goGet(repo string) error { + log.Println("Go getting", repo, "...") + dir := dirName(repo) + if err := os.Mkdir("repos", 0755); err != nil && !os.IsExist(err) { return fmt.Errorf("could not create dir: %v", err) } - dir := dirName(url) - _, err := os.Stat(dir) + d, err := filepath.Abs("repos") + if err != nil { + return fmt.Errorf("could not fetch absolute path: %v", err) + } + os.Setenv("GOPATH", d) + _, err = os.Stat(dir) if os.IsNotExist(err) { - cmd := exec.Command("git", "clone", "--depth", "1", "--single-branch", url, dir) - if err := cmd.Run(); err != nil { - return fmt.Errorf("could not run git clone: %v", err) + cmd := exec.Command("go", "get", "-d", repo) + cmd.Stdout = os.Stdout + stderr, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("could not get stderr pipe: %v", err) } + err = cmd.Start() + if err != nil { + return fmt.Errorf("could not start command: %v", err) + } + + b, err := ioutil.ReadAll(stderr) + if err != nil { + return fmt.Errorf("could not read stderr: %v", err) + } + + err = cmd.Wait() + // we don't care if there are no buildable Go source files, we just need the source on disk + if err != nil && !strings.Contains(string(b), "no buildable Go source files") { + return fmt.Errorf("could not run go get: %v", err) + } return nil } if err != nil { return fmt.Errorf("could not stat dir: %v", err) } - cmd := exec.Command("git", "-C", dir, "fetch", "origin", "master") - if err := cmd.Run(); err != nil { - return fmt.Errorf("could not fetch master branch: %v", err) - } - cmd = exec.Command("git", "-C", dir, "reset", "--hard", "origin/master") - if err = cmd.Run(); err != nil { - return fmt.Errorf("could not reset origin/master: %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 { @@ -130,12 +129,12 @@ func newChecksResp(repo string, forceRefresh bool) (checksResp, error) { } // fetch the repo and grade it - err := clone(url) + err := goGet(repo) if err != nil { return checksResp{}, fmt.Errorf("could not clone repo: %v", err) } - dir := dirName(url) + dir := dirName(repo) filenames, err := check.GoFiles(dir) if err != nil { return checksResp{}, fmt.Errorf("could not get filenames: %v", err) diff --git a/handlers/checks_test.go b/handlers/checks_test.go index b55069c..b53ee38 100644 --- a/handlers/checks_test.go +++ b/handlers/checks_test.go @@ -6,7 +6,7 @@ var dirNameTests = []struct { url string want string }{ - {"https://github.com/foo/bar", "repos/src/github.com/foo/bar"}, + {"github.com/foo/bar", "repos/src/github.com/foo/bar"}, } func TestDirName(t *testing.T) { diff --git a/handlers/report.go b/handlers/report.go index 19b043c..7fe2fd6 100644 --- a/handlers/report.go +++ b/handlers/report.go @@ -3,6 +3,6 @@ package handlers import "net/http" // ReportHandler handles the report page -func ReportHandler(w http.ResponseWriter, r *http.Request, org, repo string) { +func ReportHandler(w http.ResponseWriter, r *http.Request, repo string) { http.ServeFile(w, r, "templates/home.html") } diff --git a/main.go b/main.go index 2d0e4e3..d483699 100644 --- a/main.go +++ b/main.go @@ -12,9 +12,9 @@ import ( "github.com/gojp/goreportcard/handlers" ) -func makeHandler(name string, fn func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc { +func makeHandler(name string, fn func(http.ResponseWriter, *http.Request, 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)) + validPath := regexp.MustCompile(fmt.Sprintf(`^/%s/([a-zA-Z0-9\-_\/\.]+)$`, name)) m := validPath.FindStringSubmatch(r.URL.Path) @@ -22,14 +22,30 @@ func makeHandler(name string, fn func(http.ResponseWriter, *http.Request, string http.NotFound(w, r) return } - - // catch the special period cases that github does not allow for repos - if m[2] == "." || m[2] == ".." { - http.NotFound(w, r) + if len(m) < 1 || m[1] == "" { + http.Error(w, "Please enter a repository", http.StatusBadRequest) return } - fn(w, r, m[1], m[2]) + repo := m[1] + + // for backwards-compatibility, we must support URLs formatted as + // /report/[org]/[repo] + // and they will be assumed to be github.com URLs. This is because + // at first Go Report Card only supported github.com URLs, and assumed + // took only the org name and repo name as parameters. This is no longer the + // case, but we do not want external links to break. + oldFormat := regexp.MustCompile(fmt.Sprintf(`^/%s/([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`, name)) + m2 := oldFormat.FindStringSubmatch(r.URL.Path) + if m2 != nil { + // old format is being used + repo = "github.com/" + repo + log.Printf("Assuming intended repo is %q, redirecting", repo) + http.Redirect(w, r, fmt.Sprintf("/%s/%s", name, repo), http.StatusMovedPermanently) + return + } + + fn(w, r, repo) } } @@ -68,6 +84,7 @@ func main() { http.HandleFunc("/report/", makeHandler("report", handlers.ReportHandler)) http.HandleFunc("/badge/", makeHandler("badge", handlers.BadgeHandler)) http.HandleFunc("/high_scores/", handlers.HighScoresHandler) + http.HandleFunc("/about/", handlers.AboutHandler) http.HandleFunc("/", handlers.HomeHandler) fmt.Println("Running on 127.0.0.1:8080...") diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..8c6b492 --- /dev/null +++ b/templates/about.html @@ -0,0 +1,82 @@ + + + + + + + Go Report Card | Go project code quality report cards + + + + + +
+
+ + + + + + + + + + + +
+ + High Scores + + + Github + + + About + +
+
+
+
+
+

About

+

Go Report Card was initially developed during Gopher Gala 2015. It is free, open source and run by volunteers. If you feel like there is something that could be improved, we encourage you to open an issue or contribute on Github!

+
+

Credits

+

The Go gopher on the home page was designed by Renée French.

+
+
+ + + diff --git a/templates/high_scores.html b/templates/high_scores.html index fcf8530..c143bbb 100644 --- a/templates/high_scores.html +++ b/templates/high_scores.html @@ -4,9 +4,8 @@ - Go Report Card | A Gopher Gala Hackathon Product - - + Go Report Card | Go project code quality report cards + -
-

High Scores

-

Back

- - - - - - - - - - - {{ range $index, $highScore := .HighScores }} - - - - - - - {{end}} - -
RankNameGo FilesScore
{{ add $index 1 }}{{ $highScore.Repo }}{{ $highScore.Files }}{{ formatScore $highScore.Score }}
-

Stats

- -
- - - - + diff --git a/templates/home.html b/templates/home.html index ad2239d..8e3203b 100644 --- a/templates/home.html +++ b/templates/home.html @@ -4,266 +4,261 @@ - Go Report Card | A Gopher Gala Hackathon Product - - + Go Report Card | Go project code quality report cards +
-
-
-
-
-

Go Report Card

-

Enter the Github URL below to generate a report on the quality of the Go code in the project

-
-
-
-
-
-
-

- -

-
-
- -
-
-
-
-
-
+
-
-
- -
-
-
+
+
+
+

+ Enter the go get path to the project below: +

+
+
+

+ +

+
+
+ +
+
+
- -
+ + + + + diff --git a/tools/migrate_repo_names.go b/tools/migrate_repo_names.go new file mode 100644 index 0000000..4240ae4 --- /dev/null +++ b/tools/migrate_repo_names.go @@ -0,0 +1,116 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "time" + + "github.com/boltdb/bolt" + "github.com/gojp/goreportcard/handlers" +) + +const ( + dbPath string = "goreportcard.db" + repoBucket string = "repos" + metaBucket string = "meta" +) + +func main() { + oldFormat := regexp.MustCompile(`^([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`) + + db, err := bolt.Open(handlers.DBPath, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + log.Fatal(err) + } + defer db.Close() + err = db.Update(func(tx *bolt.Tx) error { + rb := tx.Bucket([]byte(repoBucket)) + if rb == nil { + return fmt.Errorf("repo bucket not found") + } + toDelete := []string{} + rb.ForEach(func(k, v []byte) error { + sk := string(k) + m := oldFormat.FindStringSubmatch(sk) + if m != nil { + err = rb.Put([]byte("github.com/"+sk), v) + if err != nil { + return err + } + toDelete = append(toDelete, string(v)) + } + return nil + }) + for i := range toDelete { + err = rb.Delete([]byte(toDelete[i])) + if err != nil { + return err + } + } + + // finally update the high scores + mb := tx.Bucket([]byte(metaBucket)) + if mb == nil { + return fmt.Errorf("meta bucket not found") + } + + scoreBytes := mb.Get([]byte("scores")) + if scoreBytes == nil { + scoreBytes, _ = json.Marshal([]scoreHeap{}) + } + scores := &scoreHeap{} + json.Unmarshal(scoreBytes, scores) + for i := range *scores { + m := oldFormat.FindStringSubmatch((*scores)[i].Repo) + if m != nil { + (*scores)[i] = scoreItem{ + Repo: "github.com/" + (*scores)[i].Repo, + Score: (*scores)[i].Score, + Files: (*scores)[i].Files, + } + } + } + scoreBytes, err = json.Marshal(scores) + if err != nil { + return err + } + err = mb.Put([]byte("scores"), scoreBytes) + if err != nil { + return err + } + + return nil + }) + if err != nil { + log.Fatal(err) + } +} + +type scoreItem struct { + Repo string `json:"repo"` + Score float64 `json:"score"` + Files int `json:"files"` +} + +// An scoreHeap is a min-heap of ints. +type scoreHeap []scoreItem + +func (h scoreHeap) Len() int { return len(h) } +func (h scoreHeap) Less(i, j int) bool { return h[i].Score < h[j].Score } +func (h scoreHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *scoreHeap) Push(x interface{}) { + // Push and Pop use pointer receivers because they modify the slice's length, + // not just its contents. + *h = append(*h, x.(scoreItem)) +} + +func (h *scoreHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} diff --git a/tools/mongotobolt.go b/tools/mongotobolt.go deleted file mode 100644 index 9d2d5ca..0000000 --- a/tools/mongotobolt.go +++ /dev/null @@ -1,211 +0,0 @@ -package main - -func main() {} - -/* -import ( - "container/heap" - "encoding/json" - "fmt" - "log" - "time" - - "github.com/boltdb/bolt" - "github.com/gojp/goreportcard/check" - "github.com/gojp/goreportcard/handlers" - - "gopkg.in/mgo.v2" -) - -const ( - dbPath string = "goreportcard.db" - repoBucket string = "repos" - metaBucket string = "meta" - - mongoURL = "mongodb://127.0.0.1:27017" - mongoDatabase = "goreportcard" - mongoCollection = "reports" -) - -type Grade string - -type score struct { - Name string `json:"name"` - Description string `json:"description"` - FileSummaries []check.FileSummary `json:"file_summaries"` - Weight float64 `json:"weight"` - 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"` -} - -// initDB opens the bolt database file (or creates it if it does not exist), and creates -// a bucket for saving the repos, also only if it does not exist. -func initDB() error { - db, err := bolt.Open(handlers.DBPath, 0600, &bolt.Options{Timeout: 1 * time.Second}) - if err != nil { - return err - } - defer db.Close() - - err = db.Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucketIfNotExists([]byte(repoBucket)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(metaBucket)) - return err - }) - return err -} - -func main() { - // initialize bolt database - if err := initDB(); err != nil { - log.Fatal("ERROR: could not open bolt db: ", err) - } - - session, err := mgo.Dial(mongoURL) - if err != nil { - log.Fatal("ERROR: could not get collection:", err) - } - defer session.Close() - coll := session.DB(mongoDatabase).C(mongoCollection) - - db, err := bolt.Open(dbPath, 0755, &bolt.Options{Timeout: 1 * time.Second}) - if err != nil { - log.Println("Failed to open bolt database: ", err) - return - } - defer db.Close() - - var repos []checksResp - coll.Find(nil).All(&repos) - - for _, repo := range repos { - fmt.Printf("inserting %q into bolt...\n", repo.Repo) - err = db.Update(func(tx *bolt.Tx) error { - bkt := tx.Bucket([]byte(repoBucket)) - if bkt == nil { - return fmt.Errorf("repo bucket not found") - } - b, err := json.Marshal(repo) - if err != nil { - return err - } - - mb := tx.Bucket([]byte(metaBucket)) - if mb == nil { - return fmt.Errorf("repo bucket not found") - } - updateHighScores(mb, repo, repo.Repo) - - return bkt.Put([]byte(repo.Repo), b) - }) - if err != nil { - log.Println("Bolt writing error:", err) - } - } - - err = db.Update(func(tx *bolt.Tx) error { - mb := tx.Bucket([]byte(metaBucket)) - if mb == nil { - return fmt.Errorf("repo bucket not found") - } - totalInt := len(repos) - total, err := json.Marshal(totalInt) - if err != nil { - return fmt.Errorf("could not marshal total repos count: %v", err) - } - return mb.Put([]byte("total_repos"), total) - }) - if err != nil { - log.Fatal(err) - } -} - -func updateHighScores(mb *bolt.Bucket, resp checksResp, repo string) error { - // check if we need to update the high score list - if resp.Files < 100 { - // only repos with >= 100 files are considered for the high score list - return nil - } - - // start updating high score list - scoreBytes := mb.Get([]byte("scores")) - if scoreBytes == nil { - scoreBytes, _ = json.Marshal([]scoreHeap{}) - } - scores := &scoreHeap{} - json.Unmarshal(scoreBytes, scores) - - heap.Init(scores) - if len(*scores) > 0 && (*scores)[0].Score > resp.Average*100.0 && len(*scores) == 50 { - // lowest score on list is higher than this repo's score, so no need to add, unless - // we do not have 50 high scores yet - return nil - } - // if this repo is already in the list, remove the original entry: - for i := range *scores { - if (*scores)[i].Repo == repo { - heap.Remove(scores, i) - break - } - } - // now we can safely push it onto the heap - heap.Push(scores, scoreItem{ - Repo: repo, - Score: resp.Average * 100.0, - Files: resp.Files, - }) - if len(*scores) > 50 { - // trim heap if it's grown to over 50 - *scores = (*scores)[1:51] - } - scoreBytes, err := json.Marshal(&scores) - if err != nil { - return err - } - err = mb.Put([]byte("scores"), scoreBytes) - if err != nil { - return err - } - - return nil -} - -type scoreItem struct { - Repo string `json:"repo"` - Score float64 `json:"score"` - Files int `json:"files"` -} - -// An scoreHeap is a min-heap of ints. -type scoreHeap []scoreItem - -func (h scoreHeap) Len() int { return len(h) } -func (h scoreHeap) Less(i, j int) bool { return h[i].Score < h[j].Score } -func (h scoreHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } - -func (h *scoreHeap) Push(x interface{}) { - // Push and Pop use pointer receivers because they modify the slice's length, - // not just its contents. - *h = append(*h, x.(scoreItem)) -} - -func (h *scoreHeap) Pop() interface{} { - old := *h - n := len(old) - x := old[n-1] - *h = old[0 : n-1] - return x -} -*/