mirror of
https://github.com/gojp/goreportcard.git
synced 2026-01-29 06:49:05 +08:00
BIN
assets/biggopher.png
Normal file
BIN
assets/biggopher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
12
handlers/about.go
Normal file
12
handlers/about.go
Normal file
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
31
main.go
31
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...")
|
||||
|
||||
82
templates/about.html
Normal file
82
templates/about.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Go Report Card | Go project code quality report cards</title>
|
||||
<link rel="stylesheet" href="https://cdn.rawgit.com/jgthms/bulma/master/css/bulma.min.css">
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-58936835-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.header .is-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<!-- Left side -->
|
||||
<div class="header-left">
|
||||
<a class="header-item" href="/">
|
||||
<h3 class="title">Go Report Card</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger menu (on mobile) -->
|
||||
<span class="header-toggle">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</span>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="header-right header-menu">
|
||||
<span class="header-item">
|
||||
<a href="/high_scores">High Scores</a>
|
||||
</span>
|
||||
<span class="header-item">
|
||||
<a href="https://github.com/gojp/goreportcard">Github</a>
|
||||
</span>
|
||||
<span class="header-item is-active">
|
||||
<a href="/about">About</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">About</h1>
|
||||
<p>Go Report Card was initially developed during <a href="http://gophergala.com/blog/gopher/gala/2015/02/03/winners/">Gopher Gala 2015</a>. It is free, open source and run by volunteers. If you feel like there is something that could be improved, we encourage you to <a href="https://github.com/gojp/goreportcard">open an issue or contribute on Github</a>!</p>
|
||||
<hr>
|
||||
<h3 class="subtitle">Credits</h3>
|
||||
<p>The Go gopher on the home page was designed by <a href="http://reneefrench.blogspot.com/">Renée French</a>.</p>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content is-centered">
|
||||
<p>
|
||||
<strong>Go Report Card</strong> by
|
||||
<a href="https://twitter.com/shawnps">Shawn Smith</a> and
|
||||
<a href="https://twitter.com/ironzeb">Herman Schaaf</a>.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/" class="dolink">
|
||||
<img width="15%" src="/assets/do-proudly-hosted.png">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,9 +4,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Go Report Card | A Gopher Gala Hackathon Product</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
|
||||
<title>Go Report Card | Go project code quality report cards</title>
|
||||
<link rel="stylesheet" href="https://cdn.rawgit.com/jgthms/bulma/master/css/bulma.min.css">
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
@@ -18,76 +17,86 @@
|
||||
|
||||
</script>
|
||||
<style>
|
||||
/* Footer section!
|
||||
-------------------------------------------------- */
|
||||
.footer-text {
|
||||
text-align: center;
|
||||
line-height: 4em;
|
||||
}
|
||||
|
||||
/* Sticky footer
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
body {
|
||||
/* Margin bottom by footer height */
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
/* Set the fixed height of the footer here */
|
||||
height: 60px;
|
||||
background-color: #f5f5f5;
|
||||
.header .is-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>High Scores</h1>
|
||||
<p><a href="/">Back</a></p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Name</th>
|
||||
<th>Go Files</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $index, $highScore := .HighScores }}
|
||||
<tr>
|
||||
<td><a href="/report/{{ $highScore.Repo}}">{{ add $index 1 }}</td></a>
|
||||
<td><a href="https://github.com/{{ $highScore.Repo }}" rel="nofollow">{{ $highScore.Repo }}</a></td>
|
||||
<td>{{ $highScore.Files }}</td>
|
||||
<td>{{ formatScore $highScore.Score }}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<h1>Stats</h1>
|
||||
<ul>
|
||||
<li><h4>Reports generated for {{ .Count }} repos.</h4></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p class="footer-text">
|
||||
Made by
|
||||
<a href="https://twitter.com/shawnps">@shawnps</a> and
|
||||
<a href="https://twitter.com/ironzeb">@ironzeb</a>. The Go Gopher was created by <a href="http://reneefrench.blogspot.com/">Renée French</a>
|
||||
<!-- Left side -->
|
||||
<div class="header-left">
|
||||
<a class="header-item" href="/">
|
||||
<h3 class="title">Go Report Card</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger menu (on mobile) -->
|
||||
<span class="header-toggle">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</span>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="header-right header-menu">
|
||||
<span class="header-item is-active">
|
||||
<a href="/high_scores">High Scores</a>
|
||||
</span>
|
||||
<span class="header-item">
|
||||
<a href="https://github.com/gojp/goreportcard">Github</a>
|
||||
</span>
|
||||
<span class="header-item">
|
||||
<a href="/about">About</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">High Scores</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Name</th>
|
||||
<th>Go Files</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $index, $highScore := .HighScores }}
|
||||
<tr>
|
||||
<td><a href="/report/{{ $highScore.Repo}}">{{ add $index 1 }}</td></a>
|
||||
<td><a href="https://{{ $highScore.Repo }}" rel="nofollow">{{ $highScore.Repo }}</a></td>
|
||||
<td>{{ $highScore.Files }}</td>
|
||||
<td>{{ formatScore $highScore.Score }}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<p>
|
||||
Reports generated for <strong>{{ .Count }}</strong> unique repos so far.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content is-centered">
|
||||
<p>
|
||||
<strong>Go Report Card</strong> by
|
||||
<a href="https://twitter.com/shawnps">Shawn Smith</a> and
|
||||
<a href="https://twitter.com/ironzeb">Herman Schaaf</a>.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/" class="dolink">
|
||||
<img width="15%" src="/assets/do-proudly-hosted.png">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.js"></script>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,266 +4,261 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Go Report Card | A Gopher Gala Hackathon Product</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
|
||||
<title>Go Report Card | Go project code quality report cards</title>
|
||||
<link rel="stylesheet" href="https://cdn.rawgit.com/jgthms/bulma/master/css/bulma.min.css">
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
|
||||
ga('create', 'UA-58936835-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
|
||||
</script>
|
||||
<style>
|
||||
/* Page header
|
||||
-------------------------------------------------- */
|
||||
.header {
|
||||
text-align: center;
|
||||
.header .is-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
.header h2 {
|
||||
font-size: 3em;
|
||||
.header .title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.input-row {
|
||||
margin-top: 20px;
|
||||
.hero-content .subtitle {
|
||||
font-size: 1.5em;
|
||||
font-weight: 300;
|
||||
color: rgb(255, 255, 255) !important;
|
||||
text-shadow: 0px 0px 4px rgba(150, 150, 150, 1);
|
||||
}
|
||||
.input-row input.input-box {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
#check_form {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.btn-test {
|
||||
width: 100%;
|
||||
#check_form .input-box {
|
||||
padding: 0.5em;
|
||||
min-width: 350px;
|
||||
}
|
||||
.url label {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
line-height: 46px;
|
||||
font-weight: normal;
|
||||
color: rgb(51, 51, 51);
|
||||
#check_form .btn-test {
|
||||
margin-top: 1em;
|
||||
font-weight: 200;
|
||||
}
|
||||
.url input {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
margin-left: -190px;
|
||||
padding-left: 190px;
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Results section
|
||||
-------------------------------------------------- */
|
||||
.results {
|
||||
width: 100%;
|
||||
.menu.results .percentage {
|
||||
float: right;
|
||||
}
|
||||
.results .head-row {
|
||||
cursor: pointer;
|
||||
.percentage.danger {
|
||||
color: #C61E1E;
|
||||
}
|
||||
.results .cell-description {
|
||||
width: 30%;
|
||||
.percentage.info {
|
||||
}
|
||||
.results .cell-progress-bar {
|
||||
width: 70%;
|
||||
.percentage.warning {
|
||||
color: #C6761E;
|
||||
}
|
||||
.results .files, .results .errors {
|
||||
.percentage.success {
|
||||
color: #4CC61E;
|
||||
}
|
||||
.results-text {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.results-text .huge {
|
||||
font-size: 2em;
|
||||
color: black;
|
||||
}
|
||||
.results-text .badge-col {
|
||||
text-align: right;
|
||||
}
|
||||
.results-text .badge-col img {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.menu-block.is-active {
|
||||
font-weight: bold;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.results-details .files, .results .errors {
|
||||
padding-left: 0;
|
||||
}
|
||||
.results .files .file {
|
||||
.results-details .files .file {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
.results .files .errors .error {
|
||||
.results-details .files .errors .error {
|
||||
list-style-type: none;
|
||||
padding-left: 4em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.results .progress {
|
||||
margin-bottom: 0;
|
||||
.results-details .tool-title {
|
||||
font-size: 1.8em;
|
||||
color: #050505;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.results .details-row:hover {
|
||||
background: inherit;
|
||||
.results-details .percentage {
|
||||
float: right;
|
||||
}
|
||||
.results .perfect {
|
||||
color: #aaa;
|
||||
.results-details .wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.results-text p {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.huge {
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.results .tool-description {
|
||||
color: #666;
|
||||
}
|
||||
.container-update {
|
||||
text-align: right;
|
||||
nav.results.stickytop {
|
||||
position:fixed;
|
||||
top:0;
|
||||
width: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Partials section
|
||||
-------------------------------------------------- */
|
||||
#partials {
|
||||
display: none;
|
||||
@media (max-width: 800px) {
|
||||
nav.results.stickytop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.gopher {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
/* Footer section!
|
||||
-------------------------------------------------- */
|
||||
.footer-text {
|
||||
text-align: center;
|
||||
line-height: 4em;
|
||||
}
|
||||
|
||||
/* Sticky footer
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
body {
|
||||
/* Margin bottom by footer height */
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
/* Set the fixed height of the footer here */
|
||||
height: 60px;
|
||||
background-color: #f5f5f5;
|
||||
@media (min-width: 600px) {
|
||||
.hero.gopher {
|
||||
background-image: url(/assets/biggopher.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: left middle;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="notifications">
|
||||
</div>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<div class="row header">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h2 class="title">Go Report Card</h2>
|
||||
<p class="description">Enter the <strong>Github URL</strong> below to generate a report on the quality of the Go code in the project</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row input-row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<form method="GET" action="/checks" id="check-form">
|
||||
<div class="col-md-9">
|
||||
<p class="url">
|
||||
<label>https://github.com/</label><input name="repo" type="text" class="input-box" placeholder="gojp/kana"/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-primary btn-lg" href="#" role="button" class="btn-test">Test Now</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="row container-gopher">
|
||||
<div class="col-md-12 text-center">
|
||||
<img src="/assets/gopherhat.jpg" class="gopher" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row container-results hidden">
|
||||
<div class="col-md-10 col-md-offset-1 results-text">
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<table class="table table-hover results">
|
||||
</table>
|
||||
<div class="container-update">
|
||||
<!-- Left side -->
|
||||
<div class="header-left">
|
||||
<a class="header-item" href="/">
|
||||
<h3 class="title">Go Report Card</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger menu (on mobile) -->
|
||||
<span class="header-toggle">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</span>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="header-right header-menu">
|
||||
<span class="header-item">
|
||||
<a href="/high_scores">High Scores</a>
|
||||
</span>
|
||||
<span class="header-item">
|
||||
<a href="https://github.com/gojp/goreportcard">Github</a>
|
||||
</span>
|
||||
<span class="header-item">
|
||||
<a href="/about">About</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="hero is-primary is-medium gopher">
|
||||
<div class="hero-content">
|
||||
<div class="container">
|
||||
<h2 class="subtitle">
|
||||
Enter the <strong>go get</strong> path to the project below:
|
||||
</h2>
|
||||
<form method="GET" action="/checks" id="check_form">
|
||||
<div>
|
||||
<p class="url">
|
||||
<input name="repo" type="text" class="input-box" placeholder="github.com/gojp/goreportcard"/>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button btn-test is-large" href="#" role="button">Generate Report</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
</section>
|
||||
|
||||
<section class="section container-results hidden">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p class="footer-text">
|
||||
Made by
|
||||
<a href="https://twitter.com/shawnps">@shawnps</a> and
|
||||
<a href="https://twitter.com/ironzeb">@ironzeb</a>. The Go Gopher was created by <a href="http://reneefrench.blogspot.com/">Renée French</a>.
|
||||
<a href="/high_scores"><strong>High Scores</strong></a>
|
||||
<a href="https://www.digitalocean.com/"><img width="15%" src="/assets/do-proudly-hosted.png"></img></a>
|
||||
</p>
|
||||
<div class="columns results-text">
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-quarter">
|
||||
<nav class="menu results">
|
||||
</nav>
|
||||
<div class="container-update">
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="results-details">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content is-centered">
|
||||
<p>
|
||||
<strong>Go Report Card</strong> by
|
||||
<a href="https://twitter.com/shawnps">Shawn Smith</a> and
|
||||
<a href="https://twitter.com/ironzeb">Herman Schaaf</a>.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/" class="dolink">
|
||||
<img width="15%" src="/assets/do-proudly-hosted.png">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.js"></script>
|
||||
<script id="template-alert" type="text/x-handlebars-template">
|
||||
<div class="alert alert-warning alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<div class="message">
|
||||
{{{message}}}
|
||||
<div class="notification is-warning">
|
||||
<button class="delete"></button>
|
||||
{{{message}}}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script id="template-grade" type="text/x-handlebars-template">
|
||||
<div>
|
||||
<div class="col-md-6">
|
||||
<p><span class="huge">{{grade}}</span> {{gradeMessage grade}}</p>
|
||||
<div class="column">
|
||||
<h1 class="title">Report for <strong>{{repo}}</strong></h1>
|
||||
<p><span class="huge">{{grade}}</span> {{gradeMessage grade}}    Found <strong>{{issues}}</strong> issues across <strong>{{files}}</strong> files</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right"><img src="http://goreportcard.com/badge/{{repo}}"/></div>
|
||||
<div class="col-md-6 text-right">
|
||||
<p>Found <span class="huge">{{issues}}</span> issues across <span class="huge">{{files}}</span> files</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-quarter badge-col"><img src="http://goreportcard.com/badge/{{repo}}"/></div>
|
||||
</script>
|
||||
<script id="template-check" type="text/x-handlebars-template">
|
||||
<tr class="head-row">
|
||||
<td class="cell-description">
|
||||
<a class="menu-block" href="#{{{name}}}">
|
||||
{{{name}}}
|
||||
</td>
|
||||
<td class="cell-progress-bar">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-{{color percentage}}" role="progressbar" aria-valuenow="{{percentage}}" aria-valuemin="0" aria-valuemax="100" style="width: {{percentage}}%;">
|
||||
{{percentage}}%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<span class="percentage {{color percentage}}">{{percentage}}%</span>
|
||||
</a>
|
||||
</script>
|
||||
<script id="template-details" type="text/x-handlebars-template">
|
||||
<tr class="details-row hidden">
|
||||
<td colspan="2">
|
||||
<div class="wrapper">
|
||||
<p class="tool-description">{{{description}}}</p>
|
||||
{{^file_summaries}}
|
||||
<p class="perfect">No problems detected. Good job!</p>
|
||||
{{/file_summaries}}
|
||||
{{#each file_summaries}}
|
||||
<ul class="files">
|
||||
<li class="file">
|
||||
<ul class="errors">
|
||||
<a href="{{this.file_url}}">{{this.filename}}</a>
|
||||
{{#each this.errors}}
|
||||
{{#if line_number}}
|
||||
<li class="error"><a href="{{../../file_url}}#L{{this.line_number}}">Line {{this.line_number}}</a>: {{this.error_string}}</li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</li>
|
||||
<div class="wrapper">
|
||||
<a name="{{{name}}}"></a><h1 class="tool-title">{{{name}}}<span class="percentage {{color percentage}}">{{percentage}}%</span></h1>
|
||||
<p class="tool-description">{{{description}}}</p>
|
||||
{{^file_summaries}}
|
||||
<p class="perfect">No problems detected. Good job!</p>
|
||||
{{/file_summaries}}
|
||||
{{#each file_summaries}}
|
||||
<ul class="files">
|
||||
<li class="file">
|
||||
<ul class="errors">
|
||||
<a href="{{this.file_url}}">{{this.filename}}</a>
|
||||
{{#each this.errors}}
|
||||
{{#if line_number}}
|
||||
<li class="error"><a href="{{../../file_url}}#L{{this.line_number}}">Line {{this.line_number}}</a>: {{this.error_string}}</li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/each}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</li>
|
||||
</ul>
|
||||
{{/each}}
|
||||
</div>
|
||||
</script>
|
||||
<script id="template-lastrefresh" type="text/x-handlebars-template">
|
||||
<p>Last refresh: {{last_refresh}} <a class="refresh-button" href=""><strong>Update now</strong></a></p>
|
||||
<div class="col-md-12 text-right">
|
||||
</div>
|
||||
<p>Last refresh: {{last_refresh}}</p>
|
||||
<br>
|
||||
<p><a class="refresh-button button" href=""><strong>Refresh now</strong></a></p>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
var loading = false;
|
||||
@@ -308,63 +303,38 @@
|
||||
});
|
||||
|
||||
var shrinkHeader = function(){
|
||||
var $header = $(".header");
|
||||
$header.find(".title, .description").slideUp();
|
||||
}
|
||||
|
||||
function spinGopher(){
|
||||
if (!loading) {
|
||||
return false;
|
||||
};
|
||||
var $gopher = $(".gopher"),
|
||||
angle = 360;
|
||||
$({deg: 0}).animate({deg: angle}, {
|
||||
duration: 1000,
|
||||
step: function(now) {
|
||||
$gopher.css({
|
||||
transform: 'rotate(' + now + 'deg)'
|
||||
});
|
||||
},
|
||||
easing: 'linear',
|
||||
always: spinGopher
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideGopher(){
|
||||
$(".container-gopher").hide();
|
||||
}
|
||||
|
||||
function showGopher(){
|
||||
$(".container-gopher").show();
|
||||
spinGopher();
|
||||
var $hero = $("section.hero");
|
||||
$hero.slideUp();
|
||||
}
|
||||
|
||||
var populateResults = function(data){
|
||||
var checks = data.checks;
|
||||
var $resultsText = $(".results-text");
|
||||
var $resultsDetails = $('.results-details');
|
||||
$resultsText.html($(templates.grade(data)));
|
||||
|
||||
var $table = $(".table.results");
|
||||
$table.empty();
|
||||
var $table = $(".results");
|
||||
$table.html('<p class="menu-heading">Results</p>');
|
||||
for (var i = 0; i < checks.length; i++) {
|
||||
checks[i].percentage = parseInt(checks[i].percentage * 100.0);
|
||||
var $headRow = $(templates.check(checks[i]));
|
||||
$headRow.on("click", function(){
|
||||
$(this).next(".details-row").toggleClass('hidden');
|
||||
|
||||
$(this).closest("nav").find(".is-active").removeClass("is-active");
|
||||
$(this).toggleClass("is-active");
|
||||
});
|
||||
$headRow.appendTo($table);
|
||||
if (i == 0) {
|
||||
$headRow.toggleClass("is-active");
|
||||
}
|
||||
|
||||
var $details = $(templates.details(checks[i]));
|
||||
$details.appendTo($table);
|
||||
$details.appendTo($resultsDetails);
|
||||
}
|
||||
$(".container-results").removeClass('hidden').slideDown();
|
||||
|
||||
$lastRefresh = $(templates.lastrefresh(data));
|
||||
$div = $(".container-update").html($lastRefresh);
|
||||
$div.find("a.refresh-button").on("click", function(e){
|
||||
loadData.call($("form#check-form")[0], false);
|
||||
loadData.call($("form#check_form")[0], false);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
@@ -372,9 +342,9 @@
|
||||
function alertMessage(msg){
|
||||
var html = templates.alert({message: msg});
|
||||
var $alert = $(html);
|
||||
$alert.on("click", function(){
|
||||
$(this).closest(".alert").remove();
|
||||
});
|
||||
$alert.find(".delete").on("click", function(){
|
||||
$(this).closest(".notification").remove();
|
||||
})
|
||||
$alert.hide();
|
||||
$alert.appendTo("#notifications");
|
||||
$alert.slideDown();
|
||||
@@ -386,24 +356,24 @@
|
||||
url = $form.attr("action"),
|
||||
method = $form.attr("method"),
|
||||
data = {};
|
||||
shrinkHeader();
|
||||
hideResults();
|
||||
showGopher();
|
||||
$form.serializeArray().map(function(x){data[x.name] = x.value;});
|
||||
$("#check_form .button").addClass("is-loading");
|
||||
$form.serializeArray().map(function(x){data[x.name] = x.value;});
|
||||
$.ajax({
|
||||
type: getRequest ? "GET" : "POST",
|
||||
url: url,
|
||||
data: data,
|
||||
dataType: "json"
|
||||
}).fail(function(xhr, status, err){;
|
||||
alertMessage("<strong>Oops!</strong> There was an error processing your request: " + xhr.responseText);
|
||||
}).fail(function(xhr, status, err){
|
||||
alertMessage("There was an error processing your request: " + xhr.responseText);
|
||||
}).done(function(data, textStatus, jqXHR){
|
||||
hideGopher();
|
||||
shrinkHeader();
|
||||
populateResults(data);
|
||||
}).always(function(){
|
||||
loading = false;
|
||||
$("#check_form .button").removeClass("is-loading");
|
||||
});
|
||||
history.pushState('', "Go Report Card for " + data.repo + " | A Gopher Gala Hackathon Product", "/report/" + data.repo);
|
||||
history.pushState('', "Go Report Card for " + data.repo + "", "/report/" + data.repo);
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -415,14 +385,24 @@
|
||||
$(function(){
|
||||
|
||||
// handle form submission
|
||||
$("form#check-form").submit(loadData);
|
||||
$("form#check_form").submit(loadData);
|
||||
|
||||
var path = window.location.pathname;
|
||||
if (path.indexOf("/report/") == 0) {
|
||||
repo = path.replace(/^\/report\//, "");
|
||||
$("form#check-form").find("input").val(repo);
|
||||
loadData.call($("form#check-form")[0], true);
|
||||
$("form#check_form").find("input").val(repo);
|
||||
loadData.call($("form#check_form")[0], true);
|
||||
}
|
||||
|
||||
// sticky menu
|
||||
$(window).scroll(function() {
|
||||
if ($(this).scrollTop() >= 180) {
|
||||
$('nav.results').addClass('stickytop');
|
||||
}
|
||||
else {
|
||||
$('nav.results').removeClass('stickytop');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
116
tools/migrate_repo_names.go
Normal file
116
tools/migrate_repo_names.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user