Import lintapp
diff --git a/lintapp/README.md b/lintapp/README.md
new file mode 100644
index 0000000..0edff47
--- /dev/null
+++ b/lintapp/README.md
@@ -0,0 +1,11 @@
+lintapp
+=======
+
+This directory contains the source for [go-lint.appspot.com](http://go-lint.appspot.com).
+
+Development Environment Setup
+-----------------------------
+
+- Copy config.go.template to config.go and edit the file as described in the comments.
+- Install Go App Engine SDK
+- Run the server using the dev_appserver command.
diff --git a/lintapp/app.yaml b/lintapp/app.yaml
new file mode 100644
index 0000000..9894f1b
--- /dev/null
+++ b/lintapp/app.yaml
@@ -0,0 +1,16 @@
+application: go-lint
+version: 1
+runtime: go
+api_version: go1
+
+handlers:
+- url: /favicon\.ico
+ static_files: assets/favicon.ico
+ upload: assets/favicon\.ico
+
+- url: /robots\.txt
+ static_files: assets/robots.txt
+ upload: assets/robots\.txt
+
+- url: /.*
+ script: _go_app
diff --git a/lintapp/assets/favicon.ico b/lintapp/assets/favicon.ico
new file mode 100644
index 0000000..f19c04d
--- /dev/null
+++ b/lintapp/assets/favicon.ico
Binary files differ
diff --git a/lintapp/assets/robots.txt b/lintapp/assets/robots.txt
new file mode 100644
index 0000000..6ffbc30
--- /dev/null
+++ b/lintapp/assets/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow: /
+
diff --git a/lintapp/assets/templates/common.html b/lintapp/assets/templates/common.html
new file mode 100644
index 0000000..b494dd8
--- /dev/null
+++ b/lintapp/assets/templates/common.html
@@ -0,0 +1,9 @@
+{{define "commonHead"}}
+ <meta charset="utf-8" />
+ <link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.3.0/base-min.css">
+ <style>body { padding: 15px; }</style>
+{{end}}
+
+{{define "commonFooter"}}
+<p><a href="/">Home</a> | <a href="mailto:{{contactEmail}}">Feedback</a> | <a href="https://github.com/golang/gddo/issues">Website Issues</a>
+{{end}}
diff --git a/lintapp/assets/templates/error.html b/lintapp/assets/templates/error.html
new file mode 100644
index 0000000..4ad7724
--- /dev/null
+++ b/lintapp/assets/templates/error.html
@@ -0,0 +1,10 @@
+{{define "ROOT"}}
+<!DOCTYPE html>
+<html>
+<head>
+ {{template "commonHead"}}
+ <title>{{.}}</title>
+<html><body>
+ <p>{{.}}
+</body></html>
+{{end}}
diff --git a/lintapp/assets/templates/index.html b/lintapp/assets/templates/index.html
new file mode 100644
index 0000000..9f3d18f
--- /dev/null
+++ b/lintapp/assets/templates/index.html
@@ -0,0 +1,20 @@
+{{define "ROOT"}}
+<!DOCTYPE html>
+<html>
+<head>
+ {{template "commonHead"}}
+ <title>go-lint</title>
+</head>
+<body>
+ <h3>Go Lint</h3>
+ <p>Go Lint lints <a href="http://golang.org/">Go</a> source files on GitHub,
+ Bitbucket and Google Project Hosting using the <a
+ href="https://github.com/golang/lint">lint package</a>.
+ <form method="POST" action="/-/refresh">
+ <input type="text" size=60 name="importPath" autofocus="autofocus" placeholder="Package import path">
+ <input value="Lint" type="submit">
+ </form>
+ {{template "commonFooter"}}
+</body>
+</html>
+{{end}}
diff --git a/lintapp/assets/templates/package.html b/lintapp/assets/templates/package.html
new file mode 100644
index 0000000..57f4a86
--- /dev/null
+++ b/lintapp/assets/templates/package.html
@@ -0,0 +1,21 @@
+{{define "ROOT"}}
+<!DOCTYPE html>
+<html>
+<head>
+ {{template "commonHead"}}
+ <title>Lint {{.Path}}</title>
+</head>
+<body>
+ <h3>Lint for {{if .URL}}<a href="{{.URL}}">{{.Path}}<a/>{{else}}{{.Path}}{{end}}</h3>
+ <form method="POST" action="/-/refresh">
+ <input type="hidden" name="importPath" value="{{.Path}}">
+ This report was generated {{.Updated|timeago}}. <input type="submit" value="Refresh">
+ </form>
+ {{range $f := .Files}}{{range .Problems}}
+ <p>{{if .Line}}<a href="{{printf $.LineFmt $f.URL .Line}}" title="{{.LineText}}">{{$f.Name}}:{{.Line}}</a>{{else}}{{$f.Name}}{{end}}:
+ {{.Text}}
+ {{if .Link}} <a href="{{.Link}}">☞</a>{{end}}
+ {{end}}{{end}}
+ {{template "commonFooter"}}
+</body></html>
+{{end}}
diff --git a/lintapp/config.go.template b/lintapp/config.go.template
new file mode 100644
index 0000000..e3579fa
--- /dev/null
+++ b/lintapp/config.go.template
@@ -0,0 +1,10 @@
+package lintapp
+
+func init() {
+ // Register an application at https://github.com/settings/applications/new
+ // and enter the client ID and client secret here.
+ gitHubCredentials = "client_id=<id>&client_secret=<secret>"
+
+ // Set contact email for /-/bot.html
+ contactEmail = "example@example.com"
+}
diff --git a/lintapp/main.go b/lintapp/main.go
new file mode 100644
index 0000000..b969d5b
--- /dev/null
+++ b/lintapp/main.go
@@ -0,0 +1,313 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+// Package lintapp implements the go-lint.appspot.com server.
+package lintapp
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "html/template"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "appengine"
+ "appengine/datastore"
+ "appengine/urlfetch"
+
+ "github.com/golang/gddo/gosrc"
+ "github.com/golang/lint"
+)
+
+func init() {
+ http.Handle("/", handlerFunc(serveRoot))
+ http.Handle("/-/bot", handlerFunc(serveBot))
+ http.Handle("/-/refresh", handlerFunc(serveRefresh))
+}
+
+var (
+ contactEmail = "unknown@example.com"
+ homeTemplate = parseTemplate("common.html", "index.html")
+ packageTemplate = parseTemplate("common.html", "package.html")
+ errorTemplate = parseTemplate("common.html", "error.html")
+ templateFuncs = template.FuncMap{
+ "timeago": timeagoFn,
+ "contactEmail": contactEmailFn,
+ }
+ gitHubCredentials = ""
+)
+
+func parseTemplate(fnames ...string) *template.Template {
+ paths := make([]string, len(fnames))
+ for i := range fnames {
+ paths[i] = filepath.Join("assets/templates", fnames[i])
+ }
+ t, err := template.New("").Funcs(templateFuncs).ParseFiles(paths...)
+ if err != nil {
+ panic(err)
+ }
+ t = t.Lookup("ROOT")
+ if t == nil {
+ panic(fmt.Sprintf("ROOT template not found in %v", fnames))
+ }
+ return t
+}
+
+func contactEmailFn() string {
+ return contactEmail
+}
+
+func timeagoFn(t time.Time) string {
+ d := time.Since(t)
+ switch {
+ case d < time.Second:
+ return "just now"
+ case d < 2*time.Second:
+ return "one second ago"
+ case d < time.Minute:
+ return fmt.Sprintf("%d seconds ago", d/time.Second)
+ case d < 2*time.Minute:
+ return "one minute ago"
+ case d < time.Hour:
+ return fmt.Sprintf("%d minutes ago", d/time.Minute)
+ case d < 2*time.Hour:
+ return "one hour ago"
+ case d < 48*time.Hour:
+ return fmt.Sprintf("%d hours ago", d/time.Hour)
+ default:
+ return fmt.Sprintf("%d days ago", d/(time.Hour*24))
+ }
+}
+
+func writeResponse(w http.ResponseWriter, status int, t *template.Template, v interface{}) error {
+ var buf bytes.Buffer
+ if err := t.Execute(&buf, v); err != nil {
+ return err
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
+ w.WriteHeader(status)
+ _, err := w.Write(buf.Bytes())
+ return err
+}
+
+func writeErrorResponse(w http.ResponseWriter, status int) error {
+ return writeResponse(w, status, errorTemplate, http.StatusText(status))
+}
+
+type transport struct {
+ rt http.RoundTripper
+ ua string
+}
+
+func (t transport) RoundTrip(r *http.Request) (*http.Response, error) {
+ r.Header.Set("User-Agent", t.ua)
+ if r.URL.Host == "api.github.com" && gitHubCredentials != "" {
+ if r.URL.RawQuery == "" {
+ r.URL.RawQuery = gitHubCredentials
+ } else {
+ r.URL.RawQuery += "&" + gitHubCredentials
+ }
+ }
+ return t.rt.RoundTrip(r)
+}
+
+func httpClient(r *http.Request) *http.Client {
+ c := appengine.NewContext(r)
+ return &http.Client{
+ Transport: &transport{
+ rt: &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
+ ua: fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
+ },
+ }
+}
+
+const version = 1
+
+type storePackage struct {
+ Data []byte
+ Version int
+}
+
+type lintPackage struct {
+ Files []*lintFile
+ Path string
+ Updated time.Time
+ LineFmt string
+ URL string
+}
+
+type lintFile struct {
+ Name string
+ Problems []*lintProblem
+ URL string
+}
+
+type lintProblem struct {
+ Line int
+ Text string
+ LineText string
+ Confidence float64
+ Link string
+}
+
+func putPackage(c appengine.Context, importPath string, pkg *lintPackage) error {
+ var buf bytes.Buffer
+ if err := gob.NewEncoder(&buf).Encode(pkg); err != nil {
+ return err
+ }
+ _, err := datastore.Put(c,
+ datastore.NewKey(c, "Package", importPath, 0, nil),
+ &storePackage{Data: buf.Bytes(), Version: version})
+ return err
+}
+
+func getPackage(c appengine.Context, importPath string) (*lintPackage, error) {
+ var spkg storePackage
+ if err := datastore.Get(c, datastore.NewKey(c, "Package", importPath, 0, nil), &spkg); err != nil {
+ if err == datastore.ErrNoSuchEntity {
+ err = nil
+ }
+ return nil, err
+ }
+ if spkg.Version != version {
+ return nil, nil
+ }
+ var pkg lintPackage
+ if err := gob.NewDecoder(bytes.NewReader(spkg.Data)).Decode(&pkg); err != nil {
+ return nil, err
+ }
+ return &pkg, nil
+}
+
+func runLint(r *http.Request, importPath string) (*lintPackage, error) {
+ dir, err := gosrc.Get(httpClient(r), importPath, "")
+ if err != nil {
+ return nil, err
+ }
+
+ pkg := lintPackage{
+ Path: importPath,
+ Updated: time.Now(),
+ LineFmt: dir.LineFmt,
+ URL: dir.BrowseURL,
+ }
+ linter := lint.Linter{}
+ for _, f := range dir.Files {
+ if !strings.HasSuffix(f.Name, ".go") {
+ continue
+ }
+ problems, err := linter.Lint(f.Name, f.Data)
+ if err == nil && len(problems) == 0 {
+ continue
+ }
+ file := lintFile{Name: f.Name, URL: f.BrowseURL}
+ if err != nil {
+ file.Problems = []*lintProblem{{Text: err.Error()}}
+ } else {
+ for _, p := range problems {
+ file.Problems = append(file.Problems, &lintProblem{
+ Line: p.Position.Line,
+ Text: p.Text,
+ LineText: p.LineText,
+ Confidence: p.Confidence,
+ Link: p.Link,
+ })
+ }
+ }
+ if len(file.Problems) > 0 {
+ pkg.Files = append(pkg.Files, &file)
+ }
+ }
+
+ if err := putPackage(appengine.NewContext(r), importPath, &pkg); err != nil {
+ return nil, err
+ }
+
+ return &pkg, nil
+}
+
+func filterByConfidence(r *http.Request, pkg *lintPackage) {
+ minConfidence, err := strconv.ParseFloat(r.FormValue("minConfidence"), 64)
+ if err != nil {
+ minConfidence = 0.8
+ }
+ for _, f := range pkg.Files {
+ j := 0
+ for i := range f.Problems {
+ if f.Problems[i].Confidence >= minConfidence {
+ f.Problems[j] = f.Problems[i]
+ j += 1
+ }
+ }
+ f.Problems = f.Problems[:j]
+ }
+}
+
+type handlerFunc func(http.ResponseWriter, *http.Request) error
+
+func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ err := f(w, r)
+ if err == nil {
+ return
+ } else if gosrc.IsNotFound(err) {
+ writeErrorResponse(w, 404)
+ } else if e, ok := err.(*gosrc.RemoteError); ok {
+ c.Infof("Remote error %s: %v", e.Host, e)
+ writeResponse(w, 500, errorTemplate, fmt.Sprintf("Error accessing %s.", e.Host))
+ } else if err != nil {
+ c.Errorf("Internal error %v", err)
+ writeErrorResponse(w, 500)
+ }
+}
+
+func serveRoot(w http.ResponseWriter, r *http.Request) error {
+ switch {
+ case r.Method != "GET" && r.Method != "HEAD":
+ return writeErrorResponse(w, 405)
+ case r.URL.Path == "/":
+ return writeResponse(w, 200, homeTemplate, nil)
+ default:
+ importPath := r.URL.Path[1:]
+ if !gosrc.IsValidPath(importPath) {
+ return gosrc.NotFoundError{Message: "bad path"}
+ }
+ c := appengine.NewContext(r)
+ pkg, err := getPackage(c, importPath)
+ if pkg == nil && err == nil {
+ pkg, err = runLint(r, importPath)
+ }
+ if err != nil {
+ return err
+ }
+ filterByConfidence(r, pkg)
+ return writeResponse(w, 200, packageTemplate, pkg)
+ }
+}
+
+func serveRefresh(w http.ResponseWriter, r *http.Request) error {
+ if r.Method != "POST" {
+ return writeErrorResponse(w, 405)
+ }
+ importPath := r.FormValue("importPath")
+ pkg, err := runLint(r, importPath)
+ if err != nil {
+ return err
+ }
+ http.Redirect(w, r, "/"+pkg.Path, 301)
+ return nil
+}
+
+func serveBot(w http.ResponseWriter, r *http.Request) error {
+ c := appengine.NewContext(r)
+ _, err := fmt.Fprintf(w, "Contact %s for help with the %s bot.", contactEmail, appengine.AppID(c))
+ return err
+}