Merge pull request #68 from gojp/redesign

Redesign
This commit is contained in:
Shawn Smith
2016-02-07 14:24:57 +09:00
13 changed files with 595 additions and 578 deletions

BIN
assets/biggopher.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

12
handlers/about.go Normal file
View 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")
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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
View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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">&times;</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> &nbsp;&nbsp; {{gradeMessage grade}}</p>
<div class="column">
<h1 class="title">Report for <strong>{{repo}}</strong></h1>
<p><span class="huge">{{grade}}</span> &nbsp;&nbsp; {{gradeMessage grade}} &emsp;&emsp; 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
View 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
}

View File

@@ -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
}
*/