mirror of
https://github.com/gojp/goreportcard.git
synced 2026-01-28 14:29:05 +08:00
initial commit
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -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
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -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.
|
||||
9
README.md
Normal file
9
README.md
Normal file
@@ -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)
|
||||
|
||||

|
||||
BIN
assets/gopherhat.jpg
Normal file
BIN
assets/gopherhat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
9
check/check.go
Normal file
9
check/check.go
Normal file
@@ -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)
|
||||
}
|
||||
20
check/go_vet.go
Normal file
20
check/go_vet.go
Normal file
@@ -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 `<code>go vet</code> examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string.`
|
||||
}
|
||||
25
check/gocyclo.go
Normal file
25
check/gocyclo.go
Normal file
@@ -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 `<a href="https://github.com/fzipp/gocyclo">Gocyclo</a> 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 '||'`
|
||||
}
|
||||
20
check/gofmt.go
Normal file
20
check/gofmt.go
Normal file
@@ -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 <code>gofmt -s</code> on your code, where <code>-s</code> is for the <a href="https://golang.org/cmd/gofmt/#hdr-The_simplify_command">"simplify" command</a>`
|
||||
}
|
||||
20
check/golint.go
Normal file
20
check/golint.go
Normal file
@@ -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.`
|
||||
}
|
||||
192
check/utils.go
Normal file
192
check/utils.go
Normal file
@@ -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
|
||||
}
|
||||
242
main.go
Normal file
242
main.go
Normal file
@@ -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))
|
||||
}
|
||||
442
templates/home.html
Normal file
442
templates/home.html
Normal file
@@ -0,0 +1,442 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<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">
|
||||
<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 h2 {
|
||||
font-size: 3em;
|
||||
}
|
||||
.input-row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.input-row input.input-box {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
}
|
||||
.btn-test {
|
||||
width: 100%;
|
||||
}
|
||||
.url label {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
line-height: 46px;
|
||||
font-weight: normal;
|
||||
color: rgb(51, 51, 51);
|
||||
font-weight: 200;
|
||||
}
|
||||
.url input {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
margin-left: -190px;
|
||||
padding-left: 190px;
|
||||
}
|
||||
|
||||
/* Results section
|
||||
-------------------------------------------------- */
|
||||
.results {
|
||||
width: 100%;
|
||||
}
|
||||
.results .head-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
.results .cell-description {
|
||||
width: 30%;
|
||||
}
|
||||
.results .cell-progress-bar {
|
||||
width: 70%;
|
||||
}
|
||||
.results .files, .results .errors {
|
||||
padding-left: 0;
|
||||
}
|
||||
.results .files .file {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
.results .files .errors .error {
|
||||
list-style-type: none;
|
||||
padding-left: 4em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.results .progress {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.results .details-row:hover {
|
||||
background: inherit;
|
||||
}
|
||||
.results .perfect {
|
||||
color: #aaa;
|
||||
}
|
||||
.results-text p {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.huge {
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.results .tool-description {
|
||||
color: #666;
|
||||
}
|
||||
.container-update {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Partials section
|
||||
-------------------------------------------------- */
|
||||
#partials {
|
||||
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;
|
||||
}
|
||||
</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/nihongo"/>
|
||||
</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>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</script>
|
||||
<script id="template-check" type="text/x-handlebars-template">
|
||||
<tr class="head-row">
|
||||
<td class="cell-description">
|
||||
{{{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>
|
||||
</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>
|
||||
</ul>
|
||||
{{/each}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
<script id="template-lastrefresh" type="text/x-handlebars-template">
|
||||
<p>Last refresh: {{last_refresh}} <a href=""><strong>Update now</strong></a></p>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
var loading = false;
|
||||
|
||||
// add a helper for getting the grade for a percentage
|
||||
var getGrade = function(percentage) {
|
||||
switch(true){
|
||||
case percentage > 90:
|
||||
return "A+";
|
||||
case percentage > 80:
|
||||
return "A";
|
||||
case percentage > 70:
|
||||
return "B";
|
||||
case percentage > 60:
|
||||
return "C";
|
||||
case percentage > 50:
|
||||
return "D";
|
||||
case percentage > 40:
|
||||
return "E";
|
||||
default:
|
||||
return "F";
|
||||
}
|
||||
};
|
||||
|
||||
Handlebars.registerHelper('gradeMessage', function(grade, options) {
|
||||
var gradeMessages = {
|
||||
"A+": "Excellent!",
|
||||
"A": "Great!",
|
||||
"B": "Not bad!",
|
||||
"C": "Needs some work",
|
||||
"D": "Needs lots improvement",
|
||||
"E": "Urgent improvement needed",
|
||||
"F": "... is for lots of things to Fix!"
|
||||
};
|
||||
return gradeMessages[grade];
|
||||
});
|
||||
|
||||
// add a helper for picking the progress bar colors
|
||||
Handlebars.registerHelper('color', function(percentage, options) {
|
||||
switch(true){
|
||||
case percentage < 30:
|
||||
return 'danger';
|
||||
case percentage < 50:
|
||||
return 'warning';
|
||||
case percentage < 80:
|
||||
return 'info';
|
||||
default:
|
||||
return 'success';
|
||||
};
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isfalse', function(percentage, options) {
|
||||
return percentage == false;
|
||||
});
|
||||
|
||||
// initialize handlebars templates
|
||||
var templates = {};
|
||||
$("script[id^=template]").each(function(){
|
||||
var name = $(this).attr("id").substring(9);
|
||||
var source = $(this).html();
|
||||
templates[name] = Handlebars.compile(source);
|
||||
});
|
||||
|
||||
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 populateResults = function(data){
|
||||
console.log("data is", data);
|
||||
var checks = data.checks;
|
||||
var $resultsText = $(".results-text");
|
||||
data.grade = getGrade(data.average * 100);
|
||||
$resultsText.html($(templates.grade(data)));
|
||||
|
||||
var $table = $(".table.results");
|
||||
$table.empty();
|
||||
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');
|
||||
|
||||
});
|
||||
$headRow.appendTo($table);
|
||||
|
||||
var $details = $(templates.details(checks[i]));
|
||||
$details.appendTo($table);
|
||||
}
|
||||
$(".container-results").removeClass('hidden').slideDown();
|
||||
|
||||
$lastRefresh = $(templates.lastrefresh(data));
|
||||
$(".container-update").html($lastRefresh).find("a").on("click", function(e){
|
||||
loadData.call($("form#check-form")[0], false);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
var loadData = function(getRequest){
|
||||
loading = true;
|
||||
var $form = $(this),
|
||||
url = $form.attr("action"),
|
||||
method = $form.attr("method"),
|
||||
data = {};
|
||||
console.log("GOnna make", getRequest ? "GET" : "POST");
|
||||
shrinkHeader();
|
||||
hideResults();
|
||||
showGopher();
|
||||
$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){;
|
||||
var html = templates.alert({message: "<strong>Oops!</strong> There was an error processing your request: " + err});
|
||||
var $alert = $(html);
|
||||
$alert.on("click", function(){
|
||||
$(this).closest(".alert").remove();
|
||||
});
|
||||
$alert.hide();
|
||||
$alert.appendTo("#notifications");
|
||||
$alert.slideDown();
|
||||
}).done(function(data, textStatus, jqXHR){
|
||||
hideGopher();
|
||||
populateResults(data);
|
||||
}).always(function(){
|
||||
loading = false;
|
||||
});
|
||||
history.pushState('', "Go Report Card for " + data.repo + " | A Gopher Gala Hackathon Product", "/report/" + data.repo);
|
||||
return false;
|
||||
};
|
||||
|
||||
var hideResults = function(){
|
||||
$(".container-results").hide();
|
||||
};
|
||||
|
||||
// on ready
|
||||
$(function(){
|
||||
|
||||
// handle form submission
|
||||
$("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);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user