commit 6d37bbb956e2b06a33a5d9c9c85a3aaeac667ba3 Author: Shawn Smith Date: Fri Jan 30 00:35:43 2015 +0900 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c27405f --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +*.swp +repos diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c304d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..171e50b --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Go Report Card + +A web application that generates a report on the quality of an open source go project. It uses several measures, including `gofmt`, `go vet`, `go lint` and `gocyclo`. + +#### Live demo: [http://goreportcard.com](http://goreportcard.com) + +Or here's the result for this repo: [http://goreportcard.com/report/gophergala/go_report](http://goreportcard.com/report/gophergala/go_report) + +![Screenshot of Go Report Card in action](https://cloud.githubusercontent.com/assets/1121616/5891942/a2a38b8e-a4f1-11e4-82e3-29b25137f09b.png) diff --git a/assets/gopherhat.jpg b/assets/gopherhat.jpg new file mode 100644 index 0000000..f34d7b3 Binary files /dev/null and b/assets/gopherhat.jpg differ diff --git a/check/check.go b/check/check.go new file mode 100644 index 0000000..0806221 --- /dev/null +++ b/check/check.go @@ -0,0 +1,9 @@ +package check + +type Check interface { + Name() string + Description() string + // Percentage returns the passing percentage of the check, + // as well as a map of filename to output + Percentage() (float64, []FileSummary, error) +} diff --git a/check/go_vet.go b/check/go_vet.go new file mode 100644 index 0000000..f5e7e47 --- /dev/null +++ b/check/go_vet.go @@ -0,0 +1,20 @@ +package check + +type GoVet struct { + Dir string + Filenames []string +} + +func (g GoVet) Name() string { + return "go_vet" +} + +// Percentage returns the percentage of .go files that pass go vet +func (g GoVet) Percentage() (float64, []FileSummary, error) { + return GoTool(g.Dir, g.Filenames, []string{"go", "tool", "vet"}) +} + +// Description returns the description of go lint +func (g GoVet) Description() string { + return `go vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string.` +} diff --git a/check/gocyclo.go b/check/gocyclo.go new file mode 100644 index 0000000..04bc5de --- /dev/null +++ b/check/gocyclo.go @@ -0,0 +1,25 @@ +package check + +type GoCyclo struct { + Dir string + Filenames []string +} + +func (g GoCyclo) Name() string { + return "gocyclo" +} + +// Percentage returns the percentage of .go files that pass gofmt +func (g GoCyclo) Percentage() (float64, []FileSummary, error) { + return GoTool(g.Dir, g.Filenames, []string{"gocyclo", "-over", "10"}) +} + +// Description returns the description of GoCyclo +func (g GoCyclo) Description() string { + return `Gocyclo calculates cyclomatic complexities of functions in Go source code. + +The cyclomatic complexity of a function is calculated according to the following rules: + +1 is the base complexity of a function ++1 for each 'if', 'for', 'case', '&&' or '||'` +} diff --git a/check/gofmt.go b/check/gofmt.go new file mode 100644 index 0000000..78fd079 --- /dev/null +++ b/check/gofmt.go @@ -0,0 +1,20 @@ +package check + +type GoFmt struct { + Dir string + Filenames []string +} + +func (g GoFmt) Name() string { + return "gofmt" +} + +// Percentage returns the percentage of .go files that pass gofmt +func (g GoFmt) Percentage() (float64, []FileSummary, error) { + return GoTool(g.Dir, g.Filenames, []string{"gofmt", "-s", "-l"}) +} + +// Description returns the description of gofmt +func (g GoFmt) Description() string { + return `Gofmt formats Go programs. We run gofmt -s on your code, where -s is for the "simplify" command` +} diff --git a/check/golint.go b/check/golint.go new file mode 100644 index 0000000..4b9d70c --- /dev/null +++ b/check/golint.go @@ -0,0 +1,20 @@ +package check + +type GoLint struct { + Dir string + Filenames []string +} + +func (g GoLint) Name() string { + return "golint" +} + +// Percentage returns the percentage of .go files that pass golint +func (g GoLint) Percentage() (float64, []FileSummary, error) { + return GoTool(g.Dir, g.Filenames, []string{"golint"}) +} + +// Description returns the description of go lint +func (g GoLint) Description() string { + return `Golint is a linter for Go source code.` +} diff --git a/check/utils.go b/check/utils.go new file mode 100644 index 0000000..7b4e1ce --- /dev/null +++ b/check/utils.go @@ -0,0 +1,192 @@ +package check + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "reflect" + "strconv" + "strings" + "syscall" +) + +// GoFiles returns a slice of Go filenames +// in a given directory. +func GoFiles(dir string) ([]string, error) { + var filenames []string + visit := func(fp string, fi os.FileInfo, err error) error { + if strings.Contains(fp, "Godeps") { + return nil + } + if err != nil { + fmt.Println(err) // can't walk here, + return nil // but continue walking elsewhere + } + if fi.IsDir() { + return nil // not a file. ignore. + } + ext := filepath.Ext(fi.Name()) + if ext == ".go" { + filenames = append(filenames, fp) + } + return nil + } + + err := filepath.Walk(dir, visit) + + return filenames, err +} + +// lineCount returns the number of lines in a given file +func lineCount(filepath string) (int, error) { + out, err := exec.Command("wc", "-l", filepath).Output() + if err != nil { + return 0, err + } + // wc output is like: 999 filename.go + count, err := strconv.Atoi(strings.Split(strings.TrimSpace(string(out)), " ")[0]) + if err != nil { + return 0, err + } + + return count, nil +} + +type Error struct { + LineNumber int `json:"line_number"` + ErrorString string `json:"error_string"` +} + +type FileSummary struct { + Filename string `json:"filename"` + FileURL string `json:"file_url"` + Errors []Error `json:"errors"` +} + +// ByFilename implements sort.Interface for []Person based on +// the Age field. +type ByFilename []FileSummary + +func (a ByFilename) Len() int { return len(a) } +func (a ByFilename) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByFilename) Less(i, j int) bool { return a[i].Filename < a[j].Filename } + +func getFileSummary(filename, dir, cmd, out string) (FileSummary, error) { + filename = strings.TrimPrefix(filename, "repos/src") + githubLink := strings.TrimPrefix(dir, "repos/src") + fileURL := "https://" + strings.TrimPrefix(dir, "repos/src/") + "/blob/master" + strings.TrimPrefix(filename, githubLink) + fs := FileSummary{ + Filename: filename, + FileURL: fileURL, + } + split := strings.Split(string(out), "\n") + for _, sp := range split[0 : len(split)-1] { + parts := strings.Split(sp, ":") + msg := sp + if cmd != "gocyclo" { + msg = parts[len(parts)-1] + } + e := Error{ErrorString: msg} + switch cmd { + case "golint", "gocyclo", "vet": + ln, err := strconv.Atoi(strings.Split(sp, ":")[1]) + if err != nil { + return fs, err + } + e.LineNumber = ln + } + + fs.Errors = append(fs.Errors, e) + } + + return fs, nil +} + +// GoTool runs a given go command (for example gofmt, go tool vet) +// on a directory +func GoTool(dir string, filenames, command []string) (float64, []FileSummary, error) { + var failed = []FileSummary{} + for _, fi := range filenames { + params := command[1:] + params = append(params, fi) + + cmd := exec.Command(command[0], params...) + stdout, err := cmd.StdoutPipe() + if err != nil { + return 0, []FileSummary{}, nil + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return 0, []FileSummary{}, nil + } + + err = cmd.Start() + if err != nil { + return 0, []FileSummary{}, nil + } + + out, err := ioutil.ReadAll(stdout) + if err != nil { + return 0, []FileSummary{}, nil + } + + errout, err := ioutil.ReadAll(stderr) + if err != nil { + return 0, []FileSummary{}, nil + } + + if string(out) != "" { + fs, err := getFileSummary(fi, dir, command[0], string(out)) + if err != nil { + return 0, []FileSummary{}, nil + } + failed = append(failed, fs) + } + + // go vet logs to stderr + if string(errout) != "" { + cmd := command[0] + if reflect.DeepEqual(command, []string{"go", "tool", "vet"}) { + cmd = "vet" + } + fs, err := getFileSummary(fi, dir, cmd, string(errout)) + if err != nil { + return 0, []FileSummary{}, nil + } + failed = append(failed, fs) + } + + err = cmd.Wait() + if exitErr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + // some commands exit 1 when files fail to pass (for example go vet) + if status.ExitStatus() != 1 { + return 0, failed, err + // return 0, Error{}, err + } + } + } + + } + + if len(filenames) == 1 { + lc, err := lineCount(filenames[0]) + if err != nil { + return 0, failed, err + } + + var errors int + if len(failed) != 0 { + errors = len(failed[0].Errors) + } + + return float64(lc-errors) / float64(lc), failed, nil + } + + return float64(len(filenames)-len(failed)) / float64(len(filenames)), failed, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d7c0daa --- /dev/null +++ b/main.go @@ -0,0 +1,242 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "regexp" + "strings" + "time" + + "github.com/gophergala/go_report/check" + "gopkg.in/mgo.v2" + "labix.org/v2/mgo/bson" +) + +var ( + mongoURL = "mongodb://localhost:27017" + mongoDatabase = "goreportcard" + mongoCollection = "reports" +) + +func getMongoCollection() (*mgo.Collection, error) { + session, err := mgo.Dial(mongoURL) + if err != nil { + return nil, err + } + c := session.DB(mongoDatabase).C(mongoCollection) + return c, nil +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Serving home page") + if r.URL.Path[1:] == "" { + http.ServeFile(w, r, "templates/home.html") + } else { + http.NotFound(w, r) + } +} + +func assetsHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Serving " + r.URL.Path[1:]) + http.ServeFile(w, r, r.URL.Path[1:]) +} + +func orgRepoNames(url string) (string, string) { + dir := strings.TrimSuffix(url, ".git") + split := strings.Split(dir, "/") + org := split[len(split)-2] + repoName := split[len(split)-1] + + return org, repoName +} + +func dirName(url string) string { + org, repoName := orgRepoNames(url) + + return fmt.Sprintf("repos/src/github.com/%s/%s", org, repoName) +} + +func clone(url string) error { + org, _ := orgRepoNames(url) + if err := os.Mkdir(fmt.Sprintf("repos/src/github.com/%s", org), 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("could not create dir: %v", err) + } + dir := dirName(url) + _, err := os.Stat(dir) + if os.IsNotExist(err) { + cmd := exec.Command("git", "clone", "--depth", "1", "--single-branch", url, dir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("could not run git clone: %v", err) + } + } else if err != nil { + return fmt.Errorf("could not stat dir: %v", err) + } else { + cmd := exec.Command("git", "-C", dir, "pull") + if err := cmd.Run(); err != nil { + return fmt.Errorf("could not pull repo: %v", err) + } + } + + return nil +} + +type score struct { + Name string `json:"name"` + Description string `json:"description"` + FileSummaries []check.FileSummary `json:"file_summaries"` + Percentage float64 `json:"percentage"` +} + +type checksResp struct { + Checks []score `json:"checks"` + Average float64 `json:"average"` + Files int `json:"files"` + Issues int `json:"issues"` + Repo string `json:"repo"` + LastRefresh time.Time `json:"last_refresh"` +} + +func checkHandler(w http.ResponseWriter, r *http.Request) { + repo := r.FormValue("repo") + url := repo + if !strings.HasPrefix(url, "https://github.com/") { + url = "https://github.com/" + url + } + + w.Header().Set("Content-Type", "application/json") + + // if this is a GET request, fetch from cached version in mongo + if r.Method == "GET" { + // try and fetch from mongo + coll, err := getMongoCollection() + if err != nil { + log.Println("Failed to get mongo collection during GET: ", err) + } else { + resp := checksResp{} + err := coll.Find(bson.M{"repo": repo}).One(&resp) + if err != nil { + log.Println("Failed to fetch from mongo: ", err) + } else { + resp.LastRefresh = resp.LastRefresh.UTC() + b, err := json.Marshal(resp) + if err != nil { + log.Println("ERROR: could not marshal json:", err) + http.Error(w, err.Error(), 500) + return + } + w.Write(b) + log.Println("Loaded from cache!", repo) + return + } + } + } + + err := clone(url) + if err != nil { + log.Println("ERROR: could not clone repo: ", err) + http.Error(w, fmt.Sprintf("Could not clone repo: %v", err), 500) + return + } + + dir := dirName(url) + filenames, err := check.GoFiles(dir) + if err != nil { + log.Println("ERROR: could not get filenames: ", err) + http.Error(w, fmt.Sprintf("Could not get filenames: %v", err), 500) + return + } + checks := []check.Check{check.GoFmt{Dir: dir, Filenames: filenames}, + check.GoVet{Dir: dir, Filenames: filenames}, + check.GoLint{Dir: dir, Filenames: filenames}, + check.GoCyclo{Dir: dir, Filenames: filenames}, + } + + ch := make(chan score) + for _, c := range checks { + go func(c check.Check) { + p, summaries, err := c.Percentage() + if err != nil { + log.Printf("ERROR: (%s) %v", c.Name(), err) + } + s := score{ + Name: c.Name(), + Description: c.Description(), + FileSummaries: summaries, + Percentage: p, + } + ch <- s + }(c) + } + + resp := checksResp{Repo: repo, + Files: len(filenames), + LastRefresh: time.Now().UTC()} + var avg float64 + var issues = make(map[string]bool) + for i := 0; i < len(checks); i++ { + s := <-ch + resp.Checks = append(resp.Checks, s) + avg += s.Percentage + for _, fs := range s.FileSummaries { + issues[fs.Filename] = true + } + } + + resp.Average = avg / float64(len(checks)) + resp.Issues = len(issues) + + b, err := json.Marshal(resp) + if err != nil { + log.Println("ERROR: could not marshal json:", err) + http.Error(w, err.Error(), 500) + return + } + w.Write(b) + + // write to mongo + coll, err := getMongoCollection() + if err != nil { + log.Println("Failed to get mongo collection: ", err) + } else { + log.Println("Writing to mongo...") + _, err := coll.Upsert(bson.M{"Repo": repo}, resp) + if err != nil { + log.Println("Mongo writing error:", err) + } + } +} + +func reportHandler(w http.ResponseWriter, r *http.Request, org, repo string) { + http.ServeFile(w, r, "templates/home.html") +} + +func makeReportHandler(fn func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + validPath := regexp.MustCompile(`^/report/([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`) + + m := validPath.FindStringSubmatch(r.URL.Path) + if m == nil { + http.NotFound(w, r) + return + } + fn(w, r, m[1], m[2]) + } +} + +func main() { + if err := os.MkdirAll("repos/src/github.com", 0755); err != nil && !os.IsExist(err) { + log.Fatal("ERROR: could not create repos dir: ", err) + } + + http.HandleFunc("/assets/", assetsHandler) + http.HandleFunc("/checks", checkHandler) + http.HandleFunc("/report/", makeReportHandler(reportHandler)) + http.HandleFunc("/", homeHandler) + + fmt.Println("Running on 127.0.01:8080...") + log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil)) +} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..b2374a6 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,442 @@ + + + + + + + Go Report Card | A Gopher Gala Hackathon Product + + + + + + +
+
+
+
+
+
+

Go Report Card

+

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

+
+
+
+
+
+
+

+ +

+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+ +
+ + + + + + + + + + +