x/website: add code and static files for website
All of the code and static assets that the website uses to run have been copied to this repo. There was also a few lines of code added telling the website where the doc directory, favicon.ico and robots.txt are. go repo change-id: Ife6443c32673b38000b90dd2efb2985db37ab773 x/tools repo change-id: Ia979a8b06d1b4db47d25ffdfdf925ba8a0ac67de Real new code additions: - main.go * lines 89-95 added getFullPath method * lines 217-222 mapped paths to doc/, favicon.ico, robots.txt in vfs - appinit.go * lines 147-153 added getFullPath method * lines 80-84 mapped paths to doc/, favicon.ico in vfs Several files were copied from x/tools and go so paths (and corresponding import paths) were changed as follows: "x/tools/cmd/godoc/" --> "x/website/cmd/golangorg/" "x/tools/godoc/static/" --> "x/website/content/static/" "x/tools/godoc/" (without godoc/static/) --> "x/website/cmd/golangorg/godoc/" "x/tools/internal/memcache" --> "x/website/internal/memcache" "go/doc/" --> "x/website/content/doc/" "go/favicon.ico" --> "x/website/favicon.ico" "go/robots.txt" --> "x/website/robots.txt" Updates golang/go#29206 Change-Id: I53985fc027f73e60c6946038f85133acf1ecb08c Reviewed-on: https://go-review.googlesource.com/c/156321 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Родитель
001739136e
Коммит
da9e5ccbe1
|
@ -0,0 +1,67 @@
|
|||
# Builder
|
||||
#########
|
||||
|
||||
FROM golang:1.11 AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
zip # required for generate-index.bash
|
||||
|
||||
# Check out the desired version of Go, both to build the godoc binary and serve
|
||||
# as the goroot for content serving.
|
||||
ARG GO_REF
|
||||
RUN test -n "$GO_REF" # GO_REF is required.
|
||||
RUN git clone --single-branch --depth=1 -b $GO_REF https://go.googlesource.com/go /goroot
|
||||
RUN cd /goroot/src && ./make.bash
|
||||
|
||||
ENV GOROOT /goroot
|
||||
ENV PATH=/goroot/bin:$PATH
|
||||
|
||||
RUN go version
|
||||
|
||||
RUN go get -v -d \
|
||||
golang.org/x/net/context \
|
||||
google.golang.org/appengine \
|
||||
cloud.google.com/go/datastore \
|
||||
golang.org/x/build \
|
||||
github.com/gomodule/redigo/redis
|
||||
|
||||
COPY . /go/src/golang.org/x/tools
|
||||
|
||||
WORKDIR /go/src/golang.org/x/website/cmd/golangorg
|
||||
RUN GODOC_DOCSET=/goroot ./generate-index.bash
|
||||
|
||||
RUN go build -o /godoc -tags=golangorg golang.org/x/website/cmd/golangorg
|
||||
|
||||
# Clean up goroot for the final image.
|
||||
RUN cd /goroot && git clean -xdf
|
||||
|
||||
# Add build metadata.
|
||||
RUN cd /goroot && echo "go repo HEAD: $(git rev-parse HEAD)" >> /goroot/buildinfo
|
||||
RUN echo "requested go ref: ${GO_REF}" >> /goroot/buildinfo
|
||||
ARG TOOLS_HEAD
|
||||
RUN echo "x/tools HEAD: ${TOOLS_HEAD}" >> /goroot/buildinfo
|
||||
ARG TOOLS_CLEAN
|
||||
RUN echo "x/tools clean: ${TOOLS_CLEAN}" >> /goroot/buildinfo
|
||||
ARG DOCKER_TAG
|
||||
RUN echo "image: ${DOCKER_TAG}" >> /goroot/buildinfo
|
||||
ARG BUILD_ENV
|
||||
RUN echo "build env: ${BUILD_ENV}" >> /goroot/buildinfo
|
||||
|
||||
RUN rm -rf /goroot/.git
|
||||
|
||||
# Final image
|
||||
#############
|
||||
|
||||
FROM gcr.io/distroless/base
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /godoc /app/
|
||||
COPY --from=build /go/src/golang.org/x/website/cmd/golangorg/hg-git-mapping.bin /app/
|
||||
|
||||
COPY --from=build /goroot /goroot
|
||||
ENV GOROOT /goroot
|
||||
|
||||
COPY --from=build /go/src/golang.org/x/website/cmd/golangorg/index.split.* /app/
|
||||
ENV GODOC_INDEX_GLOB index.split.*
|
||||
|
||||
CMD ["/app/godoc"]
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright 2018 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.
|
||||
|
||||
.PHONY: usage
|
||||
|
||||
GO_REF ?= release-branch.go1.11
|
||||
TOOLS_HEAD := $(shell git rev-parse HEAD)
|
||||
TOOLS_CLEAN := $(shell (git status --porcelain | grep -q .) && echo dirty || echo clean)
|
||||
ifeq ($(TOOLS_CLEAN),clean)
|
||||
DOCKER_VERSION ?= $(TOOLS_HEAD)
|
||||
else
|
||||
DOCKER_VERSION ?= $(TOOLS_HEAD)-dirty
|
||||
endif
|
||||
GCP_PROJECT := golang-org
|
||||
DOCKER_TAG := gcr.io/$(GCP_PROJECT)/godoc:$(DOCKER_VERSION)
|
||||
|
||||
usage:
|
||||
@echo "See Makefile and README.golangorg-app"
|
||||
@exit 1
|
||||
|
||||
cloud-build: Dockerfile.prod
|
||||
gcloud builds submit \
|
||||
--project=$(GCP_PROJECT) \
|
||||
--config=cloudbuild.yaml \
|
||||
--substitutions=_GO_REF=$(GO_REF),_TOOLS_HEAD=$(TOOLS_HEAD),_TOOLS_CLEAN=$(TOOLS_CLEAN),_DOCKER_TAG=$(DOCKER_TAG) \
|
||||
../.. # source code
|
||||
|
||||
docker-build: Dockerfile.prod
|
||||
# NOTE(cbro): move up in directory to include entire tools repo.
|
||||
# NOTE(cbro): any changes made to this command must also be made in cloudbuild.yaml.
|
||||
cd ../..; docker build \
|
||||
-f=cmd/godoc/Dockerfile.prod \
|
||||
--build-arg=GO_REF=$(GO_REF) \
|
||||
--build-arg=TOOLS_HEAD=$(TOOLS_HEAD) \
|
||||
--build-arg=TOOLS_CLEAN=$(TOOLS_CLEAN) \
|
||||
--build-arg=DOCKER_TAG=$(DOCKER_TAG) \
|
||||
--build-arg=BUILD_ENV=local \
|
||||
--tag=$(DOCKER_TAG) \
|
||||
.
|
||||
|
||||
docker-push: docker-build
|
||||
docker push $(DOCKER_TAG)
|
||||
|
||||
deploy:
|
||||
gcloud -q app deploy app.prod.yaml \
|
||||
--project=$(GCP_PROJECT) \
|
||||
--no-promote \
|
||||
--image-url=$(DOCKER_TAG)
|
||||
|
||||
get-latest-url:
|
||||
@gcloud app versions list \
|
||||
--service=default \
|
||||
--project=$(GCP_PROJECT) \
|
||||
--sort-by=~version.createTime \
|
||||
--format='value(version.versionUrl)' \
|
||||
--limit 1 | cut -f1 # NOTE(cbro): gcloud prints out createTime as the second field.
|
||||
|
||||
get-latest-id:
|
||||
@gcloud app versions list \
|
||||
--service=default \
|
||||
--project=$(GCP_PROJECT) \
|
||||
--sort-by=~version.createTime \
|
||||
--format='value(version.id)' \
|
||||
--limit 1 | cut -f1 # NOTE(cbro): gcloud prints out createTime as the second field.
|
||||
|
||||
regtest:
|
||||
go test -v \
|
||||
-regtest.host=$(shell make get-latest-url) \
|
||||
-run=Live
|
||||
|
||||
publish: regtest
|
||||
gcloud -q app services set-traffic default \
|
||||
--splits=$(shell make get-latest-id)=1 \
|
||||
--project=$(GCP_PROJECT)
|
||||
|
||||
@echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
@echo Stop and/or delete old versions:
|
||||
@echo "https://console.cloud.google.com/appengine/versions?project=$(GCP_PROJECT)&serviceId=default&versionssize=50"
|
||||
@echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
@ -0,0 +1,94 @@
|
|||
godoc on Google App Engine
|
||||
==========================
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
* Google Cloud SDK
|
||||
https://cloud.google.com/sdk/
|
||||
|
||||
* Redis
|
||||
|
||||
* Go sources under $GOROOT
|
||||
|
||||
* Godoc sources inside $GOPATH
|
||||
(go get -d golang.org/x/website/cmd/golangorg)
|
||||
|
||||
|
||||
Running locally, in production mode
|
||||
-----------------------------------
|
||||
|
||||
Build the app:
|
||||
|
||||
go build -tags golangorg
|
||||
|
||||
Run the app:
|
||||
|
||||
./golangorg
|
||||
|
||||
godoc should come up at http://localhost:8080
|
||||
|
||||
Use the PORT environment variable to change the port:
|
||||
|
||||
PORT=8081 ./golangorg
|
||||
|
||||
Running locally, in production mode, using Docker
|
||||
-------------------------------------------------
|
||||
|
||||
Build the app's Docker container:
|
||||
|
||||
make docker-build
|
||||
|
||||
Make sure redis is running on port 6379:
|
||||
|
||||
$ echo PING | nc localhost 6379
|
||||
+PONG
|
||||
^C
|
||||
|
||||
Run the datastore emulator:
|
||||
|
||||
gcloud beta emulators datastore start --project golang-org
|
||||
|
||||
In another terminal window, run the container:
|
||||
|
||||
$(gcloud beta emulators datastore env-init)
|
||||
|
||||
docker run --rm \
|
||||
--net host \
|
||||
--env GODOC_REDIS_ADDR=localhost:6379 \
|
||||
--env DATASTORE_EMULATOR_HOST=$DATASTORE_EMULATOR_HOST \
|
||||
--env DATASTORE_PROJECT_ID=$DATASTORE_PROJECT_ID \
|
||||
gcr.io/golang-org/godoc
|
||||
|
||||
godoc should come up at http://localhost:8080
|
||||
|
||||
|
||||
Deploying to golang.org
|
||||
-----------------------
|
||||
|
||||
Make sure you're signed in to gcloud:
|
||||
|
||||
gcloud auth login
|
||||
|
||||
Build the image, push it to gcr.io, and deploy to Flex:
|
||||
|
||||
make cloud-build deploy
|
||||
|
||||
Point the load balancer to the newly deployed version:
|
||||
(This also runs regression tests)
|
||||
|
||||
make publish
|
||||
|
||||
Stop and/or delete down any very old versions. (Stopped versions can be re-started.)
|
||||
Keep at least one older verson to roll back to, just in case.
|
||||
You can also migrate traffic to the new version via this UI.
|
||||
|
||||
https://console.cloud.google.com/appengine/versions?project=golang-org&serviceId=default&versionssize=50
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
Ensure the Cloud SDK is on your PATH and you have the app-engine-go component
|
||||
installed (gcloud components install app-engine-go) and your components are
|
||||
up-to-date (gcloud components update)
|
|
@ -0,0 +1,13 @@
|
|||
runtime: go
|
||||
api_version: go1
|
||||
instance_class: F4_1G
|
||||
|
||||
handlers:
|
||||
- url: /s
|
||||
script: _go_app
|
||||
login: admin
|
||||
- url: /dl/init
|
||||
script: _go_app
|
||||
login: admin
|
||||
- url: /.*
|
||||
script: _go_app
|
|
@ -0,0 +1,16 @@
|
|||
runtime: custom
|
||||
env: flex
|
||||
|
||||
env_variables:
|
||||
GODOC_PROD: true
|
||||
GODOC_ENFORCE_HOSTS: true
|
||||
GODOC_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"
|
||||
GODOC_ANALYTICS: UA-11222381-2
|
||||
DATASTORE_PROJECT_ID: golang-org
|
||||
|
||||
network:
|
||||
name: golang
|
||||
|
||||
resources:
|
||||
cpu: 4
|
||||
memory_gb: 7.50
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// +build golangorg
|
||||
|
||||
package main
|
||||
|
||||
// This file replaces main.go when running godoc under app-engine.
|
||||
// See README.golangorg-app for details.
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"go/build"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/dl"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/proxy"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/redirect"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/short"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/zipfs"
|
||||
"golang.org/x/website/content/static"
|
||||
|
||||
"cloud.google.com/go/datastore"
|
||||
"golang.org/x/website/internal/memcache"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
||||
|
||||
var (
|
||||
// .zip filename
|
||||
zipFilename = os.Getenv("GODOC_ZIP")
|
||||
|
||||
// goroot directory in .zip file
|
||||
zipGoroot = os.Getenv("GODOC_ZIP_PREFIX")
|
||||
|
||||
// glob pattern describing search index files
|
||||
// (if empty, the index is built at run-time)
|
||||
indexFilenames = os.Getenv("GODOC_INDEX_GLOB")
|
||||
)
|
||||
|
||||
playEnabled = true
|
||||
|
||||
log.Println("initializing godoc ...")
|
||||
log.Printf(".zip file = %s", zipFilename)
|
||||
log.Printf(".zip GOROOT = %s", zipGoroot)
|
||||
log.Printf("index files = %s", indexFilenames)
|
||||
|
||||
fsGate := make(chan bool, 20)
|
||||
|
||||
if zipFilename != "" {
|
||||
goroot := path.Join("/", zipGoroot) // fsHttp paths are relative to '/'
|
||||
// read .zip file and set up file systems
|
||||
rc, err := zip.OpenReader(zipFilename)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %s\n", zipFilename, err)
|
||||
}
|
||||
// rc is never closed (app running forever)
|
||||
fs.Bind("/", zipfs.New(rc, zipFilename), goroot, vfs.BindReplace)
|
||||
} else {
|
||||
rootfs := gatefs.New(vfs.OS(runtime.GOROOT()), fsGate)
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
}
|
||||
|
||||
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
|
||||
|
||||
docPath := getFullPath("/src/golang.org/x/website/content/doc")
|
||||
fs.Bind("/doc", gatefs.New(vfs.OS(docPath), fsGate), "/", vfs.BindBefore)
|
||||
|
||||
webroot := getFullPath("/src/golang.org/x/website")
|
||||
fs.Bind("/favicon.ico", gatefs.New(vfs.OS(webroot), fsGate), "/favicon.ico", vfs.BindBefore)
|
||||
|
||||
corpus := godoc.NewCorpus(fs)
|
||||
corpus.Verbose = false
|
||||
corpus.MaxResults = 10000 // matches flag default in main.go
|
||||
corpus.IndexEnabled = true
|
||||
corpus.IndexFiles = indexFilenames
|
||||
if err := corpus.Init(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
corpus.IndexDirectory = indexDirectoryDefault
|
||||
corpus.InitVersionInfo()
|
||||
if indexFilenames != "" {
|
||||
corpus.RunIndexer()
|
||||
} else {
|
||||
go corpus.RunIndexer()
|
||||
}
|
||||
|
||||
pres = godoc.NewPresentation(corpus)
|
||||
pres.TabWidth = 8
|
||||
pres.ShowPlayground = true
|
||||
pres.DeclLinks = true
|
||||
pres.NotesRx = regexp.MustCompile("BUG")
|
||||
pres.GoogleAnalytics = os.Getenv("GODOC_ANALYTICS")
|
||||
|
||||
readTemplates(pres)
|
||||
|
||||
datastoreClient, memcacheClient := getClients()
|
||||
|
||||
// NOTE(cbro): registerHandlers registers itself against DefaultServeMux.
|
||||
// The mux returned has host enforcement, so it's important to register
|
||||
// against this mux and not DefaultServeMux.
|
||||
mux := registerHandlers(pres)
|
||||
dl.RegisterHandlers(mux, datastoreClient, memcacheClient)
|
||||
short.RegisterHandlers(mux, datastoreClient, memcacheClient)
|
||||
|
||||
// Register /compile and /share handlers against the default serve mux
|
||||
// so that other app modules can make plain HTTP requests to those
|
||||
// hosts. (For reasons, HTTPS communication between modules is broken.)
|
||||
proxy.RegisterHandlers(http.DefaultServeMux)
|
||||
|
||||
http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "ok")
|
||||
})
|
||||
|
||||
http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "User-agent: *\nDisallow: /search\n")
|
||||
})
|
||||
|
||||
if err := redirect.LoadChangeMap("hg-git-mapping.bin"); err != nil {
|
||||
log.Fatalf("LoadChangeMap: %v", err)
|
||||
}
|
||||
|
||||
log.Println("godoc initialization complete")
|
||||
|
||||
// TODO(cbro): add instrumentation via opencensus.
|
||||
port := "8080"
|
||||
if p := os.Getenv("PORT"); p != "" { // PORT is set by GAE flex.
|
||||
port = p
|
||||
}
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
}
|
||||
|
||||
func getFullPath(relPath string) string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
if gopath == "" {
|
||||
gopath = build.Default.GOPATH
|
||||
}
|
||||
return gopath + relPath
|
||||
}
|
||||
|
||||
func getClients() (*datastore.Client, *memcache.Client) {
|
||||
ctx := context.Background()
|
||||
|
||||
datastoreClient, err := datastore.NewClient(ctx, "")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "missing project") {
|
||||
log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
|
||||
}
|
||||
log.Fatalf("datastore.NewClient: %v.", err)
|
||||
}
|
||||
|
||||
redisAddr := os.Getenv("GODOC_REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
log.Fatalf("Missing redis server for godoc in production mode. set GODOC_REDIS_ADDR environment variable.")
|
||||
}
|
||||
memcacheClient := memcache.New(redisAddr)
|
||||
return datastoreClient, memcacheClient
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// +build autocert
|
||||
|
||||
// This file adds automatic TLS certificate support (using
|
||||
// golang.org/x/crypto/acme/autocert), conditional on the use of the
|
||||
// autocert build tag. It sets the serveAutoCertHook func variable
|
||||
// non-nil. It is used by main.go.
|
||||
//
|
||||
// TODO: make this the default? We're in the Go 1.8 freeze now, so
|
||||
// this is too invasive to be default, but we want it for
|
||||
// https://beta.golang.org/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
autoCertDirFlag = flag.String("autocert_cache_dir", "/var/cache/autocert", "Directory to cache TLS certs")
|
||||
autoCertHostFlag = flag.String("autocert_hostname", "", "optional hostname to require in autocert SNI requests")
|
||||
)
|
||||
|
||||
func init() {
|
||||
runHTTPS = runHTTPSAutocert
|
||||
certInit = certInitAutocert
|
||||
wrapHTTPMux = wrapHTTPMuxAutocert
|
||||
}
|
||||
|
||||
var autocertManager *autocert.Manager
|
||||
|
||||
func certInitAutocert() {
|
||||
autocertManager = &autocert.Manager{
|
||||
Cache: autocert.DirCache(*autoCertDirFlag),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
}
|
||||
if *autoCertHostFlag != "" {
|
||||
autocertManager.HostPolicy = autocert.HostWhitelist(*autoCertHostFlag)
|
||||
}
|
||||
}
|
||||
|
||||
func runHTTPSAutocert(h http.Handler) error {
|
||||
srv := &http.Server{
|
||||
Handler: h,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: autocertManager.GetCertificate,
|
||||
},
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
http2.ConfigureServer(srv, &http2.Server{})
|
||||
ln, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.Serve(tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, srv.TLSConfig))
|
||||
}
|
||||
|
||||
func wrapHTTPMuxAutocert(h http.Handler) http.Handler {
|
||||
return autocertManager.HTTPHandler(h)
|
||||
}
|
||||
|
||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
// go away.
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/blog"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/redirect"
|
||||
)
|
||||
|
||||
const (
|
||||
blogRepo = "golang.org/x/blog"
|
||||
blogURL = "https://blog.golang.org/"
|
||||
blogPath = "/blog/"
|
||||
)
|
||||
|
||||
var (
|
||||
blogServer http.Handler // set by blogInit
|
||||
blogInitOnce sync.Once
|
||||
playEnabled bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize blog only when first accessed.
|
||||
http.HandleFunc(blogPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
blogInitOnce.Do(func() {
|
||||
blogInit(r.Host)
|
||||
})
|
||||
blogServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func blogInit(host string) {
|
||||
// Binary distributions included the blog content in "/blog".
|
||||
// We stopped including this in Go 1.11.
|
||||
root := filepath.Join(runtime.GOROOT(), "blog")
|
||||
|
||||
// Prefer content from the golang.org/x/blog repository if present.
|
||||
if pkg, err := build.Import(blogRepo, "", build.FindOnly); err == nil {
|
||||
root = pkg.Dir
|
||||
}
|
||||
|
||||
// If content is not available fall back to redirect.
|
||||
if fi, err := os.Stat(root); err != nil || !fi.IsDir() {
|
||||
fmt.Fprintf(os.Stderr, "Blog content not available locally. "+
|
||||
"To install, run \n\tgo get %v\n", blogRepo)
|
||||
blogServer = http.HandlerFunc(blogRedirectHandler)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := blog.NewServer(blog.Config{
|
||||
BaseURL: blogPath,
|
||||
BasePath: strings.TrimSuffix(blogPath, "/"),
|
||||
ContentPath: filepath.Join(root, "content"),
|
||||
TemplatePath: filepath.Join(root, "template"),
|
||||
HomeArticles: 5,
|
||||
PlayEnabled: playEnabled,
|
||||
ServeLocalLinks: strings.HasPrefix(host, "localhost"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
blogServer = s
|
||||
}
|
||||
|
||||
func blogRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == blogPath {
|
||||
http.Redirect(w, r, blogURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
blogPrefixHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
var blogPrefixHandler = redirect.PrefixHandler(blogPath, blogURL)
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright 2018 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.
|
||||
|
||||
# NOTE(cbro): any changes to the docker command must also be
|
||||
# made in docker-build in the Makefile.
|
||||
#
|
||||
# Variable substitutions must have a preceding underscore. See:
|
||||
# https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values#using_user-defined_substitutions
|
||||
steps:
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args: [
|
||||
'build',
|
||||
'-f=cmd/godoc/Dockerfile.prod',
|
||||
'--build-arg=GO_REF=${_GO_REF}',
|
||||
'--build-arg=TOOLS_HEAD=${_TOOLS_HEAD}',
|
||||
'--build-arg=TOOLS_CLEAN=${_TOOLS_CLEAN}',
|
||||
'--build-arg=DOCKER_TAG=${_DOCKER_TAG}',
|
||||
'--build-arg=BUILD_ENV=cloudbuild',
|
||||
'--tag=${_DOCKER_TAG}',
|
||||
'.',
|
||||
]
|
||||
images: ['${_DOCKER_TAG}']
|
||||
options:
|
||||
machineType: 'N1_HIGHCPU_8' # building the godoc index takes a lot of memory.
|
|
@ -0,0 +1,523 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
|
||||
// files named $GOROOT/doc/codewalk/*.xml.
|
||||
// For an example and a description of the format, see
|
||||
// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
|
||||
// and see http://localhost:6060/doc/codewalk/codewalk .
|
||||
// That page is itself a codewalk; the source code for it is
|
||||
// $GOROOT/doc/codewalk/codewalk.xml.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
var codewalkHTML, codewalkdirHTML *template.Template
|
||||
|
||||
// Handler for /doc/codewalk/ and below.
|
||||
func codewalk(w http.ResponseWriter, r *http.Request) {
|
||||
relpath := r.URL.Path[len("/doc/codewalk/"):]
|
||||
abspath := r.URL.Path
|
||||
|
||||
r.ParseForm()
|
||||
if f := r.FormValue("fileprint"); f != "" {
|
||||
codewalkFileprint(w, r, f)
|
||||
return
|
||||
}
|
||||
|
||||
// If directory exists, serve list of code walks.
|
||||
dir, err := fs.Lstat(abspath)
|
||||
if err == nil && dir.IsDir() {
|
||||
codewalkDir(w, r, relpath, abspath)
|
||||
return
|
||||
}
|
||||
|
||||
// If file exists, serve using standard file server.
|
||||
if err == nil {
|
||||
pres.ServeFile(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise append .xml and hope to find
|
||||
// a codewalk description, but before trim
|
||||
// the trailing /.
|
||||
abspath = strings.TrimRight(abspath, "/")
|
||||
cw, err := loadCodewalk(abspath + ".xml")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Canonicalize the path and redirect if changed
|
||||
if redir(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
pres.ServePage(w, godoc.Page{
|
||||
Title: "Codewalk: " + cw.Title,
|
||||
Tabtitle: cw.Title,
|
||||
Body: applyTemplate(codewalkHTML, "codewalk", cw),
|
||||
})
|
||||
}
|
||||
|
||||
func redir(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
canonical := pathpkg.Clean(r.URL.Path)
|
||||
if !strings.HasSuffix(canonical, "/") {
|
||||
canonical += "/"
|
||||
}
|
||||
if r.URL.Path != canonical {
|
||||
url := *r.URL
|
||||
url.Path = canonical
|
||||
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
||||
redirected = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
log.Printf("%s.Execute: %s", name, err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// A Codewalk represents a single codewalk read from an XML file.
|
||||
type Codewalk struct {
|
||||
Title string `xml:"title,attr"`
|
||||
File []string `xml:"file"`
|
||||
Step []*Codestep `xml:"step"`
|
||||
}
|
||||
|
||||
// A Codestep is a single step in a codewalk.
|
||||
type Codestep struct {
|
||||
// Filled in from XML
|
||||
Src string `xml:"src,attr"`
|
||||
Title string `xml:"title,attr"`
|
||||
XML string `xml:",innerxml"`
|
||||
|
||||
// Derived from Src; not in XML.
|
||||
Err error
|
||||
File string
|
||||
Lo int
|
||||
LoByte int
|
||||
Hi int
|
||||
HiByte int
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// String method for printing in template.
|
||||
// Formats file address nicely.
|
||||
func (st *Codestep) String() string {
|
||||
s := st.File
|
||||
if st.Lo != 0 || st.Hi != 0 {
|
||||
s += fmt.Sprintf(":%d", st.Lo)
|
||||
if st.Lo != st.Hi {
|
||||
s += fmt.Sprintf(",%d", st.Hi)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// loadCodewalk reads a codewalk from the named XML file.
|
||||
func loadCodewalk(filename string) (*Codewalk, error) {
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
cw := new(Codewalk)
|
||||
d := xml.NewDecoder(f)
|
||||
d.Entity = xml.HTMLEntity
|
||||
err = d.Decode(cw)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "parsing", Path: filename, Err: err}
|
||||
}
|
||||
|
||||
// Compute file list, evaluate line numbers for addresses.
|
||||
m := make(map[string]bool)
|
||||
for _, st := range cw.Step {
|
||||
i := strings.Index(st.Src, ":")
|
||||
if i < 0 {
|
||||
i = len(st.Src)
|
||||
}
|
||||
filename := st.Src[0:i]
|
||||
data, err := vfs.ReadFile(fs, filename)
|
||||
if err != nil {
|
||||
st.Err = err
|
||||
continue
|
||||
}
|
||||
if i < len(st.Src) {
|
||||
lo, hi, err := addrToByteRange(st.Src[i+1:], 0, data)
|
||||
if err != nil {
|
||||
st.Err = err
|
||||
continue
|
||||
}
|
||||
// Expand match to line boundaries.
|
||||
for lo > 0 && data[lo-1] != '\n' {
|
||||
lo--
|
||||
}
|
||||
for hi < len(data) && (hi == 0 || data[hi-1] != '\n') {
|
||||
hi++
|
||||
}
|
||||
st.Lo = byteToLine(data, lo)
|
||||
st.Hi = byteToLine(data, hi-1)
|
||||
}
|
||||
st.Data = data
|
||||
st.File = filename
|
||||
m[filename] = true
|
||||
}
|
||||
|
||||
// Make list of files
|
||||
cw.File = make([]string, len(m))
|
||||
i := 0
|
||||
for f := range m {
|
||||
cw.File[i] = f
|
||||
i++
|
||||
}
|
||||
sort.Strings(cw.File)
|
||||
|
||||
return cw, nil
|
||||
}
|
||||
|
||||
// codewalkDir serves the codewalk directory listing.
|
||||
// It scans the directory for subdirectories or files named *.xml
|
||||
// and prepares a table.
|
||||
func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string) {
|
||||
type elem struct {
|
||||
Name string
|
||||
Title string
|
||||
}
|
||||
|
||||
dir, err := fs.ReadDir(abspath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
var v []interface{}
|
||||
for _, fi := range dir {
|
||||
name := fi.Name()
|
||||
if fi.IsDir() {
|
||||
v = append(v, &elem{name + "/", ""})
|
||||
} else if strings.HasSuffix(name, ".xml") {
|
||||
cw, err := loadCodewalk(abspath + "/" + name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
v = append(v, &elem{name[0 : len(name)-len(".xml")], cw.Title})
|
||||
}
|
||||
}
|
||||
|
||||
pres.ServePage(w, godoc.Page{
|
||||
Title: "Codewalks",
|
||||
Body: applyTemplate(codewalkdirHTML, "codewalkdir", v),
|
||||
})
|
||||
}
|
||||
|
||||
// codewalkFileprint serves requests with ?fileprint=f&lo=lo&hi=hi.
|
||||
// The filename f has already been retrieved and is passed as an argument.
|
||||
// Lo and hi are the numbers of the first and last line to highlight
|
||||
// in the response. This format is used for the middle window pane
|
||||
// of the codewalk pages. It is a separate iframe and does not get
|
||||
// the usual godoc HTML wrapper.
|
||||
func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
|
||||
abspath := f
|
||||
data, err := vfs.ReadFile(fs, abspath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
pres.ServeError(w, r, f, err)
|
||||
return
|
||||
}
|
||||
lo, _ := strconv.Atoi(r.FormValue("lo"))
|
||||
hi, _ := strconv.Atoi(r.FormValue("hi"))
|
||||
if hi < lo {
|
||||
hi = lo
|
||||
}
|
||||
lo = lineToByte(data, lo)
|
||||
hi = lineToByte(data, hi+1)
|
||||
|
||||
// Put the mark 4 lines before lo, so that the iframe
|
||||
// shows a few lines of context before the highlighted
|
||||
// section.
|
||||
n := 4
|
||||
mark := lo
|
||||
for ; mark > 0 && n > 0; mark-- {
|
||||
if data[mark-1] == '\n' {
|
||||
if n--; n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, `<style type="text/css">@import "/doc/codewalk/codewalk.css";</style><pre>`)
|
||||
template.HTMLEscape(w, data[0:mark])
|
||||
io.WriteString(w, "<a name='mark'></a>")
|
||||
template.HTMLEscape(w, data[mark:lo])
|
||||
if lo < hi {
|
||||
io.WriteString(w, "<div class='codewalkhighlight'>")
|
||||
template.HTMLEscape(w, data[lo:hi])
|
||||
io.WriteString(w, "</div>")
|
||||
}
|
||||
template.HTMLEscape(w, data[hi:])
|
||||
io.WriteString(w, "</pre>")
|
||||
}
|
||||
|
||||
// addrToByte evaluates the given address starting at offset start in data.
|
||||
// It returns the lo and hi byte offset of the matched region within data.
|
||||
// See http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II
|
||||
// for details on the syntax.
|
||||
func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) {
|
||||
var (
|
||||
dir byte
|
||||
prevc byte
|
||||
charOffset bool
|
||||
)
|
||||
lo = start
|
||||
hi = start
|
||||
for addr != "" && err == nil {
|
||||
c := addr[0]
|
||||
switch c {
|
||||
default:
|
||||
err = errors.New("invalid address syntax near " + string(c))
|
||||
case ',':
|
||||
if len(addr) == 1 {
|
||||
hi = len(data)
|
||||
} else {
|
||||
_, hi, err = addrToByteRange(addr[1:], hi, data)
|
||||
}
|
||||
return
|
||||
|
||||
case '+', '-':
|
||||
if prevc == '+' || prevc == '-' {
|
||||
lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset)
|
||||
}
|
||||
dir = c
|
||||
|
||||
case '$':
|
||||
lo = len(data)
|
||||
hi = len(data)
|
||||
if len(addr) > 1 {
|
||||
dir = '+'
|
||||
}
|
||||
|
||||
case '#':
|
||||
charOffset = true
|
||||
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
var i int
|
||||
for i = 1; i < len(addr); i++ {
|
||||
if addr[i] < '0' || addr[i] > '9' {
|
||||
break
|
||||
}
|
||||
}
|
||||
var n int
|
||||
n, err = strconv.Atoi(addr[0:i])
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset)
|
||||
dir = 0
|
||||
charOffset = false
|
||||
prevc = c
|
||||
addr = addr[i:]
|
||||
continue
|
||||
|
||||
case '/':
|
||||
var i, j int
|
||||
Regexp:
|
||||
for i = 1; i < len(addr); i++ {
|
||||
switch addr[i] {
|
||||
case '\\':
|
||||
i++
|
||||
case '/':
|
||||
j = i + 1
|
||||
break Regexp
|
||||
}
|
||||
}
|
||||
if j == 0 {
|
||||
j = i
|
||||
}
|
||||
pattern := addr[1:i]
|
||||
lo, hi, err = addrRegexp(data, lo, hi, dir, pattern)
|
||||
prevc = c
|
||||
addr = addr[j:]
|
||||
continue
|
||||
}
|
||||
prevc = c
|
||||
addr = addr[1:]
|
||||
}
|
||||
|
||||
if err == nil && dir != 0 {
|
||||
lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return lo, hi, nil
|
||||
}
|
||||
|
||||
// addrNumber applies the given dir, n, and charOffset to the address lo, hi.
|
||||
// dir is '+' or '-', n is the count, and charOffset is true if the syntax
|
||||
// used was #n. Applying +n (or +#n) means to advance n lines
|
||||
// (or characters) after hi. Applying -n (or -#n) means to back up n lines
|
||||
// (or characters) before lo.
|
||||
// The return value is the new lo, hi.
|
||||
func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) {
|
||||
switch dir {
|
||||
case 0:
|
||||
lo = 0
|
||||
hi = 0
|
||||
fallthrough
|
||||
|
||||
case '+':
|
||||
if charOffset {
|
||||
pos := hi
|
||||
for ; n > 0 && pos < len(data); n-- {
|
||||
_, size := utf8.DecodeRune(data[pos:])
|
||||
pos += size
|
||||
}
|
||||
if n == 0 {
|
||||
return pos, pos, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
// find next beginning of line
|
||||
if hi > 0 {
|
||||
for hi < len(data) && data[hi-1] != '\n' {
|
||||
hi++
|
||||
}
|
||||
}
|
||||
lo = hi
|
||||
if n == 0 {
|
||||
return lo, hi, nil
|
||||
}
|
||||
for ; hi < len(data); hi++ {
|
||||
if data[hi] != '\n' {
|
||||
continue
|
||||
}
|
||||
switch n--; n {
|
||||
case 1:
|
||||
lo = hi + 1
|
||||
case 0:
|
||||
return lo, hi + 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
case '-':
|
||||
if charOffset {
|
||||
// Scan backward for bytes that are not UTF-8 continuation bytes.
|
||||
pos := lo
|
||||
for ; pos > 0 && n > 0; pos-- {
|
||||
if data[pos]&0xc0 != 0x80 {
|
||||
n--
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return pos, pos, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
// find earlier beginning of line
|
||||
for lo > 0 && data[lo-1] != '\n' {
|
||||
lo--
|
||||
}
|
||||
hi = lo
|
||||
if n == 0 {
|
||||
return lo, hi, nil
|
||||
}
|
||||
for ; lo >= 0; lo-- {
|
||||
if lo > 0 && data[lo-1] != '\n' {
|
||||
continue
|
||||
}
|
||||
switch n--; n {
|
||||
case 1:
|
||||
hi = lo
|
||||
case 0:
|
||||
return lo, hi, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, 0, errors.New("address out of range")
|
||||
}
|
||||
|
||||
// addrRegexp searches for pattern in the given direction starting at lo, hi.
|
||||
// The direction dir is '+' (search forward from hi) or '-' (search backward from lo).
|
||||
// Backward searches are unimplemented.
|
||||
func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if dir == '-' {
|
||||
// Could implement reverse search using binary search
|
||||
// through file, but that seems like overkill.
|
||||
return 0, 0, errors.New("reverse search not implemented")
|
||||
}
|
||||
m := re.FindIndex(data[hi:])
|
||||
if len(m) > 0 {
|
||||
m[0] += hi
|
||||
m[1] += hi
|
||||
} else if hi > 0 {
|
||||
// No match. Wrap to beginning of data.
|
||||
m = re.FindIndex(data)
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return 0, 0, errors.New("no match for " + pattern)
|
||||
}
|
||||
return m[0], m[1], nil
|
||||
}
|
||||
|
||||
// lineToByte returns the byte index of the first byte of line n.
|
||||
// Line numbers begin at 1.
|
||||
func lineToByte(data []byte, n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
n--
|
||||
for i, c := range data {
|
||||
if c == '\n' {
|
||||
if n--; n == 0 {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(data)
|
||||
}
|
||||
|
||||
// byteToLine returns the number of the line containing the byte at index i.
|
||||
func byteToLine(data []byte, i int) int {
|
||||
l := 1
|
||||
for j, c := range data {
|
||||
if j == i {
|
||||
return l
|
||||
}
|
||||
if c == '\n' {
|
||||
l++
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
// +build !golangorg
|
||||
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Register a redirect handler for /dl/ to the golang.org download page.
|
||||
// This file will not be included when deploying godoc to golang.org.
|
||||
|
||||
func init() {
|
||||
http.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
/*
|
||||
|
||||
Godoc extracts and generates documentation for Go programs.
|
||||
|
||||
It runs as a web server and presents the documentation as a
|
||||
web page.
|
||||
|
||||
godoc -http=:6060
|
||||
|
||||
Usage:
|
||||
|
||||
godoc [flag]
|
||||
|
||||
The flags are:
|
||||
|
||||
-v
|
||||
verbose mode
|
||||
-timestamps=true
|
||||
show timestamps with directory listings
|
||||
-index
|
||||
enable identifier and full text search index
|
||||
(no search box is shown if -index is not set)
|
||||
-index_files=""
|
||||
glob pattern specifying index files; if not empty,
|
||||
the index is read from these files in sorted order
|
||||
-index_throttle=0.75
|
||||
index throttle value; a value of 0 means no time is allocated
|
||||
to the indexer (the indexer will never finish), a value of 1.0
|
||||
means that index creation is running at full throttle (other
|
||||
goroutines may get no time while the index is built)
|
||||
-index_interval=0
|
||||
interval of indexing; a value of 0 sets it to 5 minutes, a
|
||||
negative value indexes only once at startup
|
||||
-play=false
|
||||
enable playground
|
||||
-links=true
|
||||
link identifiers to their declarations
|
||||
-write_index=false
|
||||
write index to a file; the file name must be specified with
|
||||
-index_files
|
||||
-maxresults=10000
|
||||
maximum number of full text search results shown
|
||||
(no full text index is built if maxresults <= 0)
|
||||
-notes="BUG"
|
||||
regular expression matching note markers to show
|
||||
(e.g., "BUG|TODO", ".*")
|
||||
-goroot=$GOROOT
|
||||
Go root directory
|
||||
-http=addr
|
||||
HTTP service address (e.g., '127.0.0.1:6060' or just ':6060')
|
||||
-analysis=type,pointer
|
||||
comma-separated list of analyses to perform
|
||||
"type": display identifier resolution, type info, method sets,
|
||||
'implements', and static callees
|
||||
"pointer": display channel peers, callers and dynamic callees
|
||||
(significantly slower)
|
||||
See https://golang.org/lib/godoc/analysis/help.html for details.
|
||||
-templates=""
|
||||
directory containing alternate template files; if set,
|
||||
the directory may provide alternative template files
|
||||
for the files in $GOROOT/lib/godoc
|
||||
-url=path
|
||||
print to standard output the data that would be served by
|
||||
an HTTP request for path
|
||||
-zip=""
|
||||
zip file providing the file system to serve; disabled if empty
|
||||
|
||||
By default, godoc looks at the packages it finds via $GOROOT and $GOPATH (if set).
|
||||
This behavior can be altered by providing an alternative $GOROOT with the -goroot
|
||||
flag.
|
||||
|
||||
When the -index flag is set, a search index is maintained.
|
||||
The index is created at startup.
|
||||
|
||||
The index contains both identifier and full text search information (searchable
|
||||
via regular expressions). The maximum number of full text search results shown
|
||||
can be set with the -maxresults flag; if set to 0, no full text results are
|
||||
shown, and only an identifier index but no full text search index is created.
|
||||
|
||||
By default, godoc uses the system's GOOS/GOARCH. You can provide the URL parameters
|
||||
"GOOS" and "GOARCH" to set the output on the web page for the target system.
|
||||
|
||||
The presentation mode of web pages served by godoc can be controlled with the
|
||||
"m" URL parameter; it accepts a comma-separated list of flag names as value:
|
||||
|
||||
all show documentation for all declarations, not just the exported ones
|
||||
methods show all embedded methods, not just those of unexported anonymous fields
|
||||
src show the original source code rather then the extracted documentation
|
||||
|
||||
For instance, https://golang.org/pkg/math/big/?m=all shows the documentation
|
||||
for all (not just the exported) declarations of package big.
|
||||
|
||||
By default, godoc serves files from the file system of the underlying OS.
|
||||
Instead, a .zip file may be provided via the -zip flag, which contains
|
||||
the file system to serve. The file paths stored in the .zip file must use
|
||||
slash ('/') as path separator; and they must be unrooted. $GOROOT (or -goroot)
|
||||
must be set to the .zip file directory path containing the Go root directory.
|
||||
For instance, for a .zip file created by the command:
|
||||
|
||||
zip -r go.zip $HOME/go
|
||||
|
||||
one may run godoc as follows:
|
||||
|
||||
godoc -http=:6060 -zip=go.zip -goroot=$HOME/go
|
||||
|
||||
Godoc documentation is converted to HTML or to text using the go/doc package;
|
||||
see https://golang.org/pkg/go/doc/#ToHTML for the exact rules.
|
||||
Godoc also shows example code that is runnable by the testing package;
|
||||
see https://golang.org/pkg/testing/#hdr-Examples for the conventions.
|
||||
See "Godoc: documenting Go code" for how to write good comments for godoc:
|
||||
https://golang.org/doc/articles/godoc_documenting_go_code.html
|
||||
|
||||
*/
|
||||
package main // import "golang.org/x/website/cmd/golangorg"
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
# This script creates a .zip file representing the $GOROOT file system
|
||||
# and computes the corresponding search index files.
|
||||
#
|
||||
# These are used in production (see app.prod.yaml)
|
||||
|
||||
set -e -u -x
|
||||
|
||||
ZIPFILE=godoc.zip
|
||||
INDEXFILE=godoc.index
|
||||
SPLITFILES=index.split.
|
||||
|
||||
error() {
|
||||
echo "error: $1"
|
||||
exit 2
|
||||
}
|
||||
|
||||
install() {
|
||||
go install
|
||||
}
|
||||
|
||||
getArgs() {
|
||||
if [ ! -v GODOC_DOCSET ]; then
|
||||
GODOC_DOCSET="$(go env GOROOT)"
|
||||
echo "GODOC_DOCSET not set explicitly, using GOROOT instead"
|
||||
fi
|
||||
|
||||
# safety checks
|
||||
if [ ! -d "$GODOC_DOCSET" ]; then
|
||||
error "$GODOC_DOCSET is not a directory"
|
||||
fi
|
||||
|
||||
# reporting
|
||||
echo "GODOC_DOCSET = $GODOC_DOCSET"
|
||||
}
|
||||
|
||||
makeZipfile() {
|
||||
echo "*** make $ZIPFILE"
|
||||
rm -f $ZIPFILE goroot
|
||||
ln -s "$GODOC_DOCSET" goroot
|
||||
zip -q -r $ZIPFILE goroot/* # glob to ignore dotfiles (like .git)
|
||||
rm goroot
|
||||
}
|
||||
|
||||
makeIndexfile() {
|
||||
echo "*** make $INDEXFILE"
|
||||
godoc=$(go env GOPATH)/bin/godoc
|
||||
# NOTE: run godoc without GOPATH set. Otherwise third-party packages will end up in the index.
|
||||
GOPATH= $godoc -write_index -goroot goroot -index_files=$INDEXFILE -zip=$ZIPFILE
|
||||
}
|
||||
|
||||
splitIndexfile() {
|
||||
echo "*** split $INDEXFILE"
|
||||
rm -f $SPLITFILES*
|
||||
split -b8m $INDEXFILE $SPLITFILES
|
||||
}
|
||||
|
||||
cd $(dirname $0)
|
||||
|
||||
install
|
||||
getArgs "$@"
|
||||
makeZipfile
|
||||
makeIndexfile
|
||||
splitIndexfile
|
||||
rm $INDEXFILE
|
||||
|
||||
echo "*** setup complete"
|
|
@ -0,0 +1,111 @@
|
|||
|
||||
Type and Pointer Analysis to-do list
|
||||
====================================
|
||||
|
||||
Alan Donovan <adonovan@google.com>
|
||||
|
||||
|
||||
Overall design
|
||||
--------------
|
||||
|
||||
We should re-run the type and pointer analyses periodically,
|
||||
as we do with the indexer.
|
||||
|
||||
Version skew: how to mitigate the bad effects of stale URLs in old pages?
|
||||
We could record the file's length/CRC32/mtime in the go/loader, and
|
||||
refuse to decorate it with links unless they match at serving time.
|
||||
|
||||
Use the VFS mechanism when (a) enumerating packages and (b) loading
|
||||
them. (Requires planned changes to go/loader.)
|
||||
|
||||
Future work: shard this using map/reduce for larger corpora.
|
||||
|
||||
Testing: how does one test that a web page "looks right"?
|
||||
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
(*ssa.Program).Create requires transitively error-free packages. We
|
||||
can make this more robust by making the requirement transitively free
|
||||
of "hard" errors; soft errors are fine.
|
||||
|
||||
Markup of compiler errors is slightly buggy because they overlap with
|
||||
other selections (e.g. Idents). Fix.
|
||||
|
||||
|
||||
User Interface
|
||||
--------------
|
||||
|
||||
CALLGRAPH:
|
||||
- Add a search box: given a search node, expand path from each entry
|
||||
point to it.
|
||||
- Cause hovering over a given node to highlight that node, and all
|
||||
nodes that are logically identical to it.
|
||||
- Initially expand the callgraph trees (but not their toggle divs).
|
||||
|
||||
CALLEES:
|
||||
- The '(' links are not very discoverable. Highlight them?
|
||||
|
||||
Type info:
|
||||
- In the source viewer's lower pane, use a toggle div around the
|
||||
IMPLEMENTS and METHODSETS lists, like we do in the package view.
|
||||
Only expand them initially if short.
|
||||
- Include IMPLEMENTS and METHOD SETS information in search index.
|
||||
- URLs in IMPLEMENTS/METHOD SETS always link to source, even from the
|
||||
package docs view. This makes sense for links to non-exported
|
||||
types, but links to exported types and funcs should probably go to
|
||||
other package docs.
|
||||
- Suppress toggle divs for empty method sets.
|
||||
|
||||
Misc:
|
||||
- The [X] button in the lower pane is subject to scrolling.
|
||||
- Should the lower pane be floating? An iframe?
|
||||
When we change document.location by clicking on a link, it will go away.
|
||||
How do we prevent that (a la Gmail's chat windows)?
|
||||
- Progress/status: for each file, display its analysis status, one of:
|
||||
- not in analysis scope
|
||||
- type analysis running...
|
||||
- type analysis complete
|
||||
(+ optionally: there were type errors in this file)
|
||||
And if PTA requested:
|
||||
- type analysis complete; PTA not attempted due to type errors
|
||||
- PTA running...
|
||||
- PTA complete
|
||||
- Scroll the selection into view, e.g. the vertical center, or better
|
||||
still, under the pointer (assuming we have a mouse).
|
||||
|
||||
|
||||
More features
|
||||
-------------
|
||||
|
||||
Display the REFERRERS relation? (Useful but potentially large.)
|
||||
|
||||
Display the INSTANTIATIONS relation? i.e. given a type T, show the set of
|
||||
syntactic constructs that can instantiate it:
|
||||
var x T
|
||||
x := T{...}
|
||||
x = new(T)
|
||||
x = make([]T, n)
|
||||
etc
|
||||
+ all INSTANTIATIONS of all S defined as struct{t T} or [n]T
|
||||
(Potentially a lot of information.)
|
||||
(Add this to guru too.)
|
||||
|
||||
|
||||
Optimisations
|
||||
-------------
|
||||
|
||||
Each call to addLink takes a (per-file) lock. The locking is
|
||||
fine-grained so server latency isn't terrible, but overall it makes
|
||||
the link computation quite slow. Batch update might be better.
|
||||
|
||||
Memory usage is now about 1.5GB for GOROOT + go.tools. It used to be 700MB.
|
||||
|
||||
Optimize for time and space. The main slowdown is the network I/O
|
||||
time caused by an increase in page size of about 3x: about 2x from
|
||||
HTML, and 0.7--2.1x from JSON (unindented vs indented). The JSON
|
||||
contains a lot of filenames (e.g. 820 copies of 16 distinct
|
||||
filenames). 20% of the HTML is L%d spans (now disabled). The HTML
|
||||
also contains lots of tooltips for long struct/interface types.
|
||||
De-dup or just abbreviate? The actual formatting is very fast.
|
|
@ -0,0 +1,613 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
// Package analysis performs type and pointer analysis
|
||||
// and generates mark-up for the Go source view.
|
||||
//
|
||||
// The Run method populates a Result object by running type and
|
||||
// (optionally) pointer analysis. The Result object is thread-safe
|
||||
// and at all times may be accessed by a serving thread, even as it is
|
||||
// progressively populated as analysis facts are derived.
|
||||
//
|
||||
// The Result is a mapping from each godoc file URL
|
||||
// (e.g. /src/fmt/print.go) to information about that file. The
|
||||
// information is a list of HTML markup links and a JSON array of
|
||||
// structured data values. Some of the links call client-side
|
||||
// JavaScript functions that index this array.
|
||||
//
|
||||
// The analysis computes mark-up for the following relations:
|
||||
//
|
||||
// IMPORTS: for each ast.ImportSpec, the package that it denotes.
|
||||
//
|
||||
// RESOLUTION: for each ast.Ident, its kind and type, and the location
|
||||
// of its definition.
|
||||
//
|
||||
// METHOD SETS, IMPLEMENTS: for each ast.Ident defining a named type,
|
||||
// its method-set, the set of interfaces it implements or is
|
||||
// implemented by, and its size/align values.
|
||||
//
|
||||
// CALLERS, CALLEES: for each function declaration ('func' token), its
|
||||
// callers, and for each call-site ('(' token), its callees.
|
||||
//
|
||||
// CALLGRAPH: the package docs include an interactive viewer for the
|
||||
// intra-package call graph of "fmt".
|
||||
//
|
||||
// CHANNEL PEERS: for each channel operation make/<-/close, the set of
|
||||
// other channel ops that alias the same channel(s).
|
||||
//
|
||||
// ERRORS: for each locus of a frontend (scanner/parser/type) error, the
|
||||
// location is highlighted in red and hover text provides the compiler
|
||||
// error message.
|
||||
//
|
||||
package analysis // import "golang.org/x/website/cmd/golangorg/godoc/analysis"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/pointer"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
)
|
||||
|
||||
// -- links ------------------------------------------------------------
|
||||
|
||||
// A Link is an HTML decoration of the bytes [Start, End) of a file.
|
||||
// Write is called before/after those bytes to emit the mark-up.
|
||||
type Link interface {
|
||||
Start() int
|
||||
End() int
|
||||
Write(w io.Writer, _ int, start bool) // the godoc.LinkWriter signature
|
||||
}
|
||||
|
||||
// An <a> element.
|
||||
type aLink struct {
|
||||
start, end int // =godoc.Segment
|
||||
title string // hover text
|
||||
onclick string // JS code (NB: trusted)
|
||||
href string // URL (NB: trusted)
|
||||
}
|
||||
|
||||
func (a aLink) Start() int { return a.start }
|
||||
func (a aLink) End() int { return a.end }
|
||||
func (a aLink) Write(w io.Writer, _ int, start bool) {
|
||||
if start {
|
||||
fmt.Fprintf(w, `<a title='%s'`, html.EscapeString(a.title))
|
||||
if a.onclick != "" {
|
||||
fmt.Fprintf(w, ` onclick='%s'`, html.EscapeString(a.onclick))
|
||||
}
|
||||
if a.href != "" {
|
||||
// TODO(adonovan): I think that in principle, a.href must first be
|
||||
// url.QueryEscape'd, but if I do that, a leading slash becomes "%2F",
|
||||
// which causes the browser to treat the path as relative, not absolute.
|
||||
// WTF?
|
||||
fmt.Fprintf(w, ` href='%s'`, html.EscapeString(a.href))
|
||||
}
|
||||
fmt.Fprintf(w, ">")
|
||||
} else {
|
||||
fmt.Fprintf(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
// An <a class='error'> element.
|
||||
type errorLink struct {
|
||||
start int
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e errorLink) Start() int { return e.start }
|
||||
func (e errorLink) End() int { return e.start + 1 }
|
||||
|
||||
func (e errorLink) Write(w io.Writer, _ int, start bool) {
|
||||
// <span> causes havoc, not sure why, so use <a>.
|
||||
if start {
|
||||
fmt.Fprintf(w, `<a class='error' title='%s'>`, html.EscapeString(e.msg))
|
||||
} else {
|
||||
fmt.Fprintf(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
// -- fileInfo ---------------------------------------------------------
|
||||
|
||||
// FileInfo holds analysis information for the source file view.
|
||||
// Clients must not mutate it.
|
||||
type FileInfo struct {
|
||||
Data []interface{} // JSON serializable values
|
||||
Links []Link // HTML link markup
|
||||
}
|
||||
|
||||
// A fileInfo is the server's store of hyperlinks and JSON data for a
|
||||
// particular file.
|
||||
type fileInfo struct {
|
||||
mu sync.Mutex
|
||||
data []interface{} // JSON objects
|
||||
links []Link
|
||||
sorted bool
|
||||
hasErrors bool // TODO(adonovan): surface this in the UI
|
||||
}
|
||||
|
||||
// addLink adds a link to the Go source file fi.
|
||||
func (fi *fileInfo) addLink(link Link) {
|
||||
fi.mu.Lock()
|
||||
fi.links = append(fi.links, link)
|
||||
fi.sorted = false
|
||||
if _, ok := link.(errorLink); ok {
|
||||
fi.hasErrors = true
|
||||
}
|
||||
fi.mu.Unlock()
|
||||
}
|
||||
|
||||
// addData adds the structured value x to the JSON data for the Go
|
||||
// source file fi. Its index is returned.
|
||||
func (fi *fileInfo) addData(x interface{}) int {
|
||||
fi.mu.Lock()
|
||||
index := len(fi.data)
|
||||
fi.data = append(fi.data, x)
|
||||
fi.mu.Unlock()
|
||||
return index
|
||||
}
|
||||
|
||||
// get returns the file info in external form.
|
||||
// Callers must not mutate its fields.
|
||||
func (fi *fileInfo) get() FileInfo {
|
||||
var r FileInfo
|
||||
// Copy slices, to avoid races.
|
||||
fi.mu.Lock()
|
||||
r.Data = append(r.Data, fi.data...)
|
||||
if !fi.sorted {
|
||||
sort.Sort(linksByStart(fi.links))
|
||||
fi.sorted = true
|
||||
}
|
||||
r.Links = append(r.Links, fi.links...)
|
||||
fi.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// PackageInfo holds analysis information for the package view.
|
||||
// Clients must not mutate it.
|
||||
type PackageInfo struct {
|
||||
CallGraph []*PCGNodeJSON
|
||||
CallGraphIndex map[string]int
|
||||
Types []*TypeInfoJSON
|
||||
}
|
||||
|
||||
type pkgInfo struct {
|
||||
mu sync.Mutex
|
||||
callGraph []*PCGNodeJSON
|
||||
callGraphIndex map[string]int // keys are (*ssa.Function).RelString()
|
||||
types []*TypeInfoJSON // type info for exported types
|
||||
}
|
||||
|
||||
func (pi *pkgInfo) setCallGraph(callGraph []*PCGNodeJSON, callGraphIndex map[string]int) {
|
||||
pi.mu.Lock()
|
||||
pi.callGraph = callGraph
|
||||
pi.callGraphIndex = callGraphIndex
|
||||
pi.mu.Unlock()
|
||||
}
|
||||
|
||||
func (pi *pkgInfo) addType(t *TypeInfoJSON) {
|
||||
pi.mu.Lock()
|
||||
pi.types = append(pi.types, t)
|
||||
pi.mu.Unlock()
|
||||
}
|
||||
|
||||
// get returns the package info in external form.
|
||||
// Callers must not mutate its fields.
|
||||
func (pi *pkgInfo) get() PackageInfo {
|
||||
var r PackageInfo
|
||||
// Copy slices, to avoid races.
|
||||
pi.mu.Lock()
|
||||
r.CallGraph = append(r.CallGraph, pi.callGraph...)
|
||||
r.CallGraphIndex = pi.callGraphIndex
|
||||
r.Types = append(r.Types, pi.types...)
|
||||
pi.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// -- Result -----------------------------------------------------------
|
||||
|
||||
// Result contains the results of analysis.
|
||||
// The result contains a mapping from filenames to a set of HTML links
|
||||
// and JavaScript data referenced by the links.
|
||||
type Result struct {
|
||||
mu sync.Mutex // guards maps (but not their contents)
|
||||
status string // global analysis status
|
||||
fileInfos map[string]*fileInfo // keys are godoc file URLs
|
||||
pkgInfos map[string]*pkgInfo // keys are import paths
|
||||
}
|
||||
|
||||
// fileInfo returns the fileInfo for the specified godoc file URL,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) fileInfo(url string) *fileInfo {
|
||||
res.mu.Lock()
|
||||
fi, ok := res.fileInfos[url]
|
||||
if !ok {
|
||||
if res.fileInfos == nil {
|
||||
res.fileInfos = make(map[string]*fileInfo)
|
||||
}
|
||||
fi = new(fileInfo)
|
||||
res.fileInfos[url] = fi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return fi
|
||||
}
|
||||
|
||||
// Status returns a human-readable description of the current analysis status.
|
||||
func (res *Result) Status() string {
|
||||
res.mu.Lock()
|
||||
defer res.mu.Unlock()
|
||||
return res.status
|
||||
}
|
||||
|
||||
func (res *Result) setStatusf(format string, args ...interface{}) {
|
||||
res.mu.Lock()
|
||||
res.status = fmt.Sprintf(format, args...)
|
||||
log.Printf(format, args...)
|
||||
res.mu.Unlock()
|
||||
}
|
||||
|
||||
// FileInfo returns new slices containing opaque JSON values and the
|
||||
// HTML link markup for the specified godoc file URL. Thread-safe.
|
||||
// Callers must not mutate the elements.
|
||||
// It returns "zero" if no data is available.
|
||||
//
|
||||
func (res *Result) FileInfo(url string) (fi FileInfo) {
|
||||
return res.fileInfo(url).get()
|
||||
}
|
||||
|
||||
// pkgInfo returns the pkgInfo for the specified import path,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) pkgInfo(importPath string) *pkgInfo {
|
||||
res.mu.Lock()
|
||||
pi, ok := res.pkgInfos[importPath]
|
||||
if !ok {
|
||||
if res.pkgInfos == nil {
|
||||
res.pkgInfos = make(map[string]*pkgInfo)
|
||||
}
|
||||
pi = new(pkgInfo)
|
||||
res.pkgInfos[importPath] = pi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return pi
|
||||
}
|
||||
|
||||
// PackageInfo returns new slices of JSON values for the callgraph and
|
||||
// type info for the specified package. Thread-safe.
|
||||
// Callers must not mutate its fields.
|
||||
// PackageInfo returns "zero" if no data is available.
|
||||
//
|
||||
func (res *Result) PackageInfo(importPath string) PackageInfo {
|
||||
return res.pkgInfo(importPath).get()
|
||||
}
|
||||
|
||||
// -- analysis ---------------------------------------------------------
|
||||
|
||||
type analysis struct {
|
||||
result *Result
|
||||
prog *ssa.Program
|
||||
ops []chanOp // all channel ops in program
|
||||
allNamed []*types.Named // all "defined" (formerly "named") types in the program
|
||||
ptaConfig pointer.Config
|
||||
path2url map[string]string // maps openable path to godoc file URL (/src/fmt/print.go)
|
||||
pcgs map[*ssa.Package]*packageCallGraph
|
||||
}
|
||||
|
||||
// fileAndOffset returns the file and offset for a given pos.
|
||||
func (a *analysis) fileAndOffset(pos token.Pos) (fi *fileInfo, offset int) {
|
||||
return a.fileAndOffsetPosn(a.prog.Fset.Position(pos))
|
||||
}
|
||||
|
||||
// fileAndOffsetPosn returns the file and offset for a given position.
|
||||
func (a *analysis) fileAndOffsetPosn(posn token.Position) (fi *fileInfo, offset int) {
|
||||
url := a.path2url[posn.Filename]
|
||||
return a.result.fileInfo(url), posn.Offset
|
||||
}
|
||||
|
||||
// posURL returns the URL of the source extent [pos, pos+len).
|
||||
func (a *analysis) posURL(pos token.Pos, len int) string {
|
||||
if pos == token.NoPos {
|
||||
return ""
|
||||
}
|
||||
posn := a.prog.Fset.Position(pos)
|
||||
url := a.path2url[posn.Filename]
|
||||
return fmt.Sprintf("%s?s=%d:%d#L%d",
|
||||
url, posn.Offset, posn.Offset+len, posn.Line)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Run runs program analysis and computes the resulting markup,
|
||||
// populating *result in a thread-safe manner, first with type
|
||||
// information then later with pointer analysis information if
|
||||
// enabled by the pta flag.
|
||||
//
|
||||
func Run(pta bool, result *Result) {
|
||||
conf := loader.Config{
|
||||
AllowErrors: true,
|
||||
}
|
||||
|
||||
// Silence the default error handler.
|
||||
// Don't print all errors; we'll report just
|
||||
// one per errant package later.
|
||||
conf.TypeChecker.Error = func(e error) {}
|
||||
|
||||
var roots, args []string // roots[i] ends with os.PathSeparator
|
||||
|
||||
// Enumerate packages in $GOROOT.
|
||||
root := filepath.Join(build.Default.GOROOT, "src") + string(os.PathSeparator)
|
||||
roots = append(roots, root)
|
||||
args = allPackages(root)
|
||||
log.Printf("GOROOT=%s: %s\n", root, args)
|
||||
|
||||
// Enumerate packages in $GOPATH.
|
||||
for i, dir := range filepath.SplitList(build.Default.GOPATH) {
|
||||
root := filepath.Join(dir, "src") + string(os.PathSeparator)
|
||||
roots = append(roots, root)
|
||||
pkgs := allPackages(root)
|
||||
log.Printf("GOPATH[%d]=%s: %s\n", i, root, pkgs)
|
||||
args = append(args, pkgs...)
|
||||
}
|
||||
|
||||
// Uncomment to make startup quicker during debugging.
|
||||
//args = []string{"golang.org/x/website/cmd/golangorg"}
|
||||
//args = []string{"fmt"}
|
||||
|
||||
if _, err := conf.FromArgs(args, true); err != nil {
|
||||
// TODO(adonovan): degrade gracefully, not fail totally.
|
||||
// (The crippling case is a parse error in an external test file.)
|
||||
result.setStatusf("Analysis failed: %s.", err) // import error
|
||||
return
|
||||
}
|
||||
|
||||
result.setStatusf("Loading and type-checking packages...")
|
||||
iprog, err := conf.Load()
|
||||
if iprog != nil {
|
||||
// Report only the first error of each package.
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, err := range info.Errors {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Printf("Loaded %d packages.", len(iprog.AllPackages))
|
||||
}
|
||||
if err != nil {
|
||||
result.setStatusf("Loading failed: %s.\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create SSA-form program representation.
|
||||
// Only the transitively error-free packages are used.
|
||||
prog := ssautil.CreateProgram(iprog, ssa.GlobalDebug)
|
||||
|
||||
// Create a "testmain" package for each package with tests.
|
||||
for _, pkg := range prog.AllPackages() {
|
||||
if testmain := prog.CreateTestMainPackage(pkg); testmain != nil {
|
||||
log.Printf("Adding tests for %s", pkg.Pkg.Path())
|
||||
}
|
||||
}
|
||||
|
||||
// Build SSA code for bodies of all functions in the whole program.
|
||||
result.setStatusf("Constructing SSA form...")
|
||||
prog.Build()
|
||||
log.Print("SSA construction complete")
|
||||
|
||||
a := analysis{
|
||||
result: result,
|
||||
prog: prog,
|
||||
pcgs: make(map[*ssa.Package]*packageCallGraph),
|
||||
}
|
||||
|
||||
// Build a mapping from openable filenames to godoc file URLs,
|
||||
// i.e. "/src/" plus path relative to GOROOT/src or GOPATH[i]/src.
|
||||
a.path2url = make(map[string]string)
|
||||
for _, info := range iprog.AllPackages {
|
||||
nextfile:
|
||||
for _, f := range info.Files {
|
||||
if f.Pos() == 0 {
|
||||
continue // e.g. files generated by cgo
|
||||
}
|
||||
abs := iprog.Fset.File(f.Pos()).Name()
|
||||
// Find the root to which this file belongs.
|
||||
for _, root := range roots {
|
||||
rel := strings.TrimPrefix(abs, root)
|
||||
if len(rel) < len(abs) {
|
||||
a.path2url[abs] = "/src/" + filepath.ToSlash(rel)
|
||||
continue nextfile
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Can't locate file %s (package %q) beneath any root",
|
||||
abs, info.Pkg.Path())
|
||||
}
|
||||
}
|
||||
|
||||
// Add links for scanner, parser, type-checker errors.
|
||||
// TODO(adonovan): fix: these links can overlap with
|
||||
// identifier markup, causing the renderer to emit some
|
||||
// characters twice.
|
||||
errors := make(map[token.Position][]string)
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, err := range info.Errors {
|
||||
switch err := err.(type) {
|
||||
case types.Error:
|
||||
posn := a.prog.Fset.Position(err.Pos)
|
||||
errors[posn] = append(errors[posn], err.Msg)
|
||||
case scanner.ErrorList:
|
||||
for _, e := range err {
|
||||
errors[e.Pos] = append(errors[e.Pos], e.Msg)
|
||||
}
|
||||
default:
|
||||
log.Printf("Package %q has error (%T) without position: %v\n",
|
||||
info.Pkg.Path(), err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for posn, errs := range errors {
|
||||
fi, offset := a.fileAndOffsetPosn(posn)
|
||||
fi.addLink(errorLink{
|
||||
start: offset,
|
||||
msg: strings.Join(errs, "\n"),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- type-based analyses ----------
|
||||
|
||||
// Compute the all-pairs IMPLEMENTS relation.
|
||||
// Collect all named types, even local types
|
||||
// (which can have methods via promotion)
|
||||
// and the built-in "error".
|
||||
errorType := types.Universe.Lookup("error").Type().(*types.Named)
|
||||
a.allNamed = append(a.allNamed, errorType)
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
if named, ok := obj.Type().(*types.Named); ok {
|
||||
a.allNamed = append(a.allNamed, named)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Print("Computing implements relation...")
|
||||
facts := computeImplements(&a.prog.MethodSets, a.allNamed)
|
||||
|
||||
// Add the type-based analysis results.
|
||||
log.Print("Extracting type info...")
|
||||
for _, info := range iprog.AllPackages {
|
||||
a.doTypeInfo(info, facts)
|
||||
}
|
||||
|
||||
a.visitInstrs(pta)
|
||||
|
||||
result.setStatusf("Type analysis complete.")
|
||||
|
||||
if pta {
|
||||
mainPkgs := ssautil.MainPackages(prog.AllPackages())
|
||||
log.Print("Transitively error-free main packages: ", mainPkgs)
|
||||
a.pointer(mainPkgs)
|
||||
}
|
||||
}
|
||||
|
||||
// visitInstrs visits all SSA instructions in the program.
|
||||
func (a *analysis) visitInstrs(pta bool) {
|
||||
log.Print("Visit instructions...")
|
||||
for fn := range ssautil.AllFunctions(a.prog) {
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
// CALLEES (static)
|
||||
// (Dynamic calls require pointer analysis.)
|
||||
//
|
||||
// We use the SSA representation to find the static callee,
|
||||
// since in many cases it does better than the
|
||||
// types.Info.{Refs,Selection} information. For example:
|
||||
//
|
||||
// defer func(){}() // static call to anon function
|
||||
// f := func(){}; f() // static call to anon function
|
||||
// f := fmt.Println; f() // static call to named function
|
||||
//
|
||||
// The downside is that we get no static callee information
|
||||
// for packages that (transitively) contain errors.
|
||||
if site, ok := instr.(ssa.CallInstruction); ok {
|
||||
if callee := site.Common().StaticCallee(); callee != nil {
|
||||
// TODO(adonovan): callgraph: elide wrappers.
|
||||
// (Do static calls ever go to wrappers?)
|
||||
if site.Common().Pos() != token.NoPos {
|
||||
a.addCallees(site, []*ssa.Function{callee})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pta {
|
||||
continue
|
||||
}
|
||||
|
||||
// CHANNEL PEERS
|
||||
// Collect send/receive/close instructions in the whole ssa.Program.
|
||||
for _, op := range chanOps(instr) {
|
||||
a.ops = append(a.ops, op)
|
||||
a.ptaConfig.AddQuery(op.ch) // add channel ssa.Value to PTA query
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Print("Visit instructions complete")
|
||||
}
|
||||
|
||||
// pointer runs the pointer analysis.
|
||||
func (a *analysis) pointer(mainPkgs []*ssa.Package) {
|
||||
// Run the pointer analysis and build the complete callgraph.
|
||||
a.ptaConfig.Mains = mainPkgs
|
||||
a.ptaConfig.BuildCallGraph = true
|
||||
a.ptaConfig.Reflection = false // (for now)
|
||||
|
||||
a.result.setStatusf("Pointer analysis running...")
|
||||
|
||||
ptares, err := pointer.Analyze(&a.ptaConfig)
|
||||
if err != nil {
|
||||
// If this happens, it indicates a bug.
|
||||
a.result.setStatusf("Pointer analysis failed: %s.", err)
|
||||
return
|
||||
}
|
||||
log.Print("Pointer analysis complete.")
|
||||
|
||||
// Add the results of pointer analysis.
|
||||
|
||||
a.result.setStatusf("Computing channel peers...")
|
||||
a.doChannelPeers(ptares.Queries)
|
||||
a.result.setStatusf("Computing dynamic call graph edges...")
|
||||
a.doCallgraph(ptares.CallGraph)
|
||||
|
||||
a.result.setStatusf("Analysis complete.")
|
||||
}
|
||||
|
||||
type linksByStart []Link
|
||||
|
||||
func (a linksByStart) Less(i, j int) bool { return a[i].Start() < a[j].Start() }
|
||||
func (a linksByStart) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a linksByStart) Len() int { return len(a) }
|
||||
|
||||
// allPackages returns a new sorted slice of all packages beneath the
|
||||
// specified package root directory, e.g. $GOROOT/src or $GOPATH/src.
|
||||
// Derived from from go/ssa/stdlib_test.go
|
||||
// root must end with os.PathSeparator.
|
||||
//
|
||||
// TODO(adonovan): use buildutil.AllPackages when the tree thaws.
|
||||
func allPackages(root string) []string {
|
||||
var pkgs []string
|
||||
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil // non-existent root directory?
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil // not a directory
|
||||
}
|
||||
// Prune the search if we encounter any of these names:
|
||||
base := filepath.Base(path)
|
||||
if base == "testdata" || strings.HasPrefix(base, ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
pkg := filepath.ToSlash(strings.TrimPrefix(path, root))
|
||||
switch pkg {
|
||||
case "builtin":
|
||||
return filepath.SkipDir
|
||||
case "":
|
||||
return nil // ignore root of tree
|
||||
}
|
||||
pkgs = append(pkgs, pkg)
|
||||
return nil
|
||||
})
|
||||
return pkgs
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the CALLERS and CALLEES relations from the call
|
||||
// graph. CALLERS/CALLEES information is displayed in the lower pane
|
||||
// when a "func" token or ast.CallExpr.Lparen is clicked, respectively.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
)
|
||||
|
||||
// doCallgraph computes the CALLEES and CALLERS relations.
|
||||
func (a *analysis) doCallgraph(cg *callgraph.Graph) {
|
||||
log.Print("Deleting synthetic nodes...")
|
||||
// TODO(adonovan): opt: DeleteSyntheticNodes is asymptotically
|
||||
// inefficient and can be (unpredictably) slow.
|
||||
cg.DeleteSyntheticNodes()
|
||||
log.Print("Synthetic nodes deleted")
|
||||
|
||||
// Populate nodes of package call graphs (PCGs).
|
||||
for _, n := range cg.Nodes {
|
||||
a.pcgAddNode(n.Func)
|
||||
}
|
||||
// Within each PCG, sort funcs by name.
|
||||
for _, pcg := range a.pcgs {
|
||||
pcg.sortNodes()
|
||||
}
|
||||
|
||||
calledFuncs := make(map[ssa.CallInstruction]map[*ssa.Function]bool)
|
||||
callingSites := make(map[*ssa.Function]map[ssa.CallInstruction]bool)
|
||||
for _, n := range cg.Nodes {
|
||||
for _, e := range n.Out {
|
||||
if e.Site == nil {
|
||||
continue // a call from a synthetic node such as <root>
|
||||
}
|
||||
|
||||
// Add (site pos, callee) to calledFuncs.
|
||||
// (Dynamic calls only.)
|
||||
callee := e.Callee.Func
|
||||
|
||||
a.pcgAddEdge(n.Func, callee)
|
||||
|
||||
if callee.Synthetic != "" {
|
||||
continue // call of a package initializer
|
||||
}
|
||||
|
||||
if e.Site.Common().StaticCallee() == nil {
|
||||
// dynamic call
|
||||
// (CALLEES information for static calls
|
||||
// is computed using SSA information.)
|
||||
lparen := e.Site.Common().Pos()
|
||||
if lparen != token.NoPos {
|
||||
fns := calledFuncs[e.Site]
|
||||
if fns == nil {
|
||||
fns = make(map[*ssa.Function]bool)
|
||||
calledFuncs[e.Site] = fns
|
||||
}
|
||||
fns[callee] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add (callee, site) to callingSites.
|
||||
fns := callingSites[callee]
|
||||
if fns == nil {
|
||||
fns = make(map[ssa.CallInstruction]bool)
|
||||
callingSites[callee] = fns
|
||||
}
|
||||
fns[e.Site] = true
|
||||
}
|
||||
}
|
||||
|
||||
// CALLEES.
|
||||
log.Print("Callees...")
|
||||
for site, fns := range calledFuncs {
|
||||
var funcs funcsByPos
|
||||
for fn := range fns {
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
sort.Sort(funcs)
|
||||
|
||||
a.addCallees(site, funcs)
|
||||
}
|
||||
|
||||
// CALLERS
|
||||
log.Print("Callers...")
|
||||
for callee, sites := range callingSites {
|
||||
pos := funcToken(callee)
|
||||
if pos == token.NoPos {
|
||||
log.Printf("CALLERS: skipping %s: no pos", callee)
|
||||
continue
|
||||
}
|
||||
|
||||
var this *types.Package // for relativizing names
|
||||
if callee.Pkg != nil {
|
||||
this = callee.Pkg.Pkg
|
||||
}
|
||||
|
||||
// Compute sites grouped by parent, with text and URLs.
|
||||
sitesByParent := make(map[*ssa.Function]sitesByPos)
|
||||
for site := range sites {
|
||||
fn := site.Parent()
|
||||
sitesByParent[fn] = append(sitesByParent[fn], site)
|
||||
}
|
||||
var funcs funcsByPos
|
||||
for fn := range sitesByParent {
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
sort.Sort(funcs)
|
||||
|
||||
v := callersJSON{
|
||||
Callee: callee.String(),
|
||||
Callers: []callerJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
caller := callerJSON{
|
||||
Func: prettyFunc(this, fn),
|
||||
Sites: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
sites := sitesByParent[fn]
|
||||
sort.Sort(sites)
|
||||
for _, site := range sites {
|
||||
pos := site.Common().Pos()
|
||||
if pos != token.NoPos {
|
||||
caller.Sites = append(caller.Sites, anchorJSON{
|
||||
Text: fmt.Sprintf("%d", a.prog.Fset.Position(pos).Line),
|
||||
Href: a.posURL(pos, len("(")),
|
||||
})
|
||||
}
|
||||
}
|
||||
v.Callers = append(v.Callers, caller)
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(pos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len("func"),
|
||||
title: fmt.Sprintf("%d callers", len(sites)),
|
||||
onclick: fmt.Sprintf("onClickCallers(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
|
||||
// PACKAGE CALLGRAPH
|
||||
log.Print("Package call graph...")
|
||||
for pkg, pcg := range a.pcgs {
|
||||
// Maps (*ssa.Function).RelString() to index in JSON CALLGRAPH array.
|
||||
index := make(map[string]int)
|
||||
|
||||
// Treat exported functions (and exported methods of
|
||||
// exported named types) as roots even if they aren't
|
||||
// actually called from outside the package.
|
||||
for i, n := range pcg.nodes {
|
||||
if i == 0 || n.fn.Object() == nil || !n.fn.Object().Exported() {
|
||||
continue
|
||||
}
|
||||
recv := n.fn.Signature.Recv()
|
||||
if recv == nil || deref(recv.Type()).(*types.Named).Obj().Exported() {
|
||||
roots := &pcg.nodes[0].edges
|
||||
roots.SetBit(roots, i, 1)
|
||||
}
|
||||
index[n.fn.RelString(pkg.Pkg)] = i
|
||||
}
|
||||
|
||||
json := a.pcgJSON(pcg)
|
||||
|
||||
// TODO(adonovan): pkg.Path() is not unique!
|
||||
// It is possible to declare a non-test package called x_test.
|
||||
a.result.pkgInfo(pkg.Pkg.Path()).setCallGraph(json, index)
|
||||
}
|
||||
}
|
||||
|
||||
// addCallees adds client data and links for the facts that site calls fns.
|
||||
func (a *analysis) addCallees(site ssa.CallInstruction, fns []*ssa.Function) {
|
||||
v := calleesJSON{
|
||||
Descr: site.Common().Description(),
|
||||
Callees: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
var this *types.Package // for relativizing names
|
||||
if p := site.Parent().Package(); p != nil {
|
||||
this = p.Pkg
|
||||
}
|
||||
|
||||
for _, fn := range fns {
|
||||
v.Callees = append(v.Callees, anchorJSON{
|
||||
Text: prettyFunc(this, fn),
|
||||
Href: a.posURL(funcToken(fn), len("func")),
|
||||
})
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(site.Common().Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len("("),
|
||||
title: fmt.Sprintf("%d callees", len(v.Callees)),
|
||||
onclick: fmt.Sprintf("onClickCallees(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
// stable order within packages but undefined across packages.
|
||||
type funcsByPos []*ssa.Function
|
||||
|
||||
func (a funcsByPos) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() }
|
||||
func (a funcsByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a funcsByPos) Len() int { return len(a) }
|
||||
|
||||
type sitesByPos []ssa.CallInstruction
|
||||
|
||||
func (a sitesByPos) Less(i, j int) bool { return a[i].Common().Pos() < a[j].Common().Pos() }
|
||||
func (a sitesByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sitesByPos) Len() int { return len(a) }
|
||||
|
||||
func funcToken(fn *ssa.Function) token.Pos {
|
||||
switch syntax := fn.Syntax().(type) {
|
||||
case *ast.FuncLit:
|
||||
return syntax.Type.Func
|
||||
case *ast.FuncDecl:
|
||||
return syntax.Type.Func
|
||||
}
|
||||
return token.NoPos
|
||||
}
|
||||
|
||||
// prettyFunc pretty-prints fn for the user interface.
|
||||
// TODO(adonovan): return HTML so we have more markup freedom.
|
||||
func prettyFunc(this *types.Package, fn *ssa.Function) string {
|
||||
if fn.Parent() != nil {
|
||||
return fmt.Sprintf("%s in %s",
|
||||
types.TypeString(fn.Signature, types.RelativeTo(this)),
|
||||
prettyFunc(this, fn.Parent()))
|
||||
}
|
||||
if fn.Synthetic != "" && fn.Name() == "init" {
|
||||
// (This is the actual initializer, not a declared 'func init').
|
||||
if fn.Pkg.Pkg == this {
|
||||
return "package initializer"
|
||||
}
|
||||
return fmt.Sprintf("%q package initializer", fn.Pkg.Pkg.Path())
|
||||
}
|
||||
return fn.RelString(this)
|
||||
}
|
||||
|
||||
// -- intra-package callgraph ------------------------------------------
|
||||
|
||||
// pcgNode represents a node in the package call graph (PCG).
|
||||
type pcgNode struct {
|
||||
fn *ssa.Function
|
||||
pretty string // cache of prettyFunc(fn)
|
||||
edges big.Int // set of callee func indices
|
||||
}
|
||||
|
||||
// A packageCallGraph represents the intra-package edges of the global call graph.
|
||||
// The zeroth node indicates "all external functions".
|
||||
type packageCallGraph struct {
|
||||
nodeIndex map[*ssa.Function]int // maps func to node index (a small int)
|
||||
nodes []*pcgNode // maps node index to node
|
||||
}
|
||||
|
||||
// sortNodes populates pcg.nodes in name order and updates the nodeIndex.
|
||||
func (pcg *packageCallGraph) sortNodes() {
|
||||
nodes := make([]*pcgNode, 0, len(pcg.nodeIndex))
|
||||
nodes = append(nodes, &pcgNode{fn: nil, pretty: "<external>"})
|
||||
for fn := range pcg.nodeIndex {
|
||||
nodes = append(nodes, &pcgNode{
|
||||
fn: fn,
|
||||
pretty: prettyFunc(fn.Pkg.Pkg, fn),
|
||||
})
|
||||
}
|
||||
sort.Sort(pcgNodesByPretty(nodes[1:]))
|
||||
for i, n := range nodes {
|
||||
pcg.nodeIndex[n.fn] = i
|
||||
}
|
||||
pcg.nodes = nodes
|
||||
}
|
||||
|
||||
func (pcg *packageCallGraph) addEdge(caller, callee *ssa.Function) {
|
||||
var callerIndex int
|
||||
if caller.Pkg == callee.Pkg {
|
||||
// intra-package edge
|
||||
callerIndex = pcg.nodeIndex[caller]
|
||||
if callerIndex < 1 {
|
||||
panic(caller)
|
||||
}
|
||||
}
|
||||
edges := &pcg.nodes[callerIndex].edges
|
||||
edges.SetBit(edges, pcg.nodeIndex[callee], 1)
|
||||
}
|
||||
|
||||
func (a *analysis) pcgAddNode(fn *ssa.Function) {
|
||||
if fn.Pkg == nil {
|
||||
return
|
||||
}
|
||||
pcg, ok := a.pcgs[fn.Pkg]
|
||||
if !ok {
|
||||
pcg = &packageCallGraph{nodeIndex: make(map[*ssa.Function]int)}
|
||||
a.pcgs[fn.Pkg] = pcg
|
||||
}
|
||||
pcg.nodeIndex[fn] = -1
|
||||
}
|
||||
|
||||
func (a *analysis) pcgAddEdge(caller, callee *ssa.Function) {
|
||||
if callee.Pkg != nil {
|
||||
a.pcgs[callee.Pkg].addEdge(caller, callee)
|
||||
}
|
||||
}
|
||||
|
||||
// pcgJSON returns a new slice of callgraph JSON values.
|
||||
func (a *analysis) pcgJSON(pcg *packageCallGraph) []*PCGNodeJSON {
|
||||
var nodes []*PCGNodeJSON
|
||||
for _, n := range pcg.nodes {
|
||||
|
||||
// TODO(adonovan): why is there no good way to iterate
|
||||
// over the set bits of a big.Int?
|
||||
var callees []int
|
||||
nbits := n.edges.BitLen()
|
||||
for j := 0; j < nbits; j++ {
|
||||
if n.edges.Bit(j) == 1 {
|
||||
callees = append(callees, j)
|
||||
}
|
||||
}
|
||||
|
||||
var pos token.Pos
|
||||
if n.fn != nil {
|
||||
pos = funcToken(n.fn)
|
||||
}
|
||||
nodes = append(nodes, &PCGNodeJSON{
|
||||
Func: anchorJSON{
|
||||
Text: n.pretty,
|
||||
Href: a.posURL(pos, len("func")),
|
||||
},
|
||||
Callees: callees,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
type pcgNodesByPretty []*pcgNode
|
||||
|
||||
func (a pcgNodesByPretty) Less(i, j int) bool { return a[i].pretty < a[j].pretty }
|
||||
func (a pcgNodesByPretty) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a pcgNodesByPretty) Len() int { return len(a) }
|
|
@ -0,0 +1,195 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the "implements" relation over all pairs of
|
||||
// named types in the program. (The mark-up is done by typeinfo.go.)
|
||||
|
||||
// TODO(adonovan): do we want to report implements(C, I) where C and I
|
||||
// belong to different packages and at least one is not exported?
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// computeImplements computes the "implements" relation over all pairs
|
||||
// of named types in allNamed.
|
||||
func computeImplements(cache *typeutil.MethodSetCache, allNamed []*types.Named) map[*types.Named]implementsFacts {
|
||||
// Information about a single type's method set.
|
||||
type msetInfo struct {
|
||||
typ types.Type
|
||||
mset *types.MethodSet
|
||||
mask1, mask2 uint64
|
||||
}
|
||||
|
||||
initMsetInfo := func(info *msetInfo, typ types.Type) {
|
||||
info.typ = typ
|
||||
info.mset = cache.MethodSet(typ)
|
||||
for i := 0; i < info.mset.Len(); i++ {
|
||||
name := info.mset.At(i).Obj().Name()
|
||||
info.mask1 |= 1 << methodBit(name[0])
|
||||
info.mask2 |= 1 << methodBit(name[len(name)-1])
|
||||
}
|
||||
}
|
||||
|
||||
// satisfies(T, U) reports whether type T satisfies type U.
|
||||
// U must be an interface.
|
||||
//
|
||||
// Since there are thousands of types (and thus millions of
|
||||
// pairs of types) and types.Assignable(T, U) is relatively
|
||||
// expensive, we compute assignability directly from the
|
||||
// method sets. (At least one of T and U must be an
|
||||
// interface.)
|
||||
//
|
||||
// We use a trick (thanks gri!) related to a Bloom filter to
|
||||
// quickly reject most tests, which are false. For each
|
||||
// method set, we precompute a mask, a set of bits, one per
|
||||
// distinct initial byte of each method name. Thus the mask
|
||||
// for io.ReadWriter would be {'R','W'}. AssignableTo(T, U)
|
||||
// cannot be true unless mask(T)&mask(U)==mask(U).
|
||||
//
|
||||
// As with a Bloom filter, we can improve precision by testing
|
||||
// additional hashes, e.g. using the last letter of each
|
||||
// method name, so long as the subset mask property holds.
|
||||
//
|
||||
// When analyzing the standard library, there are about 1e6
|
||||
// calls to satisfies(), of which 0.6% return true. With a
|
||||
// 1-hash filter, 95% of calls avoid the expensive check; with
|
||||
// a 2-hash filter, this grows to 98.2%.
|
||||
satisfies := func(T, U *msetInfo) bool {
|
||||
return T.mask1&U.mask1 == U.mask1 &&
|
||||
T.mask2&U.mask2 == U.mask2 &&
|
||||
containsAllIdsOf(T.mset, U.mset)
|
||||
}
|
||||
|
||||
// Information about a named type N, and perhaps also *N.
|
||||
type namedInfo struct {
|
||||
isInterface bool
|
||||
base msetInfo // N
|
||||
ptr msetInfo // *N, iff N !isInterface
|
||||
}
|
||||
|
||||
var infos []namedInfo
|
||||
|
||||
// Precompute the method sets and their masks.
|
||||
for _, N := range allNamed {
|
||||
var info namedInfo
|
||||
initMsetInfo(&info.base, N)
|
||||
_, info.isInterface = N.Underlying().(*types.Interface)
|
||||
if !info.isInterface {
|
||||
initMsetInfo(&info.ptr, types.NewPointer(N))
|
||||
}
|
||||
|
||||
if info.base.mask1|info.ptr.mask1 == 0 {
|
||||
continue // neither N nor *N has methods
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
facts := make(map[*types.Named]implementsFacts)
|
||||
|
||||
// Test all pairs of distinct named types (T, U).
|
||||
// TODO(adonovan): opt: compute (U, T) at the same time.
|
||||
for t := range infos {
|
||||
T := &infos[t]
|
||||
var to, from, fromPtr []types.Type
|
||||
for u := range infos {
|
||||
if t == u {
|
||||
continue
|
||||
}
|
||||
U := &infos[u]
|
||||
switch {
|
||||
case T.isInterface && U.isInterface:
|
||||
if satisfies(&U.base, &T.base) {
|
||||
to = append(to, U.base.typ)
|
||||
}
|
||||
if satisfies(&T.base, &U.base) {
|
||||
from = append(from, U.base.typ)
|
||||
}
|
||||
case T.isInterface: // U concrete
|
||||
if satisfies(&U.base, &T.base) {
|
||||
to = append(to, U.base.typ)
|
||||
} else if satisfies(&U.ptr, &T.base) {
|
||||
to = append(to, U.ptr.typ)
|
||||
}
|
||||
case U.isInterface: // T concrete
|
||||
if satisfies(&T.base, &U.base) {
|
||||
from = append(from, U.base.typ)
|
||||
} else if satisfies(&T.ptr, &U.base) {
|
||||
fromPtr = append(fromPtr, U.base.typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort types (arbitrarily) to avoid nondeterminism.
|
||||
sort.Sort(typesByString(to))
|
||||
sort.Sort(typesByString(from))
|
||||
sort.Sort(typesByString(fromPtr))
|
||||
|
||||
facts[T.base.typ.(*types.Named)] = implementsFacts{to, from, fromPtr}
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
type implementsFacts struct {
|
||||
to []types.Type // named or ptr-to-named types assignable to interface T
|
||||
from []types.Type // named interfaces assignable from T
|
||||
fromPtr []types.Type // named interfaces assignable only from *T
|
||||
}
|
||||
|
||||
type typesByString []types.Type
|
||||
|
||||
func (p typesByString) Len() int { return len(p) }
|
||||
func (p typesByString) Less(i, j int) bool { return p[i].String() < p[j].String() }
|
||||
func (p typesByString) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// methodBit returns the index of x in [a-zA-Z], or 52 if not found.
|
||||
func methodBit(x byte) uint64 {
|
||||
switch {
|
||||
case 'a' <= x && x <= 'z':
|
||||
return uint64(x - 'a')
|
||||
case 'A' <= x && x <= 'Z':
|
||||
return uint64(26 + x - 'A')
|
||||
}
|
||||
return 52 // all other bytes
|
||||
}
|
||||
|
||||
// containsAllIdsOf reports whether the method identifiers of T are a
|
||||
// superset of those in U. If U belongs to an interface type, the
|
||||
// result is equal to types.Assignable(T, U), but is cheaper to compute.
|
||||
//
|
||||
// TODO(gri): make this a method of *types.MethodSet.
|
||||
//
|
||||
func containsAllIdsOf(T, U *types.MethodSet) bool {
|
||||
t, tlen := 0, T.Len()
|
||||
u, ulen := 0, U.Len()
|
||||
for t < tlen && u < ulen {
|
||||
tMeth := T.At(t).Obj()
|
||||
uMeth := U.At(u).Obj()
|
||||
tId := tMeth.Id()
|
||||
uId := uMeth.Id()
|
||||
if tId > uId {
|
||||
// U has a method T lacks: fail.
|
||||
return false
|
||||
}
|
||||
if tId < uId {
|
||||
// T has a method U lacks: ignore it.
|
||||
t++
|
||||
continue
|
||||
}
|
||||
// U and T both have a method of this Id. Check types.
|
||||
if !types.Identical(tMeth.Type(), uMeth.Type()) {
|
||||
return false // type mismatch
|
||||
}
|
||||
u++
|
||||
t++
|
||||
}
|
||||
return u == ulen
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file defines types used by client-side JavaScript.
|
||||
|
||||
type anchorJSON struct {
|
||||
Text string // HTML
|
||||
Href string // URL
|
||||
}
|
||||
|
||||
type commOpJSON struct {
|
||||
Op anchorJSON
|
||||
Fn string
|
||||
}
|
||||
|
||||
// JavaScript's onClickComm() expects a commJSON.
|
||||
type commJSON struct {
|
||||
Ops []commOpJSON
|
||||
}
|
||||
|
||||
// Indicates one of these forms of fact about a type T:
|
||||
// T "is implemented by <ByKind> type <Other>" (ByKind != "", e.g. "array")
|
||||
// T "implements <Other>" (ByKind == "")
|
||||
type implFactJSON struct {
|
||||
ByKind string `json:",omitempty"`
|
||||
Other anchorJSON
|
||||
}
|
||||
|
||||
// Implements facts are grouped by form, for ease of reading.
|
||||
type implGroupJSON struct {
|
||||
Descr string
|
||||
Facts []implFactJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickIdent() expects a TypeInfoJSON.
|
||||
type TypeInfoJSON struct {
|
||||
Name string // type name
|
||||
Size, Align int64
|
||||
Methods []anchorJSON
|
||||
ImplGroups []implGroupJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickCallees() expects a calleesJSON.
|
||||
type calleesJSON struct {
|
||||
Descr string
|
||||
Callees []anchorJSON // markup for called function
|
||||
}
|
||||
|
||||
type callerJSON struct {
|
||||
Func string
|
||||
Sites []anchorJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickCallers() expects a callersJSON.
|
||||
type callersJSON struct {
|
||||
Callee string
|
||||
Callers []callerJSON
|
||||
}
|
||||
|
||||
// JavaScript's cgAddChild requires a global array of PCGNodeJSON
|
||||
// called CALLGRAPH, representing the intra-package call graph.
|
||||
// The first element is special and represents "all external callers".
|
||||
type PCGNodeJSON struct {
|
||||
Func anchorJSON
|
||||
Callees []int // indices within CALLGRAPH of nodes called by this one
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the channel "peers" relation over all pairs of
|
||||
// channel operations in the program. The peers are displayed in the
|
||||
// lower pane when a channel operation (make, <-, close) is clicked.
|
||||
|
||||
// TODO(adonovan): handle calls to reflect.{Select,Recv,Send,Close} too,
|
||||
// then enable reflection in PTA.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/go/pointer"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
)
|
||||
|
||||
func (a *analysis) doChannelPeers(ptsets map[ssa.Value]pointer.Pointer) {
|
||||
addSendRecv := func(j *commJSON, op chanOp) {
|
||||
j.Ops = append(j.Ops, commOpJSON{
|
||||
Op: anchorJSON{
|
||||
Text: op.mode,
|
||||
Href: a.posURL(op.pos, op.len),
|
||||
},
|
||||
Fn: prettyFunc(nil, op.fn),
|
||||
})
|
||||
}
|
||||
|
||||
// Build an undirected bipartite multigraph (binary relation)
|
||||
// of MakeChan ops and send/recv/close ops.
|
||||
//
|
||||
// TODO(adonovan): opt: use channel element types to partition
|
||||
// the O(n^2) problem into subproblems.
|
||||
aliasedOps := make(map[*ssa.MakeChan][]chanOp)
|
||||
opToMakes := make(map[chanOp][]*ssa.MakeChan)
|
||||
for _, op := range a.ops {
|
||||
// Combine the PT sets from all contexts.
|
||||
var makes []*ssa.MakeChan // aliased ops
|
||||
ptr, ok := ptsets[op.ch]
|
||||
if !ok {
|
||||
continue // e.g. channel op in dead code
|
||||
}
|
||||
for _, label := range ptr.PointsTo().Labels() {
|
||||
makechan, ok := label.Value().(*ssa.MakeChan)
|
||||
if !ok {
|
||||
continue // skip intrinsically-created channels for now
|
||||
}
|
||||
if makechan.Pos() == token.NoPos {
|
||||
continue // not possible?
|
||||
}
|
||||
makes = append(makes, makechan)
|
||||
aliasedOps[makechan] = append(aliasedOps[makechan], op)
|
||||
}
|
||||
opToMakes[op] = makes
|
||||
}
|
||||
|
||||
// Now that complete relation is built, build links for ops.
|
||||
for _, op := range a.ops {
|
||||
v := commJSON{
|
||||
Ops: []commOpJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
ops := make(map[chanOp]bool)
|
||||
for _, makechan := range opToMakes[op] {
|
||||
v.Ops = append(v.Ops, commOpJSON{
|
||||
Op: anchorJSON{
|
||||
Text: "made",
|
||||
Href: a.posURL(makechan.Pos()-token.Pos(len("make")),
|
||||
len("make")),
|
||||
},
|
||||
Fn: makechan.Parent().RelString(op.fn.Package().Pkg),
|
||||
})
|
||||
for _, op := range aliasedOps[makechan] {
|
||||
ops[op] = true
|
||||
}
|
||||
}
|
||||
for op := range ops {
|
||||
addSendRecv(&v, op)
|
||||
}
|
||||
|
||||
// Add links for each aliased op.
|
||||
fi, offset := a.fileAndOffset(op.pos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + op.len,
|
||||
title: "show channel ops",
|
||||
onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
// Add links for makechan ops themselves.
|
||||
for makechan, ops := range aliasedOps {
|
||||
v := commJSON{
|
||||
Ops: []commOpJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
for _, op := range ops {
|
||||
addSendRecv(&v, op)
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(makechan.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset - len("make"),
|
||||
end: offset,
|
||||
title: "show channel ops",
|
||||
onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
// chanOp abstracts an ssa.Send, ssa.Unop(ARROW), close(), or a SelectState.
|
||||
// Derived from cmd/guru/peers.go.
|
||||
type chanOp struct {
|
||||
ch ssa.Value
|
||||
mode string // sent|received|closed
|
||||
pos token.Pos
|
||||
len int
|
||||
fn *ssa.Function
|
||||
}
|
||||
|
||||
// chanOps returns a slice of all the channel operations in the instruction.
|
||||
// Derived from cmd/guru/peers.go.
|
||||
func chanOps(instr ssa.Instruction) []chanOp {
|
||||
fn := instr.Parent()
|
||||
var ops []chanOp
|
||||
switch instr := instr.(type) {
|
||||
case *ssa.UnOp:
|
||||
if instr.Op == token.ARROW {
|
||||
// TODO(adonovan): don't assume <-ch; could be 'range ch'.
|
||||
ops = append(ops, chanOp{instr.X, "received", instr.Pos(), len("<-"), fn})
|
||||
}
|
||||
case *ssa.Send:
|
||||
ops = append(ops, chanOp{instr.Chan, "sent", instr.Pos(), len("<-"), fn})
|
||||
case *ssa.Select:
|
||||
for _, st := range instr.States {
|
||||
mode := "received"
|
||||
if st.Dir == types.SendOnly {
|
||||
mode = "sent"
|
||||
}
|
||||
ops = append(ops, chanOp{st.Chan, mode, st.Pos, len("<-"), fn})
|
||||
}
|
||||
case ssa.CallInstruction:
|
||||
call := instr.Common()
|
||||
if blt, ok := call.Value.(*ssa.Builtin); ok && blt.Name() == "close" {
|
||||
pos := instr.Common().Pos()
|
||||
ops = append(ops, chanOp{call.Args[0], "closed", pos - token.Pos(len("close")), len("close("), fn})
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the markup for information from go/types:
|
||||
// IMPORTS, identifier RESOLUTION, METHOD SETS, size/alignment, and
|
||||
// the IMPLEMENTS relation.
|
||||
//
|
||||
// IMPORTS links connect import specs to the documentation for the
|
||||
// imported package.
|
||||
//
|
||||
// RESOLUTION links referring identifiers to their defining
|
||||
// identifier, and adds tooltips for kind and type.
|
||||
//
|
||||
// METHOD SETS, size/alignment, and the IMPLEMENTS relation are
|
||||
// displayed in the lower pane when a type's defining identifier is
|
||||
// clicked.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// TODO(adonovan): audit to make sure it's safe on ill-typed packages.
|
||||
|
||||
// TODO(adonovan): use same Sizes as loader.Config.
|
||||
var sizes = types.StdSizes{WordSize: 8, MaxAlign: 8}
|
||||
|
||||
func (a *analysis) doTypeInfo(info *loader.PackageInfo, implements map[*types.Named]implementsFacts) {
|
||||
// We must not assume the corresponding SSA packages were
|
||||
// created (i.e. were transitively error-free).
|
||||
|
||||
// IMPORTS
|
||||
for _, f := range info.Files {
|
||||
// Package decl.
|
||||
fi, offset := a.fileAndOffset(f.Name.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(f.Name.Name),
|
||||
title: "Package docs for " + info.Pkg.Path(),
|
||||
// TODO(adonovan): fix: we're putting the untrusted Path()
|
||||
// into a trusted field. What's the appropriate sanitizer?
|
||||
href: "/pkg/" + info.Pkg.Path(),
|
||||
})
|
||||
|
||||
// Import specs.
|
||||
for _, imp := range f.Imports {
|
||||
// Remove quotes.
|
||||
L := int(imp.End()-imp.Path.Pos()) - len(`""`)
|
||||
path, _ := strconv.Unquote(imp.Path.Value)
|
||||
fi, offset := a.fileAndOffset(imp.Path.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset + 1,
|
||||
end: offset + 1 + L,
|
||||
title: "Package docs for " + path,
|
||||
// TODO(adonovan): fix: we're putting the untrusted path
|
||||
// into a trusted field. What's the appropriate sanitizer?
|
||||
href: "/pkg/" + path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RESOLUTION
|
||||
qualifier := types.RelativeTo(info.Pkg)
|
||||
for id, obj := range info.Uses {
|
||||
// Position of the object definition.
|
||||
pos := obj.Pos()
|
||||
Len := len(obj.Name())
|
||||
|
||||
// Correct the position for non-renaming import specs.
|
||||
// import "sync/atomic"
|
||||
// ^^^^^^^^^^^
|
||||
if obj, ok := obj.(*types.PkgName); ok && id.Name == obj.Imported().Name() {
|
||||
// Assume this is a non-renaming import.
|
||||
// NB: not true for degenerate renamings: `import foo "foo"`.
|
||||
pos++
|
||||
Len = len(obj.Imported().Path())
|
||||
}
|
||||
|
||||
if obj.Pkg() == nil {
|
||||
continue // don't mark up built-ins.
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(id.NamePos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(id.Name),
|
||||
title: types.ObjectString(obj, qualifier),
|
||||
href: a.posURL(pos, Len),
|
||||
})
|
||||
}
|
||||
|
||||
// IMPLEMENTS & METHOD SETS
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
if named, ok := obj.Type().(*types.Named); ok {
|
||||
a.namedType(named, implements)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) namedType(T *types.Named, implements map[*types.Named]implementsFacts) {
|
||||
obj := T.Obj()
|
||||
qualifier := types.RelativeTo(obj.Pkg())
|
||||
v := &TypeInfoJSON{
|
||||
Name: obj.Name(),
|
||||
Size: sizes.Sizeof(T),
|
||||
Align: sizes.Alignof(T),
|
||||
Methods: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
|
||||
// addFact adds the fact "is implemented by T" (by) or
|
||||
// "implements T" (!by) to group.
|
||||
addFact := func(group *implGroupJSON, T types.Type, by bool) {
|
||||
Tobj := deref(T).(*types.Named).Obj()
|
||||
var byKind string
|
||||
if by {
|
||||
// Show underlying kind of implementing type,
|
||||
// e.g. "slice", "array", "struct".
|
||||
s := reflect.TypeOf(T.Underlying()).String()
|
||||
byKind = strings.ToLower(strings.TrimPrefix(s, "*types."))
|
||||
}
|
||||
group.Facts = append(group.Facts, implFactJSON{
|
||||
ByKind: byKind,
|
||||
Other: anchorJSON{
|
||||
Href: a.posURL(Tobj.Pos(), len(Tobj.Name())),
|
||||
Text: types.TypeString(T, qualifier),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// IMPLEMENTS
|
||||
if r, ok := implements[T]; ok {
|
||||
if isInterface(T) {
|
||||
// "T is implemented by <conc>" ...
|
||||
// "T is implemented by <iface>"...
|
||||
// "T implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: types.TypeString(T, qualifier),
|
||||
}
|
||||
// Show concrete types first; use two passes.
|
||||
for _, sub := range r.to {
|
||||
if !isInterface(sub) {
|
||||
addFact(&group, sub, true)
|
||||
}
|
||||
}
|
||||
for _, sub := range r.to {
|
||||
if isInterface(sub) {
|
||||
addFact(&group, sub, true)
|
||||
}
|
||||
}
|
||||
for _, super := range r.from {
|
||||
addFact(&group, super, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
} else {
|
||||
// T is concrete.
|
||||
if r.from != nil {
|
||||
// "T implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: types.TypeString(T, qualifier),
|
||||
}
|
||||
for _, super := range r.from {
|
||||
addFact(&group, super, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
}
|
||||
if r.fromPtr != nil {
|
||||
// "*C implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: "*" + types.TypeString(T, qualifier),
|
||||
}
|
||||
for _, psuper := range r.fromPtr {
|
||||
addFact(&group, psuper, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// METHOD SETS
|
||||
for _, sel := range typeutil.IntuitiveMethodSet(T, &a.prog.MethodSets) {
|
||||
meth := sel.Obj().(*types.Func)
|
||||
pos := meth.Pos() // may be 0 for error.Error
|
||||
v.Methods = append(v.Methods, anchorJSON{
|
||||
Href: a.posURL(pos, len(meth.Name())),
|
||||
Text: types.SelectionString(sel, qualifier),
|
||||
})
|
||||
}
|
||||
|
||||
// Since there can be many specs per decl, we
|
||||
// can't attach the link to the keyword 'type'
|
||||
// (as we do with 'func'); we use the Ident.
|
||||
fi, offset := a.fileAndOffset(obj.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(obj.Name()),
|
||||
title: fmt.Sprintf("type info for %s", obj.Name()),
|
||||
onclick: fmt.Sprintf("onClickTypeInfo(%d)", fi.addData(v)),
|
||||
})
|
||||
|
||||
// Add info for exported package-level types to the package info.
|
||||
if obj.Exported() && isPackageLevel(obj) {
|
||||
// TODO(adonovan): Path is not unique!
|
||||
// It is possible to declare a non-test package called x_test.
|
||||
a.result.pkgInfo(obj.Pkg().Path()).addType(v)
|
||||
}
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
func isInterface(T types.Type) bool { return types.IsInterface(T) }
|
||||
|
||||
// deref returns a pointer's element type; otherwise it returns typ.
|
||||
func deref(typ types.Type) types.Type {
|
||||
if p, ok := typ.Underlying().(*types.Pointer); ok {
|
||||
return p.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
// isPackageLevel reports whether obj is a package-level object.
|
||||
func isPackageLevel(obj types.Object) bool {
|
||||
return obj.Pkg().Scope().Lookup(obj.Name()) == obj
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
// 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
pathpkg "path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/analysis"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/util"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// A Corpus holds all the state related to serving and indexing a
|
||||
// collection of Go code.
|
||||
//
|
||||
// Construct a new Corpus with NewCorpus, then modify options,
|
||||
// then call its Init method.
|
||||
type Corpus struct {
|
||||
fs vfs.FileSystem
|
||||
|
||||
// Verbose logging.
|
||||
Verbose bool
|
||||
|
||||
// IndexEnabled controls whether indexing is enabled.
|
||||
IndexEnabled bool
|
||||
|
||||
// IndexFiles specifies a glob pattern specifying index files.
|
||||
// If not empty, the index is read from these files in sorted
|
||||
// order.
|
||||
IndexFiles string
|
||||
|
||||
// IndexThrottle specifies the indexing throttle value
|
||||
// between 0.0 and 1.0. At 0.0, the indexer always sleeps.
|
||||
// At 1.0, the indexer never sleeps. Because 0.0 is useless
|
||||
// and redundant with setting IndexEnabled to false, the
|
||||
// zero value for IndexThrottle means 0.9.
|
||||
IndexThrottle float64
|
||||
|
||||
// IndexInterval specifies the time to sleep between reindexing
|
||||
// all the sources.
|
||||
// If zero, a default is used. If negative, the index is only
|
||||
// built once.
|
||||
IndexInterval time.Duration
|
||||
|
||||
// IndexDocs enables indexing of Go documentation.
|
||||
// This will produce search results for exported types, functions,
|
||||
// methods, variables, and constants, and will link to the godoc
|
||||
// documentation for those identifiers.
|
||||
IndexDocs bool
|
||||
|
||||
// IndexGoCode enables indexing of Go source code.
|
||||
// This will produce search results for internal and external identifiers
|
||||
// and will link to both declarations and uses of those identifiers in
|
||||
// source code.
|
||||
IndexGoCode bool
|
||||
|
||||
// IndexFullText enables full-text indexing.
|
||||
// This will provide search results for any matching text in any file that
|
||||
// is indexed, including non-Go files (see whitelisted in index.go).
|
||||
// Regexp searching is supported via full-text indexing.
|
||||
IndexFullText bool
|
||||
|
||||
// MaxResults optionally specifies the maximum results for indexing.
|
||||
MaxResults int
|
||||
|
||||
// SummarizePackage optionally specifies a function to
|
||||
// summarize a package. It exists as an optimization to
|
||||
// avoid reading files to parse package comments.
|
||||
//
|
||||
// If SummarizePackage returns false for ok, the caller
|
||||
// ignores all return values and parses the files in the package
|
||||
// as if SummarizePackage were nil.
|
||||
//
|
||||
// If showList is false, the package is hidden from the
|
||||
// package listing.
|
||||
SummarizePackage func(pkg string) (summary string, showList, ok bool)
|
||||
|
||||
// IndexDirectory optionally specifies a function to determine
|
||||
// whether the provided directory should be indexed. The dir
|
||||
// will be of the form "/src/cmd/6a", "/doc/play",
|
||||
// "/src/io", etc.
|
||||
// If nil, all directories are indexed if indexing is enabled.
|
||||
IndexDirectory func(dir string) bool
|
||||
|
||||
testDir string // TODO(bradfitz,adg): migrate old godoc flag? looks unused.
|
||||
|
||||
// Send a value on this channel to trigger a metadata refresh.
|
||||
// It is buffered so that if a signal is not lost if sent
|
||||
// during a refresh.
|
||||
refreshMetadataSignal chan bool
|
||||
|
||||
// file system information
|
||||
fsTree util.RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now)
|
||||
fsModified util.RWValue // timestamp of last call to invalidateIndex
|
||||
docMetadata util.RWValue // mapping from paths to *Metadata
|
||||
|
||||
// SearchIndex is the search index in use.
|
||||
searchIndex util.RWValue
|
||||
|
||||
// Analysis is the result of type and pointer analysis.
|
||||
Analysis analysis.Result
|
||||
|
||||
// flag to check whether a corpus is initialized or not
|
||||
initMu sync.RWMutex
|
||||
initDone bool
|
||||
|
||||
// pkgAPIInfo contains the information about which package API
|
||||
// features were added in which version of Go.
|
||||
pkgAPIInfo apiVersions
|
||||
}
|
||||
|
||||
// NewCorpus returns a new Corpus from a filesystem.
|
||||
// The returned corpus has all indexing enabled and MaxResults set to 1000.
|
||||
// Change or set any options on Corpus before calling the Corpus.Init method.
|
||||
func NewCorpus(fs vfs.FileSystem) *Corpus {
|
||||
c := &Corpus{
|
||||
fs: fs,
|
||||
refreshMetadataSignal: make(chan bool, 1),
|
||||
|
||||
MaxResults: 1000,
|
||||
IndexEnabled: true,
|
||||
IndexDocs: true,
|
||||
IndexGoCode: true,
|
||||
IndexFullText: true,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Corpus) CurrentIndex() (*Index, time.Time) {
|
||||
v, t := c.searchIndex.Get()
|
||||
idx, _ := v.(*Index)
|
||||
return idx, t
|
||||
}
|
||||
|
||||
func (c *Corpus) FSModifiedTime() time.Time {
|
||||
_, ts := c.fsModified.Get()
|
||||
return ts
|
||||
}
|
||||
|
||||
// Init initializes Corpus, once options on Corpus are set.
|
||||
// It must be called before any subsequent method calls.
|
||||
func (c *Corpus) Init() error {
|
||||
if err := c.initFSTree(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.updateMetadata()
|
||||
go c.refreshMetadataLoop()
|
||||
|
||||
c.initMu.Lock()
|
||||
c.initDone = true
|
||||
c.initMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Corpus) initFSTree() error {
|
||||
dir := c.newDirectory(pathpkg.Join("/", c.testDir), -1)
|
||||
if dir == nil {
|
||||
return errors.New("godoc: corpus fstree is nil")
|
||||
}
|
||||
c.fsTree.Set(dir)
|
||||
c.invalidateIndex()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,383 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
// This file contains the code dealing with package directory trees.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// Conventional name for directories containing test data.
|
||||
// Excluded from directory trees.
|
||||
//
|
||||
const testdataDirName = "testdata"
|
||||
|
||||
type Directory struct {
|
||||
Depth int
|
||||
Path string // directory path; includes Name
|
||||
Name string // directory name
|
||||
HasPkg bool // true if the directory contains at least one package
|
||||
Synopsis string // package documentation, if any
|
||||
RootType vfs.RootType // root type of the filesystem containing the directory
|
||||
Dirs []*Directory // subdirectories
|
||||
}
|
||||
|
||||
func isGoFile(fi os.FileInfo) bool {
|
||||
name := fi.Name()
|
||||
return !fi.IsDir() &&
|
||||
len(name) > 0 && name[0] != '.' && // ignore .files
|
||||
pathpkg.Ext(name) == ".go"
|
||||
}
|
||||
|
||||
func isPkgFile(fi os.FileInfo) bool {
|
||||
return isGoFile(fi) &&
|
||||
!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
|
||||
}
|
||||
|
||||
func isPkgDir(fi os.FileInfo) bool {
|
||||
name := fi.Name()
|
||||
return fi.IsDir() && len(name) > 0 &&
|
||||
name[0] != '_' && name[0] != '.' // ignore _files and .files
|
||||
}
|
||||
|
||||
type treeBuilder struct {
|
||||
c *Corpus
|
||||
maxDepth int
|
||||
}
|
||||
|
||||
// ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc).
|
||||
// Send before an operation and receive after.
|
||||
var ioGate = make(chan struct{}, 20)
|
||||
|
||||
// workGate controls the number of concurrent workers. Too many concurrent
|
||||
// workers and performance degrades and the race detector gets overwhelmed. If
|
||||
// we cannot check out a concurrent worker, work is performed by the main thread
|
||||
// instead of spinning up another goroutine.
|
||||
var workGate = make(chan struct{}, runtime.NumCPU()*4)
|
||||
|
||||
func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
|
||||
if name == testdataDirName {
|
||||
return nil
|
||||
}
|
||||
|
||||
if depth >= b.maxDepth {
|
||||
// return a dummy directory so that the parent directory
|
||||
// doesn't get discarded just because we reached the max
|
||||
// directory depth
|
||||
return &Directory{
|
||||
Depth: depth,
|
||||
Path: path,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
var synopses [3]string // prioritized package documentation (0 == highest priority)
|
||||
|
||||
show := true // show in package listing
|
||||
hasPkgFiles := false
|
||||
haveSummary := false
|
||||
|
||||
if hook := b.c.SummarizePackage; hook != nil {
|
||||
if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok {
|
||||
hasPkgFiles = true
|
||||
show = show0
|
||||
synopses[0] = summary
|
||||
haveSummary = true
|
||||
}
|
||||
}
|
||||
|
||||
ioGate <- struct{}{}
|
||||
list, err := b.c.fs.ReadDir(path)
|
||||
<-ioGate
|
||||
if err != nil {
|
||||
// TODO: propagate more. See golang.org/issue/14252.
|
||||
// For now:
|
||||
if b.c.Verbose {
|
||||
log.Printf("newDirTree reading %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// determine number of subdirectories and if there are package files
|
||||
var dirchs []chan *Directory
|
||||
var dirs []*Directory
|
||||
|
||||
for _, d := range list {
|
||||
filename := pathpkg.Join(path, d.Name())
|
||||
switch {
|
||||
case isPkgDir(d):
|
||||
name := d.Name()
|
||||
select {
|
||||
case workGate <- struct{}{}:
|
||||
ch := make(chan *Directory, 1)
|
||||
dirchs = append(dirchs, ch)
|
||||
go func() {
|
||||
ch <- b.newDirTree(fset, filename, name, depth+1)
|
||||
<-workGate
|
||||
}()
|
||||
default:
|
||||
// no free workers, do work synchronously
|
||||
dir := b.newDirTree(fset, filename, name, depth+1)
|
||||
if dir != nil {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
case !haveSummary && isPkgFile(d):
|
||||
// looks like a package file, but may just be a file ending in ".go";
|
||||
// don't just count it yet (otherwise we may end up with hasPkgFiles even
|
||||
// though the directory doesn't contain any real package files - was bug)
|
||||
// no "optimal" package synopsis yet; continue to collect synopses
|
||||
ioGate <- struct{}{}
|
||||
const flags = parser.ParseComments | parser.PackageClauseOnly
|
||||
file, err := b.c.parseFile(fset, filename, flags)
|
||||
<-ioGate
|
||||
if err != nil {
|
||||
if b.c.Verbose {
|
||||
log.Printf("Error parsing %v: %v", filename, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
hasPkgFiles = true
|
||||
if file.Doc != nil {
|
||||
// prioritize documentation
|
||||
i := -1
|
||||
switch file.Name.Name {
|
||||
case name:
|
||||
i = 0 // normal case: directory name matches package name
|
||||
case "main":
|
||||
i = 1 // directory contains a main package
|
||||
default:
|
||||
i = 2 // none of the above
|
||||
}
|
||||
if 0 <= i && i < len(synopses) && synopses[i] == "" {
|
||||
synopses[i] = doc.Synopsis(file.Doc.Text())
|
||||
}
|
||||
}
|
||||
haveSummary = synopses[0] != ""
|
||||
}
|
||||
}
|
||||
|
||||
// create subdirectory tree
|
||||
for _, ch := range dirchs {
|
||||
if d := <-ch; d != nil {
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to sort the dirs slice because
|
||||
// it is appended again after reading from dirchs.
|
||||
sort.Slice(dirs, func(i, j int) bool {
|
||||
return dirs[i].Name < dirs[j].Name
|
||||
})
|
||||
|
||||
// if there are no package files and no subdirectories
|
||||
// containing package files, ignore the directory
|
||||
if !hasPkgFiles && len(dirs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// select the highest-priority synopsis for the directory entry, if any
|
||||
synopsis := ""
|
||||
for _, synopsis = range synopses {
|
||||
if synopsis != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &Directory{
|
||||
Depth: depth,
|
||||
Path: path,
|
||||
Name: name,
|
||||
HasPkg: hasPkgFiles && show, // TODO(bradfitz): add proper Hide field?
|
||||
Synopsis: synopsis,
|
||||
RootType: b.c.fs.RootType(path),
|
||||
Dirs: dirs,
|
||||
}
|
||||
}
|
||||
|
||||
// newDirectory creates a new package directory tree with at most maxDepth
|
||||
// levels, anchored at root. The result tree is pruned such that it only
|
||||
// contains directories that contain package files or that contain
|
||||
// subdirectories containing package files (transitively). If a non-nil
|
||||
// pathFilter is provided, directory paths additionally must be accepted
|
||||
// by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
|
||||
// provided for maxDepth, nodes at larger depths are pruned as well; they
|
||||
// are assumed to contain package files even if their contents are not known
|
||||
// (i.e., in this case the tree may contain directories w/o any package files).
|
||||
//
|
||||
func (c *Corpus) newDirectory(root string, maxDepth int) *Directory {
|
||||
// The root could be a symbolic link so use Stat not Lstat.
|
||||
d, err := c.fs.Stat(root)
|
||||
// If we fail here, report detailed error messages; otherwise
|
||||
// is is hard to see why a directory tree was not built.
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Printf("newDirectory(%s): %s", root, err)
|
||||
return nil
|
||||
case root != "/" && !isPkgDir(d):
|
||||
log.Printf("newDirectory(%s): not a package directory", root)
|
||||
return nil
|
||||
case root == "/" && !d.IsDir():
|
||||
log.Printf("newDirectory(%s): not a directory", root)
|
||||
return nil
|
||||
}
|
||||
if maxDepth < 0 {
|
||||
maxDepth = 1e6 // "infinity"
|
||||
}
|
||||
b := treeBuilder{c, maxDepth}
|
||||
// the file set provided is only for local parsing, no position
|
||||
// information escapes and thus we don't need to save the set
|
||||
return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
|
||||
}
|
||||
|
||||
func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
|
||||
if dir != nil {
|
||||
if !skipRoot {
|
||||
c <- dir
|
||||
}
|
||||
for _, d := range dir.Dirs {
|
||||
d.walk(c, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
|
||||
c := make(chan *Directory)
|
||||
go func() {
|
||||
dir.walk(c, skipRoot)
|
||||
close(c)
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
func (dir *Directory) lookupLocal(name string) *Directory {
|
||||
for _, d := range dir.Dirs {
|
||||
if d.Name == name {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitPath(p string) []string {
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
if p == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(p, "/")
|
||||
}
|
||||
|
||||
// lookup looks for the *Directory for a given path, relative to dir.
|
||||
func (dir *Directory) lookup(path string) *Directory {
|
||||
d := splitPath(dir.Path)
|
||||
p := splitPath(path)
|
||||
i := 0
|
||||
for i < len(d) {
|
||||
if i >= len(p) || d[i] != p[i] {
|
||||
return nil
|
||||
}
|
||||
i++
|
||||
}
|
||||
for dir != nil && i < len(p) {
|
||||
dir = dir.lookupLocal(p[i])
|
||||
i++
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// DirEntry describes a directory entry. The Depth and Height values
|
||||
// are useful for presenting an entry in an indented fashion.
|
||||
//
|
||||
type DirEntry struct {
|
||||
Depth int // >= 0
|
||||
Height int // = DirList.MaxHeight - Depth, > 0
|
||||
Path string // directory path; includes Name, relative to DirList root
|
||||
Name string // directory name
|
||||
HasPkg bool // true if the directory contains at least one package
|
||||
Synopsis string // package documentation, if any
|
||||
RootType vfs.RootType // root type of the filesystem containing the direntry
|
||||
}
|
||||
|
||||
type DirList struct {
|
||||
MaxHeight int // directory tree height, > 0
|
||||
List []DirEntry
|
||||
}
|
||||
|
||||
// hasThirdParty checks whether a list of directory entries has packages outside
|
||||
// the standard library or not.
|
||||
func hasThirdParty(list []DirEntry) bool {
|
||||
for _, entry := range list {
|
||||
if entry.RootType == vfs.RootTypeGoPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listing creates a (linear) directory listing from a directory tree.
|
||||
// If skipRoot is set, the root directory itself is excluded from the list.
|
||||
// If filter is set, only the directory entries whose paths match the filter
|
||||
// are included.
|
||||
//
|
||||
func (root *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine number of entries n and maximum height
|
||||
n := 0
|
||||
minDepth := 1 << 30 // infinity
|
||||
maxDepth := 0
|
||||
for d := range root.iter(skipRoot) {
|
||||
n++
|
||||
if minDepth > d.Depth {
|
||||
minDepth = d.Depth
|
||||
}
|
||||
if maxDepth < d.Depth {
|
||||
maxDepth = d.Depth
|
||||
}
|
||||
}
|
||||
maxHeight := maxDepth - minDepth + 1
|
||||
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// create list
|
||||
list := make([]DirEntry, 0, n)
|
||||
for d := range root.iter(skipRoot) {
|
||||
if filter != nil && !filter(d.Path) {
|
||||
continue
|
||||
}
|
||||
var p DirEntry
|
||||
p.Depth = d.Depth - minDepth
|
||||
p.Height = maxHeight - p.Depth
|
||||
// the path is relative to root.Path - remove the root.Path
|
||||
// prefix (the prefix should always be present but avoid
|
||||
// crashes and check)
|
||||
path := strings.TrimPrefix(d.Path, root.Path)
|
||||
// remove leading separator if any - path must be relative
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
p.Path = path
|
||||
p.Name = d.Name
|
||||
p.HasPkg = d.HasPkg
|
||||
p.Synopsis = d.Synopsis
|
||||
p.RootType = d.RootType
|
||||
list = append(list, p)
|
||||
}
|
||||
|
||||
return &DirList{maxHeight, list}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
|
||||
)
|
||||
|
||||
func TestNewDirTree(t *testing.T) {
|
||||
fsGate := make(chan bool, 20)
|
||||
rootfs := gatefs.New(vfs.OS(runtime.GOROOT()), fsGate)
|
||||
fs := vfs.NameSpace{}
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
|
||||
c := NewCorpus(fs)
|
||||
// 3 levels deep is enough for testing
|
||||
dir := c.newDirectory("/", 3)
|
||||
|
||||
processDir(t, dir)
|
||||
}
|
||||
|
||||
func processDir(t *testing.T, dir *Directory) {
|
||||
var list []string
|
||||
for _, d := range dir.Dirs {
|
||||
list = append(list, d.Name)
|
||||
// recursively process the lower level
|
||||
processDir(t, d)
|
||||
}
|
||||
|
||||
if sort.StringsAreSorted(list) == false {
|
||||
t.Errorf("list: %v is not sorted\n", list)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewDirectory(b *testing.B) {
|
||||
if testing.Short() {
|
||||
b.Skip("not running tests requiring large file scan in short mode")
|
||||
}
|
||||
|
||||
fsGate := make(chan bool, 20)
|
||||
|
||||
goroot := runtime.GOROOT()
|
||||
rootfs := gatefs.New(vfs.OS(goroot), fsGate)
|
||||
fs := vfs.NameSpace{}
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
||||
fs.Bind("/src/golang.org", gatefs.New(vfs.OS(p), fsGate), "/src/golang.org", vfs.BindAfter)
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for tries := 0; tries < b.N; tries++ {
|
||||
corpus := NewCorpus(fs)
|
||||
corpus.newDirectory("/", -1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,352 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package dl implements a simple downloads frontend server.
|
||||
//
|
||||
// It accepts HTTP POST requests to create a new download metadata entity, and
|
||||
// lists entities with sorting and filtering.
|
||||
// It is designed to run only on the instance of godoc that serves golang.org.
|
||||
package dl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
downloadBaseURL = "https://dl.google.com/go/"
|
||||
cacheKey = "download_list_3" // increment if listTemplateData changes
|
||||
cacheDuration = time.Hour
|
||||
)
|
||||
|
||||
// File represents a file on the golang.org downloads page.
|
||||
// It should be kept in sync with the upload code in x/build/cmd/release.
|
||||
type File struct {
|
||||
Filename string `json:"filename"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"-" datastore:",noindex"` // SHA1; deprecated
|
||||
ChecksumSHA256 string `json:"sha256" datastore:",noindex"`
|
||||
Size int64 `json:"size" datastore:",noindex"`
|
||||
Kind string `json:"kind"` // "archive", "installer", "source"
|
||||
Uploaded time.Time `json:"-"`
|
||||
}
|
||||
|
||||
func (f File) ChecksumType() string {
|
||||
if f.ChecksumSHA256 != "" {
|
||||
return "SHA256"
|
||||
}
|
||||
return "SHA1"
|
||||
}
|
||||
|
||||
func (f File) PrettyChecksum() string {
|
||||
if f.ChecksumSHA256 != "" {
|
||||
return f.ChecksumSHA256
|
||||
}
|
||||
return f.Checksum
|
||||
}
|
||||
|
||||
func (f File) PrettyOS() string {
|
||||
if f.OS == "darwin" {
|
||||
switch {
|
||||
case strings.Contains(f.Filename, "osx10.8"):
|
||||
return "OS X 10.8+"
|
||||
case strings.Contains(f.Filename, "osx10.6"):
|
||||
return "OS X 10.6+"
|
||||
}
|
||||
}
|
||||
return pretty(f.OS)
|
||||
}
|
||||
|
||||
func (f File) PrettySize() string {
|
||||
const mb = 1 << 20
|
||||
if f.Size == 0 {
|
||||
return ""
|
||||
}
|
||||
if f.Size < mb {
|
||||
// All Go releases are >1mb, but handle this case anyway.
|
||||
return fmt.Sprintf("%v bytes", f.Size)
|
||||
}
|
||||
return fmt.Sprintf("%.0fMB", float64(f.Size)/mb)
|
||||
}
|
||||
|
||||
var primaryPorts = map[string]bool{
|
||||
"darwin/amd64": true,
|
||||
"linux/386": true,
|
||||
"linux/amd64": true,
|
||||
"linux/armv6l": true,
|
||||
"windows/386": true,
|
||||
"windows/amd64": true,
|
||||
}
|
||||
|
||||
func (f File) PrimaryPort() bool {
|
||||
if f.Kind == "source" {
|
||||
return true
|
||||
}
|
||||
return primaryPorts[f.OS+"/"+f.Arch]
|
||||
}
|
||||
|
||||
func (f File) Highlight() bool {
|
||||
switch {
|
||||
case f.Kind == "source":
|
||||
return true
|
||||
case f.Arch == "amd64" && f.OS == "linux":
|
||||
return true
|
||||
case f.Arch == "amd64" && f.Kind == "installer":
|
||||
switch f.OS {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
if !strings.Contains(f.Filename, "osx10.6") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f File) URL() string {
|
||||
return downloadBaseURL + f.Filename
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
Version string `json:"version"`
|
||||
Stable bool `json:"stable"`
|
||||
Files []File `json:"files"`
|
||||
Visible bool `json:"-"` // show files on page load
|
||||
SplitPortTable bool `json:"-"` // whether files should be split by primary/other ports.
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
// The File field will be filled in by the first stable File
|
||||
// whose name matches the given fileRE.
|
||||
File
|
||||
fileRE *regexp.Regexp
|
||||
|
||||
Platform string // "Microsoft Windows", "Apple macOS", "Linux"
|
||||
Requirements string // "Windows XP and above, 64-bit Intel Processor"
|
||||
}
|
||||
|
||||
// featuredFiles lists the platforms and files to be featured
|
||||
// at the top of the downloads page.
|
||||
var featuredFiles = []Feature{
|
||||
{
|
||||
Platform: "Microsoft Windows",
|
||||
Requirements: "Windows 7 or later, Intel 64-bit processor",
|
||||
fileRE: regexp.MustCompile(`\.windows-amd64\.msi$`),
|
||||
},
|
||||
{
|
||||
Platform: "Apple macOS",
|
||||
Requirements: "macOS 10.10 or later, Intel 64-bit processor",
|
||||
fileRE: regexp.MustCompile(`\.darwin-amd64(-osx10\.8)?\.pkg$`),
|
||||
},
|
||||
{
|
||||
Platform: "Linux",
|
||||
Requirements: "Linux 2.6.23 or later, Intel 64-bit processor",
|
||||
fileRE: regexp.MustCompile(`\.linux-amd64\.tar\.gz$`),
|
||||
},
|
||||
{
|
||||
Platform: "Source",
|
||||
fileRE: regexp.MustCompile(`\.src\.tar\.gz$`),
|
||||
},
|
||||
}
|
||||
|
||||
// data to send to the template; increment cacheKey if you change this.
|
||||
type listTemplateData struct {
|
||||
Featured []Feature
|
||||
Stable, Unstable, Archive []Release
|
||||
}
|
||||
|
||||
var (
|
||||
listTemplate = template.Must(template.New("").Funcs(templateFuncs).Parse(templateHTML))
|
||||
templateFuncs = template.FuncMap{"pretty": pretty}
|
||||
)
|
||||
|
||||
func filesToFeatured(fs []File) (featured []Feature) {
|
||||
for _, feature := range featuredFiles {
|
||||
for _, file := range fs {
|
||||
if feature.fileRE.MatchString(file.Filename) {
|
||||
feature.File = file
|
||||
featured = append(featured, feature)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func filesToReleases(fs []File) (stable, unstable, archive []Release) {
|
||||
sort.Sort(fileOrder(fs))
|
||||
|
||||
var r *Release
|
||||
var stableMaj, stableMin int
|
||||
add := func() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if !r.Stable {
|
||||
if len(unstable) != 0 {
|
||||
// Only show one (latest) unstable version.
|
||||
return
|
||||
}
|
||||
maj, min, _ := parseVersion(r.Version)
|
||||
if maj < stableMaj || maj == stableMaj && min <= stableMin {
|
||||
// Display unstable version only if newer than the
|
||||
// latest stable release.
|
||||
return
|
||||
}
|
||||
unstable = append(unstable, *r)
|
||||
}
|
||||
|
||||
// Reports whether the release is the most recent minor version of the
|
||||
// two most recent major versions.
|
||||
shouldAddStable := func() bool {
|
||||
if len(stable) >= 2 {
|
||||
// Show up to two stable versions.
|
||||
return false
|
||||
}
|
||||
if len(stable) == 0 {
|
||||
// Most recent stable version.
|
||||
stableMaj, stableMin, _ = parseVersion(r.Version)
|
||||
return true
|
||||
}
|
||||
if maj, _, _ := parseVersion(r.Version); maj == stableMaj {
|
||||
// Older minor version of most recent major version.
|
||||
return false
|
||||
}
|
||||
// Second most recent stable version.
|
||||
return true
|
||||
}
|
||||
if !shouldAddStable() {
|
||||
archive = append(archive, *r)
|
||||
return
|
||||
}
|
||||
|
||||
// Split the file list into primary/other ports for the stable releases.
|
||||
// NOTE(cbro): This is only done for stable releases because maintaining the historical
|
||||
// nature of primary/other ports for older versions is infeasible.
|
||||
// If freebsd is considered primary some time in the future, we'd not want to
|
||||
// mark all of the older freebsd binaries as "primary".
|
||||
// It might be better if we set that as a flag when uploading.
|
||||
r.SplitPortTable = true
|
||||
r.Visible = true // Toggle open all stable releases.
|
||||
stable = append(stable, *r)
|
||||
}
|
||||
for _, f := range fs {
|
||||
if r == nil || f.Version != r.Version {
|
||||
add()
|
||||
r = &Release{
|
||||
Version: f.Version,
|
||||
Stable: isStable(f.Version),
|
||||
}
|
||||
}
|
||||
r.Files = append(r.Files, f)
|
||||
}
|
||||
add()
|
||||
return
|
||||
}
|
||||
|
||||
// isStable reports whether the version string v is a stable version.
|
||||
func isStable(v string) bool {
|
||||
return !strings.Contains(v, "beta") && !strings.Contains(v, "rc")
|
||||
}
|
||||
|
||||
type fileOrder []File
|
||||
|
||||
func (s fileOrder) Len() int { return len(s) }
|
||||
func (s fileOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s fileOrder) Less(i, j int) bool {
|
||||
a, b := s[i], s[j]
|
||||
if av, bv := a.Version, b.Version; av != bv {
|
||||
return versionLess(av, bv)
|
||||
}
|
||||
if a.OS != b.OS {
|
||||
return a.OS < b.OS
|
||||
}
|
||||
if a.Arch != b.Arch {
|
||||
return a.Arch < b.Arch
|
||||
}
|
||||
if a.Kind != b.Kind {
|
||||
return a.Kind < b.Kind
|
||||
}
|
||||
return a.Filename < b.Filename
|
||||
}
|
||||
|
||||
func versionLess(a, b string) bool {
|
||||
// Put stable releases first.
|
||||
if isStable(a) != isStable(b) {
|
||||
return isStable(a)
|
||||
}
|
||||
maja, mina, ta := parseVersion(a)
|
||||
majb, minb, tb := parseVersion(b)
|
||||
if maja == majb {
|
||||
if mina == minb {
|
||||
return ta >= tb
|
||||
}
|
||||
return mina >= minb
|
||||
}
|
||||
return maja >= majb
|
||||
}
|
||||
|
||||
func parseVersion(v string) (maj, min int, tail string) {
|
||||
if i := strings.Index(v, "beta"); i > 0 {
|
||||
tail = v[i:]
|
||||
v = v[:i]
|
||||
}
|
||||
if i := strings.Index(v, "rc"); i > 0 {
|
||||
tail = v[i:]
|
||||
v = v[:i]
|
||||
}
|
||||
p := strings.Split(strings.TrimPrefix(v, "go1."), ".")
|
||||
maj, _ = strconv.Atoi(p[0])
|
||||
if len(p) < 2 {
|
||||
return
|
||||
}
|
||||
min, _ = strconv.Atoi(p[1])
|
||||
return
|
||||
}
|
||||
|
||||
func validUser(user string) bool {
|
||||
switch user {
|
||||
case "adg", "bradfitz", "cbro", "andybons", "valsorda", "dmitshur", "katiehockman":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
fileRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+\.(tar\.gz|pkg|msi|zip)$`)
|
||||
goGetRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+$`)
|
||||
)
|
||||
|
||||
// pretty returns a human-readable version of the given OS, Arch, or Kind.
|
||||
func pretty(s string) string {
|
||||
t, ok := prettyStrings[s]
|
||||
if !ok {
|
||||
return s
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
var prettyStrings = map[string]string{
|
||||
"darwin": "macOS",
|
||||
"freebsd": "FreeBSD",
|
||||
"linux": "Linux",
|
||||
"windows": "Windows",
|
||||
|
||||
"386": "x86",
|
||||
"amd64": "x86-64",
|
||||
"armv6l": "ARMv6",
|
||||
"arm64": "ARMv8",
|
||||
|
||||
"archive": "Archive",
|
||||
"installer": "Installer",
|
||||
"source": "Source",
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dl
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVersion(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
in string
|
||||
maj, min int
|
||||
tail string
|
||||
}{
|
||||
{"go1.5", 5, 0, ""},
|
||||
{"go1.5beta1", 5, 0, "beta1"},
|
||||
{"go1.5.1", 5, 1, ""},
|
||||
{"go1.5.1rc1", 5, 1, "rc1"},
|
||||
} {
|
||||
maj, min, tail := parseVersion(c.in)
|
||||
if maj != c.maj || min != c.min || tail != c.tail {
|
||||
t.Errorf("parseVersion(%q) = %v, %v, %q; want %v, %v, %q",
|
||||
c.in, maj, min, tail, c.maj, c.min, c.tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOrder(t *testing.T) {
|
||||
fs := []File{
|
||||
{Filename: "go1.3.src.tar.gz", Version: "go1.3", OS: "", Arch: "", Kind: "source"},
|
||||
{Filename: "go1.3.1.src.tar.gz", Version: "go1.3.1", OS: "", Arch: "", Kind: "source"},
|
||||
{Filename: "go1.3.linux-amd64.tar.gz", Version: "go1.3", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.3.1.linux-amd64.tar.gz", Version: "go1.3.1", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.3.darwin-amd64.tar.gz", Version: "go1.3", OS: "darwin", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.3.darwin-amd64.pkg", Version: "go1.3", OS: "darwin", Arch: "amd64", Kind: "installer"},
|
||||
{Filename: "go1.3.darwin-386.tar.gz", Version: "go1.3", OS: "darwin", Arch: "386", Kind: "archive"},
|
||||
{Filename: "go1.3beta1.linux-amd64.tar.gz", Version: "go1.3beta1", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.3beta2.linux-amd64.tar.gz", Version: "go1.3beta2", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.3rc1.linux-amd64.tar.gz", Version: "go1.3rc1", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.2.linux-amd64.tar.gz", Version: "go1.2", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
{Filename: "go1.2.2.linux-amd64.tar.gz", Version: "go1.2.2", OS: "linux", Arch: "amd64", Kind: "archive"},
|
||||
}
|
||||
sort.Sort(fileOrder(fs))
|
||||
var s []string
|
||||
for _, f := range fs {
|
||||
s = append(s, f.Filename)
|
||||
}
|
||||
got := strings.Join(s, "\n")
|
||||
want := strings.Join([]string{
|
||||
"go1.3.1.src.tar.gz",
|
||||
"go1.3.1.linux-amd64.tar.gz",
|
||||
"go1.3.src.tar.gz",
|
||||
"go1.3.darwin-386.tar.gz",
|
||||
"go1.3.darwin-amd64.tar.gz",
|
||||
"go1.3.darwin-amd64.pkg",
|
||||
"go1.3.linux-amd64.tar.gz",
|
||||
"go1.2.2.linux-amd64.tar.gz",
|
||||
"go1.2.linux-amd64.tar.gz",
|
||||
"go1.3rc1.linux-amd64.tar.gz",
|
||||
"go1.3beta2.linux-amd64.tar.gz",
|
||||
"go1.3beta1.linux-amd64.tar.gz",
|
||||
}, "\n")
|
||||
if got != want {
|
||||
t.Errorf("sort order is\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesToReleases(t *testing.T) {
|
||||
fs := []File{
|
||||
{Version: "go1.7.4", OS: "darwin"},
|
||||
{Version: "go1.7.4", OS: "windows"},
|
||||
{Version: "go1.7", OS: "darwin"},
|
||||
{Version: "go1.7", OS: "windows"},
|
||||
{Version: "go1.6.2", OS: "darwin"},
|
||||
{Version: "go1.6.2", OS: "windows"},
|
||||
{Version: "go1.6", OS: "darwin"},
|
||||
{Version: "go1.6", OS: "windows"},
|
||||
{Version: "go1.5.2", OS: "darwin"},
|
||||
{Version: "go1.5.2", OS: "windows"},
|
||||
{Version: "go1.5", OS: "darwin"},
|
||||
{Version: "go1.5", OS: "windows"},
|
||||
{Version: "go1.5beta1", OS: "windows"},
|
||||
}
|
||||
stable, unstable, archive := filesToReleases(fs)
|
||||
if got, want := len(stable), 2; want != got {
|
||||
t.Errorf("len(stable): got %v, want %v", got, want)
|
||||
} else {
|
||||
if got, want := stable[0].Version, "go1.7.4"; want != got {
|
||||
t.Errorf("stable[0].Version: got %v, want %v", got, want)
|
||||
}
|
||||
if got, want := stable[1].Version, "go1.6.2"; want != got {
|
||||
t.Errorf("stable[1].Version: got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
if got, want := len(unstable), 0; want != got {
|
||||
t.Errorf("len(unstable): got %v, want %v", got, want)
|
||||
}
|
||||
if got, want := len(archive), 4; want != got {
|
||||
t.Errorf("len(archive): got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldUnstableNotShown(t *testing.T) {
|
||||
fs := []File{
|
||||
{Version: "go1.7.4"},
|
||||
{Version: "go1.7"},
|
||||
{Version: "go1.7beta1"},
|
||||
}
|
||||
_, unstable, _ := filesToReleases(fs)
|
||||
if len(unstable) != 0 {
|
||||
t.Errorf("got unstable, want none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnstableShown(t *testing.T) {
|
||||
fs := []File{
|
||||
{Version: "go1.8beta2"},
|
||||
{Version: "go1.8rc1"},
|
||||
{Version: "go1.7.4"},
|
||||
{Version: "go1.7"},
|
||||
{Version: "go1.7beta1"},
|
||||
}
|
||||
_, unstable, _ := filesToReleases(fs)
|
||||
if got, want := len(unstable), 1; got != want {
|
||||
t.Fatalf("len(unstable): got %v, want %v", got, want)
|
||||
}
|
||||
// show rcs ahead of betas.
|
||||
if got, want := unstable[0].Version, "go1.8rc1"; got != want {
|
||||
t.Fatalf("unstable[0].Version: got %v, want %v", got, want)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build golangorg
|
||||
|
||||
package dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/datastore"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/env"
|
||||
"golang.org/x/website/internal/memcache"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
datastore *datastore.Client
|
||||
memcache *memcache.CodecClient
|
||||
}
|
||||
|
||||
func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
|
||||
s := server{dc, mc.WithCodec(memcache.Gob)}
|
||||
mux.HandleFunc("/dl", s.getHandler)
|
||||
mux.HandleFunc("/dl/", s.getHandler) // also serves listHandler
|
||||
mux.HandleFunc("/dl/upload", s.uploadHandler)
|
||||
|
||||
// NOTE(cbro): this only needs to be run once per project,
|
||||
// and should be behind an admin login.
|
||||
// TODO(cbro): move into a locally-run program? or remove?
|
||||
// mux.HandleFunc("/dl/init", initHandler)
|
||||
}
|
||||
|
||||
// rootKey is the ancestor of all File entities.
|
||||
var rootKey = datastore.NameKey("FileRoot", "root", nil)
|
||||
|
||||
func (h server) listHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
var d listTemplateData
|
||||
|
||||
if err := h.memcache.Get(ctx, cacheKey, &d); err != nil {
|
||||
if err != memcache.ErrCacheMiss {
|
||||
log.Printf("ERROR cache get error: %v", err)
|
||||
// NOTE(cbro): continue to hit datastore if the memcache is down.
|
||||
}
|
||||
|
||||
var fs []File
|
||||
q := datastore.NewQuery("File").Ancestor(rootKey)
|
||||
if _, err := h.datastore.GetAll(ctx, q, &fs); err != nil {
|
||||
log.Printf("ERROR error listing: %v", err)
|
||||
http.Error(w, "Could not get download page. Try again in a few minutes.", 500)
|
||||
return
|
||||
}
|
||||
d.Stable, d.Unstable, d.Archive = filesToReleases(fs)
|
||||
if len(d.Stable) > 0 {
|
||||
d.Featured = filesToFeatured(d.Stable[0].Files)
|
||||
}
|
||||
|
||||
item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration}
|
||||
if err := h.memcache.Set(ctx, item); err != nil {
|
||||
log.Printf("ERROR cache set error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("mode") == "json" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(d.Stable); err != nil {
|
||||
log.Printf("ERROR rendering JSON for releases: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := listTemplate.ExecuteTemplate(w, "root", d); err != nil {
|
||||
log.Printf("ERROR executing template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h server) uploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
// Authenticate using a user token (same as gomote).
|
||||
user := r.FormValue("user")
|
||||
if !validUser(user) {
|
||||
http.Error(w, "bad user", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.FormValue("key") != h.userKey(ctx, user) {
|
||||
http.Error(w, "bad key", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var f File
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
|
||||
log.Printf("ERROR decoding upload JSON: %v", err)
|
||||
http.Error(w, "Something broke", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if f.Filename == "" {
|
||||
http.Error(w, "Must provide Filename", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if f.Uploaded.IsZero() {
|
||||
f.Uploaded = time.Now()
|
||||
}
|
||||
k := datastore.NameKey("File", f.Filename, rootKey)
|
||||
if _, err := h.datastore.Put(ctx, k, &f); err != nil {
|
||||
log.Printf("ERROR File entity: %v", err)
|
||||
http.Error(w, "could not put File entity", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := h.memcache.Delete(ctx, cacheKey); err != nil {
|
||||
log.Printf("ERROR delete error: %v", err)
|
||||
}
|
||||
io.WriteString(w, "OK")
|
||||
}
|
||||
|
||||
func (h server) getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
isGoGet := (r.Method == "GET" || r.Method == "HEAD") && r.FormValue("go-get") == "1"
|
||||
// For go get, we need to serve the same meta tags at /dl for cmd/go to
|
||||
// validate against the import path.
|
||||
if r.URL.Path == "/dl" && isGoGet {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, `<!DOCTYPE html><html><head>
|
||||
<meta name="go-import" content="golang.org/dl git https://go.googlesource.com/dl">
|
||||
</head></html>`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/dl" {
|
||||
http.Redirect(w, r, "/dl/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(r.URL.Path, "/dl/")
|
||||
var redirectURL string
|
||||
switch {
|
||||
case name == "":
|
||||
h.listHandler(w, r)
|
||||
return
|
||||
case fileRe.MatchString(name):
|
||||
http.Redirect(w, r, downloadBaseURL+name, http.StatusFound)
|
||||
return
|
||||
case name == "gotip":
|
||||
redirectURL = "https://godoc.org/golang.org/dl/gotip"
|
||||
case goGetRe.MatchString(name):
|
||||
redirectURL = "https://golang.org/dl/#" + name
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if !isGoGet {
|
||||
w.Header().Set("Location", redirectURL)
|
||||
}
|
||||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="go-import" content="golang.org/dl git https://go.googlesource.com/dl">
|
||||
<meta http-equiv="refresh" content="0; url=%s">
|
||||
</head>
|
||||
<body>
|
||||
Nothing to see here; <a href="%s">move along</a>.
|
||||
</body>
|
||||
</html>
|
||||
`, html.EscapeString(redirectURL), html.EscapeString(redirectURL))
|
||||
}
|
||||
|
||||
func (h server) initHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var fileRoot struct {
|
||||
Root string
|
||||
}
|
||||
ctx := r.Context()
|
||||
k := rootKey
|
||||
_, err := h.datastore.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
|
||||
err := tx.Get(k, &fileRoot)
|
||||
if err != nil && err != datastore.ErrNoSuchEntity {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Put(k, &fileRoot)
|
||||
return err
|
||||
}, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
io.WriteString(w, "OK")
|
||||
}
|
||||
|
||||
func (h server) userKey(c context.Context, user string) string {
|
||||
hash := hmac.New(md5.New, []byte(h.secret(c)))
|
||||
hash.Write([]byte("user-" + user))
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
// Code below copied from x/build/app/key
|
||||
|
||||
var theKey struct {
|
||||
sync.RWMutex
|
||||
builderKey
|
||||
}
|
||||
|
||||
type builderKey struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
func (k *builderKey) Key() *datastore.Key {
|
||||
return datastore.NameKey("BuilderKey", "root", nil)
|
||||
}
|
||||
|
||||
func (h server) secret(ctx context.Context) string {
|
||||
// check with rlock
|
||||
theKey.RLock()
|
||||
k := theKey.Secret
|
||||
theKey.RUnlock()
|
||||
if k != "" {
|
||||
return k
|
||||
}
|
||||
|
||||
// prepare to fill; check with lock and keep lock
|
||||
theKey.Lock()
|
||||
defer theKey.Unlock()
|
||||
if theKey.Secret != "" {
|
||||
return theKey.Secret
|
||||
}
|
||||
|
||||
// fill
|
||||
if err := h.datastore.Get(ctx, theKey.Key(), &theKey.builderKey); err != nil {
|
||||
if err == datastore.ErrNoSuchEntity {
|
||||
// If the key is not stored in datastore, write it.
|
||||
// This only happens at the beginning of a new deployment.
|
||||
// The code is left here for SDK use and in case a fresh
|
||||
// deployment is ever needed. "gophers rule" is not the
|
||||
// real key.
|
||||
if env.IsProd() {
|
||||
panic("lost key from datastore")
|
||||
}
|
||||
theKey.Secret = "gophers rule"
|
||||
h.datastore.Put(ctx, theKey.Key(), &theKey.builderKey)
|
||||
return theKey.Secret
|
||||
}
|
||||
panic("cannot load builder key: " + err.Error())
|
||||
}
|
||||
|
||||
return theKey.Secret
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dl
|
||||
|
||||
// TODO(adg): refactor this to use the tools/godoc/static template.
|
||||
|
||||
const templateHTML = `
|
||||
{{define "root"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Downloads - The Go Programming Language</title>
|
||||
<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
|
||||
<script type="text/javascript">window.initFuncs = [];</script>
|
||||
<style>
|
||||
table.codetable {
|
||||
margin-left: 20px; margin-right: 20px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.codetable tr {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
table.codetable tr:nth-child(2n), table.codetable tr.first {
|
||||
background-color: white;
|
||||
}
|
||||
table.codetable td, table.codetable th {
|
||||
white-space: nowrap;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
table.codetable tt {
|
||||
font-size: xx-small;
|
||||
}
|
||||
table.codetable tr.highlight td {
|
||||
font-weight: bold;
|
||||
}
|
||||
a.downloadBox {
|
||||
display: block;
|
||||
color: #222;
|
||||
border: 1px solid #375EAB;
|
||||
border-radius: 5px;
|
||||
background: #E0EBF5;
|
||||
width: 280px;
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
a.downloadBox:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.downloadBox .platform {
|
||||
font-size: large;
|
||||
}
|
||||
.downloadBox .filename {
|
||||
color: #375EAB;
|
||||
font-weight: bold;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
a.downloadBox:hover .filename {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.downloadBox .size {
|
||||
font-size: small;
|
||||
font-weight: normal;
|
||||
}
|
||||
.downloadBox .reqs {
|
||||
font-size: small;
|
||||
font-style: italic;
|
||||
}
|
||||
.downloadBox .checksum {
|
||||
font-size: 5pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="topbar"><div class="container">
|
||||
|
||||
<div class="top-heading"><a href="/">The Go Programming Language</a></div>
|
||||
<form method="GET" action="/search">
|
||||
<div id="menu">
|
||||
<a href="/doc/">Documents</a>
|
||||
<a href="/pkg/">Packages</a>
|
||||
<a href="/project/">The Project</a>
|
||||
<a href="/help/">Help</a>
|
||||
<a href="/blog/">Blog</a>
|
||||
<span class="search-box"><input type="search" id="search" name="q" placeholder="Search" aria-label="Search" required><button type="submit"><span><!-- magnifying glass: --><svg width="24" height="24" viewBox="0 0 24 24"><title>submit search</title><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/><path d="M0 0h24v24H0z" fill="none"/></svg></span></button></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div></div>
|
||||
|
||||
<div id="page">
|
||||
<div class="container">
|
||||
|
||||
<h1>Downloads</h1>
|
||||
|
||||
<p>
|
||||
After downloading a binary release suitable for your system,
|
||||
please follow the <a href="/doc/install">installation instructions</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you are building from source,
|
||||
follow the <a href="/doc/install/source">source installation instructions</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
See the <a href="/doc/devel/release.html">release history</a> for more
|
||||
information about Go releases.
|
||||
</p>
|
||||
|
||||
{{with .Featured}}
|
||||
<h3 id="featured">Featured downloads</h3>
|
||||
{{range .}}
|
||||
{{template "download" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
|
||||
{{with .Stable}}
|
||||
<h3 id="stable">Stable versions</h3>
|
||||
{{template "releases" .}}
|
||||
{{end}}
|
||||
|
||||
{{with .Unstable}}
|
||||
<h3 id="unstable">Unstable version</h3>
|
||||
{{template "releases" .}}
|
||||
{{end}}
|
||||
|
||||
{{with .Archive}}
|
||||
<div class="toggle" id="archive">
|
||||
<div class="collapsed">
|
||||
<h3 class="toggleButton" title="Click to show versions">Archived versions▹</h3>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<h3 class="toggleButton" title="Click to hide versions">Archived versions▾</h3>
|
||||
{{template "releases" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div id="footer">
|
||||
<p>
|
||||
Except as
|
||||
<a href="https://developers.google.com/site-policies#restrictions">noted</a>,
|
||||
the content of this page is licensed under the Creative Commons
|
||||
Attribution 3.0 License,<br>
|
||||
and code is licensed under a <a href="http://golang.org/LICENSE">BSD license</a>.<br>
|
||||
<a href="http://golang.org/doc/tos.html">Terms of Service</a> |
|
||||
<a href="http://www.google.com/intl/en/policies/privacy/">Privacy Policy</a>
|
||||
</p>
|
||||
</div><!-- #footer -->
|
||||
|
||||
</div><!-- .container -->
|
||||
</div><!-- #page -->
|
||||
<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-11222381-2', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
</body>
|
||||
<script src="/lib/godoc/jquery.js"></script>
|
||||
<script src="/lib/godoc/godocs.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('a.download').click(function(e) {
|
||||
// Try using the link text as the file name,
|
||||
// unless there's a child element of class 'filename'.
|
||||
var filename = $(this).text();
|
||||
var child = $(this).find('.filename');
|
||||
if (child.length > 0) {
|
||||
filename = child.text();
|
||||
}
|
||||
|
||||
// This must be kept in sync with the filenameRE in godocs.js.
|
||||
var filenameRE = /^go1\.\d+(\.\d+)?([a-z0-9]+)?\.([a-z0-9]+)(-[a-z0-9]+)?(-osx10\.[68])?\.([a-z.]+)$/;
|
||||
var m = filenameRE.exec(filename);
|
||||
if (!m) {
|
||||
// Don't redirect to the download page if it won't recognize this file.
|
||||
// (Should not happen.)
|
||||
return;
|
||||
}
|
||||
|
||||
var dest = "/doc/install";
|
||||
if (filename.indexOf(".src.") != -1) {
|
||||
dest += "/source";
|
||||
}
|
||||
dest += "?download=" + filename;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.location = dest;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "releases"}}
|
||||
{{range .}}
|
||||
<div class="toggle{{if .Visible}}Visible{{end}}" id="{{.Version}}">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show downloads for this version">{{.Version}} ▹</h2>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide downloads for this version">{{.Version}} ▾</h2>
|
||||
{{if .Stable}}{{else}}
|
||||
<p>This is an <b>unstable</b> version of Go. Use with caution.</p>
|
||||
<p>If you already have Go installed, you can install this version by running:</p>
|
||||
<pre>
|
||||
go get golang.org/dl/{{.Version}}
|
||||
</pre>
|
||||
<p>Then, use the <code>{{.Version}}</code> command instead of the <code>go</code> command to use {{.Version}}.</p>
|
||||
{{end}}
|
||||
{{template "files" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "files"}}
|
||||
<table class="codetable">
|
||||
<thead>
|
||||
<tr class="first">
|
||||
<th>File name</th>
|
||||
<th>Kind</th>
|
||||
<th>OS</th>
|
||||
<th>Arch</th>
|
||||
<th>Size</th>
|
||||
{{/* Use the checksum type of the first file for the column heading. */}}
|
||||
<th>{{(index .Files 0).ChecksumType}} Checksum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{if .SplitPortTable}}
|
||||
{{range .Files}}{{if .PrimaryPort}}{{template "file" .}}{{end}}{{end}}
|
||||
|
||||
{{/* TODO(cbro): add a link to an explanatory doc page */}}
|
||||
<tr class="first"><th colspan="6" class="first">Other Ports</th></tr>
|
||||
{{range .Files}}{{if not .PrimaryPort}}{{template "file" .}}{{end}}{{end}}
|
||||
{{else}}
|
||||
{{range .Files}}{{template "file" .}}{{end}}
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
{{define "file"}}
|
||||
<tr{{if .Highlight}} class="highlight"{{end}}>
|
||||
<td class="filename"><a class="download" href="{{.URL}}">{{.Filename}}</a></td>
|
||||
<td>{{pretty .Kind}}</td>
|
||||
<td>{{.PrettyOS}}</td>
|
||||
<td>{{pretty .Arch}}</td>
|
||||
<td>{{.PrettySize}}</td>
|
||||
<td><tt>{{.PrettyChecksum}}</tt></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
{{define "download"}}
|
||||
<a class="download downloadBox" href="{{.URL}}">
|
||||
<div class="platform">{{.Platform}}</div>
|
||||
{{with .Requirements}}<div class="reqs">{{.}}</div>{{end}}
|
||||
<div>
|
||||
<span class="filename">{{.Filename}}</span>
|
||||
{{if .Size}}<span class="size">({{.PrettySize}})</span>{{end}}
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
`
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// Package env provides environment information for the godoc server running on
|
||||
// golang.org.
|
||||
package env
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
isProd = boolEnv("GODOC_PROD")
|
||||
enforceHosts = boolEnv("GODOC_ENFORCE_HOSTS")
|
||||
)
|
||||
|
||||
// IsProd reports whether the server is running in its production configuration
|
||||
// on golang.org.
|
||||
func IsProd() bool {
|
||||
return isProd
|
||||
}
|
||||
|
||||
// EnforceHosts reports whether host filtering should be enforced.
|
||||
func EnforceHosts() bool {
|
||||
return enforceHosts
|
||||
}
|
||||
|
||||
func boolEnv(key string) bool {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
log.Fatalf("environment variable %s (%q) must be a boolean", key, v)
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// This file implements FormatSelections and FormatText.
|
||||
// FormatText is used to HTML-format Go and non-Go source
|
||||
// text with line numbers and highlighted sections. It is
|
||||
// built on top of FormatSelections, a generic formatter
|
||||
// for "selected" text.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Implementation of FormatSelections
|
||||
|
||||
// A Segment describes a text segment [start, end).
|
||||
// The zero value of a Segment is a ready-to-use empty segment.
|
||||
//
|
||||
type Segment struct {
|
||||
start, end int
|
||||
}
|
||||
|
||||
func (seg *Segment) isEmpty() bool { return seg.start >= seg.end }
|
||||
|
||||
// A Selection is an "iterator" function returning a text segment.
|
||||
// Repeated calls to a selection return consecutive, non-overlapping,
|
||||
// non-empty segments, followed by an infinite sequence of empty
|
||||
// segments. The first empty segment marks the end of the selection.
|
||||
//
|
||||
type Selection func() Segment
|
||||
|
||||
// A LinkWriter writes some start or end "tag" to w for the text offset offs.
|
||||
// It is called by FormatSelections at the start or end of each link segment.
|
||||
//
|
||||
type LinkWriter func(w io.Writer, offs int, start bool)
|
||||
|
||||
// A SegmentWriter formats a text according to selections and writes it to w.
|
||||
// The selections parameter is a bit set indicating which selections provided
|
||||
// to FormatSelections overlap with the text segment: If the n'th bit is set
|
||||
// in selections, the n'th selection provided to FormatSelections is overlapping
|
||||
// with the text.
|
||||
//
|
||||
type SegmentWriter func(w io.Writer, text []byte, selections int)
|
||||
|
||||
// FormatSelections takes a text and writes it to w using link and segment
|
||||
// writers lw and sw as follows: lw is invoked for consecutive segment starts
|
||||
// and ends as specified through the links selection, and sw is invoked for
|
||||
// consecutive segments of text overlapped by the same selections as specified
|
||||
// by selections. The link writer lw may be nil, in which case the links
|
||||
// Selection is ignored.
|
||||
//
|
||||
func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection, sw SegmentWriter, selections ...Selection) {
|
||||
// If we have a link writer, make the links
|
||||
// selection the last entry in selections
|
||||
if lw != nil {
|
||||
selections = append(selections, links)
|
||||
}
|
||||
|
||||
// compute the sequence of consecutive segment changes
|
||||
changes := newMerger(selections)
|
||||
|
||||
// The i'th bit in bitset indicates that the text
|
||||
// at the current offset is covered by selections[i].
|
||||
bitset := 0
|
||||
lastOffs := 0
|
||||
|
||||
// Text segments are written in a delayed fashion
|
||||
// such that consecutive segments belonging to the
|
||||
// same selection can be combined (peephole optimization).
|
||||
// last describes the last segment which has not yet been written.
|
||||
var last struct {
|
||||
begin, end int // valid if begin < end
|
||||
bitset int
|
||||
}
|
||||
|
||||
// flush writes the last delayed text segment
|
||||
flush := func() {
|
||||
if last.begin < last.end {
|
||||
sw(w, text[last.begin:last.end], last.bitset)
|
||||
}
|
||||
last.begin = last.end // invalidate last
|
||||
}
|
||||
|
||||
// segment runs the segment [lastOffs, end) with the selection
|
||||
// indicated by bitset through the segment peephole optimizer.
|
||||
segment := func(end int) {
|
||||
if lastOffs < end { // ignore empty segments
|
||||
if last.end != lastOffs || last.bitset != bitset {
|
||||
// the last segment is not adjacent to or
|
||||
// differs from the new one
|
||||
flush()
|
||||
// start a new segment
|
||||
last.begin = lastOffs
|
||||
}
|
||||
last.end = end
|
||||
last.bitset = bitset
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// get the next segment change
|
||||
index, offs, start := changes.next()
|
||||
if index < 0 || offs > len(text) {
|
||||
// no more segment changes or the next change
|
||||
// is past the end of the text - we're done
|
||||
break
|
||||
}
|
||||
// determine the kind of segment change
|
||||
if lw != nil && index == len(selections)-1 {
|
||||
// we have a link segment change (see start of this function):
|
||||
// format the previous selection segment, write the
|
||||
// link tag and start a new selection segment
|
||||
segment(offs)
|
||||
flush()
|
||||
lastOffs = offs
|
||||
lw(w, offs, start)
|
||||
} else {
|
||||
// we have a selection change:
|
||||
// format the previous selection segment, determine
|
||||
// the new selection bitset and start a new segment
|
||||
segment(offs)
|
||||
lastOffs = offs
|
||||
mask := 1 << uint(index)
|
||||
if start {
|
||||
bitset |= mask
|
||||
} else {
|
||||
bitset &^= mask
|
||||
}
|
||||
}
|
||||
}
|
||||
segment(len(text))
|
||||
flush()
|
||||
}
|
||||
|
||||
// A merger merges a slice of Selections and produces a sequence of
|
||||
// consecutive segment change events through repeated next() calls.
|
||||
//
|
||||
type merger struct {
|
||||
selections []Selection
|
||||
segments []Segment // segments[i] is the next segment of selections[i]
|
||||
}
|
||||
|
||||
const infinity int = 2e9
|
||||
|
||||
func newMerger(selections []Selection) *merger {
|
||||
segments := make([]Segment, len(selections))
|
||||
for i, sel := range selections {
|
||||
segments[i] = Segment{infinity, infinity}
|
||||
if sel != nil {
|
||||
if seg := sel(); !seg.isEmpty() {
|
||||
segments[i] = seg
|
||||
}
|
||||
}
|
||||
}
|
||||
return &merger{selections, segments}
|
||||
}
|
||||
|
||||
// next returns the next segment change: index specifies the Selection
|
||||
// to which the segment belongs, offs is the segment start or end offset
|
||||
// as determined by the start value. If there are no more segment changes,
|
||||
// next returns an index value < 0.
|
||||
//
|
||||
func (m *merger) next() (index, offs int, start bool) {
|
||||
// find the next smallest offset where a segment starts or ends
|
||||
offs = infinity
|
||||
index = -1
|
||||
for i, seg := range m.segments {
|
||||
switch {
|
||||
case seg.start < offs:
|
||||
offs = seg.start
|
||||
index = i
|
||||
start = true
|
||||
case seg.end < offs:
|
||||
offs = seg.end
|
||||
index = i
|
||||
start = false
|
||||
}
|
||||
}
|
||||
if index < 0 {
|
||||
// no offset found => all selections merged
|
||||
return
|
||||
}
|
||||
// offset found - it's either the start or end offset but
|
||||
// either way it is ok to consume the start offset: set it
|
||||
// to infinity so it won't be considered in the following
|
||||
// next call
|
||||
m.segments[index].start = infinity
|
||||
if start {
|
||||
return
|
||||
}
|
||||
// end offset found - consume it
|
||||
m.segments[index].end = infinity
|
||||
// advance to the next segment for that selection
|
||||
seg := m.selections[index]()
|
||||
if !seg.isEmpty() {
|
||||
m.segments[index] = seg
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Implementation of FormatText
|
||||
|
||||
// lineSelection returns the line segments for text as a Selection.
|
||||
func lineSelection(text []byte) Selection {
|
||||
i, j := 0, 0
|
||||
return func() (seg Segment) {
|
||||
// find next newline, if any
|
||||
for j < len(text) {
|
||||
j++
|
||||
if text[j-1] == '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < j {
|
||||
// text[i:j] constitutes a line
|
||||
seg = Segment{i, j}
|
||||
i = j
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// tokenSelection returns, as a selection, the sequence of
|
||||
// consecutive occurrences of token sel in the Go src text.
|
||||
//
|
||||
func tokenSelection(src []byte, sel token.Token) Selection {
|
||||
var s scanner.Scanner
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
s.Init(file, src, nil, scanner.ScanComments)
|
||||
return func() (seg Segment) {
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
offs := file.Offset(pos)
|
||||
if tok == sel {
|
||||
seg = Segment{offs, offs + len(lit)}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// makeSelection is a helper function to make a Selection from a slice of pairs.
|
||||
// Pairs describing empty segments are ignored.
|
||||
//
|
||||
func makeSelection(matches [][]int) Selection {
|
||||
i := 0
|
||||
return func() Segment {
|
||||
for i < len(matches) {
|
||||
m := matches[i]
|
||||
i++
|
||||
if m[0] < m[1] {
|
||||
// non-empty segment
|
||||
return Segment{m[0], m[1]}
|
||||
}
|
||||
}
|
||||
return Segment{}
|
||||
}
|
||||
}
|
||||
|
||||
// regexpSelection computes the Selection for the regular expression expr in text.
|
||||
func regexpSelection(text []byte, expr string) Selection {
|
||||
var matches [][]int
|
||||
if rx, err := regexp.Compile(expr); err == nil {
|
||||
matches = rx.FindAllIndex(text, -1)
|
||||
}
|
||||
return makeSelection(matches)
|
||||
}
|
||||
|
||||
var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
|
||||
|
||||
// RangeSelection computes the Selection for a text range described
|
||||
// by the argument str; the range description must match the selRx
|
||||
// regular expression.
|
||||
func RangeSelection(str string) Selection {
|
||||
m := selRx.FindStringSubmatch(str)
|
||||
if len(m) >= 2 {
|
||||
from, _ := strconv.Atoi(m[1])
|
||||
to, _ := strconv.Atoi(m[2])
|
||||
if from < to {
|
||||
return makeSelection([][]int{{from, to}})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Span tags for all the possible selection combinations that may
|
||||
// be generated by FormatText. Selections are indicated by a bitset,
|
||||
// and the value of the bitset specifies the tag to be used.
|
||||
//
|
||||
// bit 0: comments
|
||||
// bit 1: highlights
|
||||
// bit 2: selections
|
||||
//
|
||||
var startTags = [][]byte{
|
||||
/* 000 */ []byte(``),
|
||||
/* 001 */ []byte(`<span class="comment">`),
|
||||
/* 010 */ []byte(`<span class="highlight">`),
|
||||
/* 011 */ []byte(`<span class="highlight-comment">`),
|
||||
/* 100 */ []byte(`<span class="selection">`),
|
||||
/* 101 */ []byte(`<span class="selection-comment">`),
|
||||
/* 110 */ []byte(`<span class="selection-highlight">`),
|
||||
/* 111 */ []byte(`<span class="selection-highlight-comment">`),
|
||||
}
|
||||
|
||||
var endTag = []byte(`</span>`)
|
||||
|
||||
func selectionTag(w io.Writer, text []byte, selections int) {
|
||||
if selections < len(startTags) {
|
||||
if tag := startTags[selections]; len(tag) > 0 {
|
||||
w.Write(tag)
|
||||
template.HTMLEscape(w, text)
|
||||
w.Write(endTag)
|
||||
return
|
||||
}
|
||||
}
|
||||
template.HTMLEscape(w, text)
|
||||
}
|
||||
|
||||
// FormatText HTML-escapes text and writes it to w.
|
||||
// Consecutive text segments are wrapped in HTML spans (with tags as
|
||||
// defined by startTags and endTag) as follows:
|
||||
//
|
||||
// - if line >= 0, line number (ln) spans are inserted before each line,
|
||||
// starting with the value of line
|
||||
// - if the text is Go source, comments get the "comment" span class
|
||||
// - each occurrence of the regular expression pattern gets the "highlight"
|
||||
// span class
|
||||
// - text segments covered by selection get the "selection" span class
|
||||
//
|
||||
// Comments, highlights, and selections may overlap arbitrarily; the respective
|
||||
// HTML span classes are specified in the startTags variable.
|
||||
//
|
||||
func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) {
|
||||
var comments, highlights Selection
|
||||
if goSource {
|
||||
comments = tokenSelection(text, token.COMMENT)
|
||||
}
|
||||
if pattern != "" {
|
||||
highlights = regexpSelection(text, pattern)
|
||||
}
|
||||
if line >= 0 || comments != nil || highlights != nil || selection != nil {
|
||||
var lineTag LinkWriter
|
||||
if line >= 0 {
|
||||
lineTag = func(w io.Writer, _ int, start bool) {
|
||||
if start {
|
||||
fmt.Fprintf(w, "<span id=\"L%d\" class=\"ln\">%6d</span>\t", line, line)
|
||||
line++
|
||||
}
|
||||
}
|
||||
}
|
||||
FormatSelections(w, text, lineTag, lineSelection(text), selectionTag, comments, highlights, selection)
|
||||
} else {
|
||||
template.HTMLEscape(w, text)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,959 @@
|
|||
// 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.
|
||||
|
||||
// Package godoc is a work-in-progress (2013-07-17) package to
|
||||
// begin splitting up the godoc binary into multiple pieces.
|
||||
//
|
||||
// This package comment will evolve over time as this package splits
|
||||
// into smaller pieces.
|
||||
package godoc // import "golang.org/x/website/cmd/golangorg/godoc"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/format"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
htmltemplate "html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Fake relative package path for built-ins. Documentation for all globals
|
||||
// (not just exported ones) will be shown for packages in this directory.
|
||||
const builtinPkgPath = "builtin"
|
||||
|
||||
// FuncMap defines template functions used in godoc templates.
|
||||
//
|
||||
// Convention: template function names ending in "_html" or "_url" produce
|
||||
// HTML- or URL-escaped strings; all other function results may
|
||||
// require explicit escaping in the template.
|
||||
func (p *Presentation) FuncMap() template.FuncMap {
|
||||
p.initFuncMapOnce.Do(p.initFuncMap)
|
||||
return p.funcMap
|
||||
}
|
||||
|
||||
func (p *Presentation) TemplateFuncs() template.FuncMap {
|
||||
p.initFuncMapOnce.Do(p.initFuncMap)
|
||||
return p.templateFuncs
|
||||
}
|
||||
|
||||
func (p *Presentation) initFuncMap() {
|
||||
if p.Corpus == nil {
|
||||
panic("nil Presentation.Corpus")
|
||||
}
|
||||
p.templateFuncs = template.FuncMap{
|
||||
"code": p.code,
|
||||
}
|
||||
p.funcMap = template.FuncMap{
|
||||
// various helpers
|
||||
"filename": filenameFunc,
|
||||
"repeat": strings.Repeat,
|
||||
"since": p.Corpus.pkgAPIInfo.sinceVersionFunc,
|
||||
|
||||
// access to FileInfos (directory listings)
|
||||
"fileInfoName": fileInfoNameFunc,
|
||||
"fileInfoTime": fileInfoTimeFunc,
|
||||
|
||||
// access to search result information
|
||||
"infoKind_html": infoKind_htmlFunc,
|
||||
"infoLine": p.infoLineFunc,
|
||||
"infoSnippet_html": p.infoSnippet_htmlFunc,
|
||||
|
||||
// formatting of AST nodes
|
||||
"node": p.nodeFunc,
|
||||
"node_html": p.node_htmlFunc,
|
||||
"comment_html": comment_htmlFunc,
|
||||
"sanitize": sanitizeFunc,
|
||||
|
||||
// support for URL attributes
|
||||
"pkgLink": pkgLinkFunc,
|
||||
"srcLink": srcLinkFunc,
|
||||
"posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
|
||||
"docLink": docLinkFunc,
|
||||
"queryLink": queryLinkFunc,
|
||||
"srcBreadcrumb": srcBreadcrumbFunc,
|
||||
"srcToPkgLink": srcToPkgLinkFunc,
|
||||
|
||||
// formatting of Examples
|
||||
"example_html": p.example_htmlFunc,
|
||||
"example_name": p.example_nameFunc,
|
||||
"example_suffix": p.example_suffixFunc,
|
||||
|
||||
// formatting of analysis information
|
||||
"callgraph_html": p.callgraph_htmlFunc,
|
||||
"implements_html": p.implements_htmlFunc,
|
||||
"methodset_html": p.methodset_htmlFunc,
|
||||
|
||||
// formatting of Notes
|
||||
"noteTitle": noteTitle,
|
||||
|
||||
// Number operation
|
||||
"multiply": multiply,
|
||||
|
||||
// formatting of PageInfoMode query string
|
||||
"modeQueryString": modeQueryString,
|
||||
|
||||
// check whether to display third party section or not
|
||||
"hasThirdParty": hasThirdParty,
|
||||
|
||||
// get the no. of columns to split the toc in search page
|
||||
"tocColCount": tocColCount,
|
||||
}
|
||||
if p.URLForSrc != nil {
|
||||
p.funcMap["srcLink"] = p.URLForSrc
|
||||
}
|
||||
if p.URLForSrcPos != nil {
|
||||
p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos)
|
||||
}
|
||||
if p.URLForSrcQuery != nil {
|
||||
p.funcMap["queryLink"] = p.URLForSrcQuery
|
||||
}
|
||||
}
|
||||
|
||||
func multiply(a, b int) int { return a * b }
|
||||
|
||||
func filenameFunc(path string) string {
|
||||
_, localname := pathpkg.Split(path)
|
||||
return localname
|
||||
}
|
||||
|
||||
func fileInfoNameFunc(fi os.FileInfo) string {
|
||||
name := fi.Name()
|
||||
if fi.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func fileInfoTimeFunc(fi os.FileInfo) string {
|
||||
if t := fi.ModTime(); t.Unix() != 0 {
|
||||
return t.Local().String()
|
||||
}
|
||||
return "" // don't return epoch if time is obviously not set
|
||||
}
|
||||
|
||||
// The strings in infoKinds must be properly html-escaped.
|
||||
var infoKinds = [nKinds]string{
|
||||
PackageClause: "package clause",
|
||||
ImportDecl: "import decl",
|
||||
ConstDecl: "const decl",
|
||||
TypeDecl: "type decl",
|
||||
VarDecl: "var decl",
|
||||
FuncDecl: "func decl",
|
||||
MethodDecl: "method decl",
|
||||
Use: "use",
|
||||
}
|
||||
|
||||
func infoKind_htmlFunc(info SpotInfo) string {
|
||||
return infoKinds[info.Kind()] // infoKind entries are html-escaped
|
||||
}
|
||||
|
||||
func (p *Presentation) infoLineFunc(info SpotInfo) int {
|
||||
line := info.Lori()
|
||||
if info.IsIndex() {
|
||||
index, _ := p.Corpus.searchIndex.Get()
|
||||
if index != nil {
|
||||
line = index.(*Index).Snippet(line).Line
|
||||
} else {
|
||||
// no line information available because
|
||||
// we don't have an index - this should
|
||||
// never happen; be conservative and don't
|
||||
// crash
|
||||
line = 0
|
||||
}
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
|
||||
if info.IsIndex() {
|
||||
index, _ := p.Corpus.searchIndex.Get()
|
||||
// Snippet.Text was HTML-escaped when it was generated
|
||||
return index.(*Index).Snippet(info.Lori()).Text
|
||||
}
|
||||
return `<span class="alert">no snippet text available</span>`
|
||||
}
|
||||
|
||||
func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
p.writeNode(&buf, info, info.FSet, node)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
|
||||
var buf1 bytes.Buffer
|
||||
p.writeNode(&buf1, info, info.FSet, node)
|
||||
|
||||
var buf2 bytes.Buffer
|
||||
if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
|
||||
LinkifyText(&buf2, buf1.Bytes(), n)
|
||||
if st, name := isStructTypeDecl(n); st != nil {
|
||||
addStructFieldIDAttributes(&buf2, name, st)
|
||||
}
|
||||
} else {
|
||||
FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
|
||||
}
|
||||
|
||||
return buf2.String()
|
||||
}
|
||||
|
||||
// isStructTypeDecl checks whether n is a struct declaration.
|
||||
// It either returns a non-nil StructType and its name, or zero values.
|
||||
func isStructTypeDecl(n ast.Node) (st *ast.StructType, name string) {
|
||||
gd, ok := n.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.TYPE {
|
||||
return nil, ""
|
||||
}
|
||||
if gd.Lparen > 0 {
|
||||
// Parenthesized type. Who does that, anyway?
|
||||
// TODO: Reportedly gri does. Fix this to handle that too.
|
||||
return nil, ""
|
||||
}
|
||||
if len(gd.Specs) != 1 {
|
||||
return nil, ""
|
||||
}
|
||||
ts, ok := gd.Specs[0].(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return nil, ""
|
||||
}
|
||||
st, ok = ts.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, ""
|
||||
}
|
||||
return st, ts.Name.Name
|
||||
}
|
||||
|
||||
// addStructFieldIDAttributes modifies the contents of buf such that
|
||||
// all struct fields of the named struct have <span id='name.Field'>
|
||||
// in them, so people can link to /#Struct.Field.
|
||||
func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) {
|
||||
if st.Fields == nil {
|
||||
return
|
||||
}
|
||||
// needsLink is a set of identifiers that still need to be
|
||||
// linked, where value == key, to avoid an allocation in func
|
||||
// linkedField.
|
||||
needsLink := make(map[string]string)
|
||||
|
||||
for _, f := range st.Fields.List {
|
||||
if len(f.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
fieldName := f.Names[0].Name
|
||||
needsLink[fieldName] = fieldName
|
||||
}
|
||||
var newBuf bytes.Buffer
|
||||
foreachLine(buf.Bytes(), func(line []byte) {
|
||||
if fieldName := linkedField(line, needsLink); fieldName != "" {
|
||||
fmt.Fprintf(&newBuf, `<span id="%s.%s"></span>`, name, fieldName)
|
||||
delete(needsLink, fieldName)
|
||||
}
|
||||
newBuf.Write(line)
|
||||
})
|
||||
buf.Reset()
|
||||
buf.Write(newBuf.Bytes())
|
||||
}
|
||||
|
||||
// foreachLine calls fn for each line of in, where a line includes
|
||||
// the trailing "\n", except on the last line, if it doesn't exist.
|
||||
func foreachLine(in []byte, fn func(line []byte)) {
|
||||
for len(in) > 0 {
|
||||
nl := bytes.IndexByte(in, '\n')
|
||||
if nl == -1 {
|
||||
fn(in)
|
||||
return
|
||||
}
|
||||
fn(in[:nl+1])
|
||||
in = in[nl+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// commentPrefix is the line prefix for comments after they've been HTMLified.
|
||||
var commentPrefix = []byte(`<span class="comment">// `)
|
||||
|
||||
// linkedField determines whether the given line starts with an
|
||||
// identifer in the provided ids map (mapping from identifier to the
|
||||
// same identifier). The line can start with either an identifier or
|
||||
// an identifier in a comment. If one matches, it returns the
|
||||
// identifier that matched. Otherwise it returns the empty string.
|
||||
func linkedField(line []byte, ids map[string]string) string {
|
||||
line = bytes.TrimSpace(line)
|
||||
|
||||
// For fields with a doc string of the
|
||||
// conventional form, we put the new span into
|
||||
// the comment instead of the field.
|
||||
// The "conventional" form is a complete sentence
|
||||
// per https://golang.org/s/style#comment-sentences like:
|
||||
//
|
||||
// // Foo is an optional Fooer to foo the foos.
|
||||
// Foo Fooer
|
||||
//
|
||||
// In this case, we want the #StructName.Foo
|
||||
// link to make the browser go to the comment
|
||||
// line "Foo is an optional Fooer" instead of
|
||||
// the "Foo Fooer" line, which could otherwise
|
||||
// obscure the docs above the browser's "fold".
|
||||
//
|
||||
// TODO: do this better, so it works for all
|
||||
// comments, including unconventional ones.
|
||||
if bytes.HasPrefix(line, commentPrefix) {
|
||||
line = line[len(commentPrefix):]
|
||||
}
|
||||
id := scanIdentifier(line)
|
||||
if len(id) == 0 {
|
||||
// No leading identifier. Avoid map lookup for
|
||||
// somewhat common case.
|
||||
return ""
|
||||
}
|
||||
return ids[string(id)]
|
||||
}
|
||||
|
||||
// scanIdentifier scans a valid Go identifier off the front of v and
|
||||
// either returns a subslice of v if there's a valid identifier, or
|
||||
// returns a zero-length slice.
|
||||
func scanIdentifier(v []byte) []byte {
|
||||
var n int // number of leading bytes of v belonging to an identifier
|
||||
for {
|
||||
r, width := utf8.DecodeRune(v[n:])
|
||||
if !(isLetter(r) || n > 0 && isDigit(r)) {
|
||||
break
|
||||
}
|
||||
n += width
|
||||
}
|
||||
return v[:n]
|
||||
}
|
||||
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
func comment_htmlFunc(comment string) string {
|
||||
var buf bytes.Buffer
|
||||
// TODO(gri) Provide list of words (e.g. function parameters)
|
||||
// to be emphasized by ToHTML.
|
||||
doc.ToHTML(&buf, comment, nil) // does html-escaping
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// punchCardWidth is the number of columns of fixed-width
|
||||
// characters to assume when wrapping text. Very few people
|
||||
// use terminals or cards smaller than 80 characters, so 80 it is.
|
||||
// We do not try to sniff the environment or the tty to adapt to
|
||||
// the situation; instead, by using a constant we make sure that
|
||||
// godoc always produces the same output regardless of context,
|
||||
// a consistency that is lost otherwise. For example, if we sniffed
|
||||
// the environment or tty, then http://golang.org/pkg/math/?m=text
|
||||
// would depend on the width of the terminal where godoc started,
|
||||
// which is clearly bogus. More generally, the Unix tools that behave
|
||||
// differently when writing to a tty than when writing to a file have
|
||||
// a history of causing confusion (compare `ls` and `ls | cat`), and we
|
||||
// want to avoid that mistake here.
|
||||
const punchCardWidth = 80
|
||||
|
||||
func containsOnlySpace(buf []byte) bool {
|
||||
isNotSpace := func(r rune) bool { return !unicode.IsSpace(r) }
|
||||
return bytes.IndexFunc(buf, isNotSpace) == -1
|
||||
}
|
||||
|
||||
// sanitizeFunc sanitizes the argument src by replacing newlines with
|
||||
// blanks, removing extra blanks, and by removing trailing whitespace
|
||||
// and commas before closing parentheses.
|
||||
func sanitizeFunc(src string) string {
|
||||
buf := make([]byte, len(src))
|
||||
j := 0 // buf index
|
||||
comma := -1 // comma index if >= 0
|
||||
for i := 0; i < len(src); i++ {
|
||||
ch := src[i]
|
||||
switch ch {
|
||||
case '\t', '\n', ' ':
|
||||
// ignore whitespace at the beginning, after a blank, or after opening parentheses
|
||||
if j == 0 {
|
||||
continue
|
||||
}
|
||||
if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
|
||||
continue
|
||||
}
|
||||
// replace all whitespace with blanks
|
||||
ch = ' '
|
||||
case ',':
|
||||
comma = j
|
||||
case ')', '}', ']':
|
||||
// remove any trailing comma
|
||||
if comma >= 0 {
|
||||
j = comma
|
||||
}
|
||||
// remove any trailing whitespace
|
||||
if j > 0 && buf[j-1] == ' ' {
|
||||
j--
|
||||
}
|
||||
default:
|
||||
comma = -1
|
||||
}
|
||||
buf[j] = ch
|
||||
j++
|
||||
}
|
||||
// remove trailing blank, if any
|
||||
if j > 0 && buf[j-1] == ' ' {
|
||||
j--
|
||||
}
|
||||
return string(buf[:j])
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Dirname string // directory containing the package
|
||||
Err error // error or nil
|
||||
GoogleCN bool // page is being served from golang.google.cn
|
||||
|
||||
Mode PageInfoMode // display metadata from query string
|
||||
|
||||
// package info
|
||||
FSet *token.FileSet // nil if no package documentation
|
||||
PDoc *doc.Package // nil if no package documentation
|
||||
Examples []*doc.Example // nil if no example code
|
||||
Notes map[string][]*doc.Note // nil if no package Notes
|
||||
PAst map[string]*ast.File // nil if no AST with package exports
|
||||
IsMain bool // true for package main
|
||||
IsFiltered bool // true if results were filtered
|
||||
|
||||
// analysis info
|
||||
TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type)
|
||||
AnalysisData htmltemplate.JS // array of TypeInfoJSON values
|
||||
CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer)
|
||||
CallGraphIndex map[string]int // maps func name to index in CallGraph
|
||||
|
||||
// directory info
|
||||
Dirs *DirList // nil if no directory information
|
||||
DirTime time.Time // directory time stamp
|
||||
DirFlat bool // if set, show directory in a flat (non-indented) manner
|
||||
}
|
||||
|
||||
func (info *PageInfo) IsEmpty() bool {
|
||||
return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
|
||||
}
|
||||
|
||||
func pkgLinkFunc(path string) string {
|
||||
// because of the irregular mapping under goroot
|
||||
// we need to correct certain relative paths
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimPrefix(path, "src/")
|
||||
path = strings.TrimPrefix(path, "pkg/")
|
||||
return "pkg/" + path
|
||||
}
|
||||
|
||||
// srcToPkgLinkFunc builds an <a> tag linking to the package
|
||||
// documentation of relpath.
|
||||
func srcToPkgLinkFunc(relpath string) string {
|
||||
relpath = pkgLinkFunc(relpath)
|
||||
relpath = pathpkg.Dir(relpath)
|
||||
if relpath == "pkg" {
|
||||
return `<a href="/pkg">Index</a>`
|
||||
}
|
||||
return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
|
||||
}
|
||||
|
||||
// srcBreadcrumbFun converts each segment of relpath to a HTML <a>.
|
||||
// Each segment links to its corresponding src directories.
|
||||
func srcBreadcrumbFunc(relpath string) string {
|
||||
segments := strings.Split(relpath, "/")
|
||||
var buf bytes.Buffer
|
||||
var selectedSegment string
|
||||
var selectedIndex int
|
||||
|
||||
if strings.HasSuffix(relpath, "/") {
|
||||
// relpath is a directory ending with a "/".
|
||||
// Selected segment is the segment before the last slash.
|
||||
selectedIndex = len(segments) - 2
|
||||
selectedSegment = segments[selectedIndex] + "/"
|
||||
} else {
|
||||
selectedIndex = len(segments) - 1
|
||||
selectedSegment = segments[selectedIndex]
|
||||
}
|
||||
|
||||
for i := range segments[:selectedIndex] {
|
||||
buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
|
||||
strings.Join(segments[:i+1], "/"),
|
||||
segments[i],
|
||||
))
|
||||
}
|
||||
|
||||
buf.WriteString(`<span class="text-muted">`)
|
||||
buf.WriteString(selectedSegment)
|
||||
buf.WriteString(`</span>`)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
|
||||
// n must be an ast.Node or a *doc.Note
|
||||
return func(info *PageInfo, n interface{}) string {
|
||||
var pos, end token.Pos
|
||||
|
||||
switch n := n.(type) {
|
||||
case ast.Node:
|
||||
pos = n.Pos()
|
||||
end = n.End()
|
||||
case *doc.Note:
|
||||
pos = n.Pos
|
||||
end = n.End
|
||||
default:
|
||||
panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
|
||||
}
|
||||
|
||||
var relpath string
|
||||
var line int
|
||||
var low, high int // selection offset range
|
||||
|
||||
if pos.IsValid() {
|
||||
p := info.FSet.Position(pos)
|
||||
relpath = p.Filename
|
||||
line = p.Line
|
||||
low = p.Offset
|
||||
}
|
||||
if end.IsValid() {
|
||||
high = info.FSet.Position(end).Offset
|
||||
}
|
||||
|
||||
return srcPosLinkFunc(relpath, line, low, high)
|
||||
}
|
||||
}
|
||||
|
||||
func srcPosLinkFunc(s string, line, low, high int) string {
|
||||
s = srcLinkFunc(s)
|
||||
var buf bytes.Buffer
|
||||
template.HTMLEscape(&buf, []byte(s))
|
||||
// selection ranges are of form "s=low:high"
|
||||
if low < high {
|
||||
fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
|
||||
// if we have a selection, position the page
|
||||
// such that the selection is a bit below the top
|
||||
line -= 10
|
||||
if line < 1 {
|
||||
line = 1
|
||||
}
|
||||
}
|
||||
// line id's in html-printed source are of the
|
||||
// form "L%d" where %d stands for the line number
|
||||
if line > 0 {
|
||||
fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func srcLinkFunc(s string) string {
|
||||
s = pathpkg.Clean("/" + s)
|
||||
if !strings.HasPrefix(s, "/src/") {
|
||||
s = "/src" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// queryLinkFunc returns a URL for a line in a source file with a highlighted
|
||||
// query term.
|
||||
// s is expected to be a path to a source file.
|
||||
// query is expected to be a string that has already been appropriately escaped
|
||||
// for use in a URL query.
|
||||
func queryLinkFunc(s, query string, line int) string {
|
||||
url := pathpkg.Clean("/"+s) + "?h=" + query
|
||||
if line > 0 {
|
||||
url += "#L" + strconv.Itoa(line)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func docLinkFunc(s string, ident string) string {
|
||||
return pathpkg.Clean("/pkg/"+s) + "/#" + ident
|
||||
}
|
||||
|
||||
func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
|
||||
var buf bytes.Buffer
|
||||
for _, eg := range info.Examples {
|
||||
name := stripExampleSuffix(eg.Name)
|
||||
|
||||
if name != funcName {
|
||||
continue
|
||||
}
|
||||
|
||||
// print code
|
||||
cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
|
||||
code := p.node_htmlFunc(info, cnode, true)
|
||||
out := eg.Output
|
||||
wholeFile := true
|
||||
|
||||
// Additional formatting if this is a function body.
|
||||
if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
|
||||
wholeFile = false
|
||||
// remove surrounding braces
|
||||
code = code[1 : n-1]
|
||||
// unindent
|
||||
code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "")
|
||||
// remove output comment
|
||||
if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
|
||||
code = strings.TrimSpace(code[:loc[0]])
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the playground code in standard Go style
|
||||
// (use tabs, no comment highlight, etc).
|
||||
play := ""
|
||||
if eg.Play != nil && p.ShowPlayground {
|
||||
var buf bytes.Buffer
|
||||
eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
|
||||
if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
play = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Drop output, as the output comment will appear in the code.
|
||||
if wholeFile && play == "" {
|
||||
out = ""
|
||||
}
|
||||
|
||||
if p.ExampleHTML == nil {
|
||||
out = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
err := p.ExampleHTML.Execute(&buf, struct {
|
||||
Name, Doc, Code, Play, Output string
|
||||
GoogleCN bool
|
||||
}{eg.Name, eg.Doc, code, play, out, info.GoogleCN})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
|
||||
if len(cg) == 0 {
|
||||
return cg
|
||||
}
|
||||
|
||||
for i := range cg {
|
||||
if !strings.HasPrefix(cg[i].Text(), "+build ") {
|
||||
// Found the first non-build tag, return from here until the end
|
||||
// of the slice.
|
||||
return cg[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// There weren't any non-build tags, return an empty slice.
|
||||
return []*ast.CommentGroup{}
|
||||
}
|
||||
|
||||
// example_nameFunc takes an example function name and returns its display
|
||||
// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
|
||||
func (p *Presentation) example_nameFunc(s string) string {
|
||||
name, suffix := splitExampleName(s)
|
||||
// replace _ with . for method names
|
||||
name = strings.Replace(name, "_", ".", 1)
|
||||
// use "Package" if no name provided
|
||||
if name == "" {
|
||||
name = "Package"
|
||||
}
|
||||
return name + suffix
|
||||
}
|
||||
|
||||
// example_suffixFunc takes an example function name and returns its suffix in
|
||||
// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
|
||||
func (p *Presentation) example_suffixFunc(name string) string {
|
||||
_, suffix := splitExampleName(name)
|
||||
return suffix
|
||||
}
|
||||
|
||||
// implements_html returns the "> Implements" toggle for a package-level named type.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string {
|
||||
if p.ImplementsHTML == nil {
|
||||
return ""
|
||||
}
|
||||
index, ok := info.TypeInfoIndex[typeName]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// methodset_html returns the "> Method set" toggle for a package-level named type.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string {
|
||||
if p.MethodSetHTML == nil {
|
||||
return ""
|
||||
}
|
||||
index, ok := info.TypeInfoIndex[typeName]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// callgraph_html returns the "> Call graph" toggle for a package-level func.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
|
||||
if p.CallGraphHTML == nil {
|
||||
return ""
|
||||
}
|
||||
if recv != "" {
|
||||
// Format must match (*ssa.Function).RelString().
|
||||
name = fmt.Sprintf("(%s).%s", recv, name)
|
||||
}
|
||||
index, ok := info.CallGraphIndex[name]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func noteTitle(note string) string {
|
||||
return strings.Title(strings.ToLower(note))
|
||||
}
|
||||
|
||||
func startsWithUppercase(s string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(s)
|
||||
return unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
|
||||
|
||||
// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
|
||||
// while keeping uppercase Braz in Foo_Braz.
|
||||
func stripExampleSuffix(name string) string {
|
||||
if i := strings.LastIndex(name, "_"); i != -1 {
|
||||
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
|
||||
name = name[:i]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func splitExampleName(s string) (name, suffix string) {
|
||||
i := strings.LastIndex(s, "_")
|
||||
if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
|
||||
name = s[:i]
|
||||
suffix = " (" + strings.Title(s[i+1:]) + ")"
|
||||
return
|
||||
}
|
||||
name = s
|
||||
return
|
||||
}
|
||||
|
||||
// replaceLeadingIndentation replaces oldIndent at the beginning of each line
|
||||
// with newIndent. This is used for formatting examples. Raw strings that
|
||||
// span multiple lines are handled specially: oldIndent is not removed (since
|
||||
// go/printer will not add any indentation there), but newIndent is added
|
||||
// (since we may still want leading indentation).
|
||||
func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
|
||||
// Handle indent at the beginning of the first line. After this, we handle
|
||||
// indentation only after a newline.
|
||||
var buf bytes.Buffer
|
||||
if strings.HasPrefix(body, oldIndent) {
|
||||
buf.WriteString(newIndent)
|
||||
body = body[len(oldIndent):]
|
||||
}
|
||||
|
||||
// Use a state machine to keep track of whether we're in a string or
|
||||
// rune literal while we process the rest of the code.
|
||||
const (
|
||||
codeState = iota
|
||||
runeState
|
||||
interpretedStringState
|
||||
rawStringState
|
||||
)
|
||||
searchChars := []string{
|
||||
"'\"`\n", // codeState
|
||||
`\'`, // runeState
|
||||
`\"`, // interpretedStringState
|
||||
"`\n", // rawStringState
|
||||
// newlineState does not need to search
|
||||
}
|
||||
state := codeState
|
||||
for {
|
||||
i := strings.IndexAny(body, searchChars[state])
|
||||
if i < 0 {
|
||||
buf.WriteString(body)
|
||||
break
|
||||
}
|
||||
c := body[i]
|
||||
buf.WriteString(body[:i+1])
|
||||
body = body[i+1:]
|
||||
switch state {
|
||||
case codeState:
|
||||
switch c {
|
||||
case '\'':
|
||||
state = runeState
|
||||
case '"':
|
||||
state = interpretedStringState
|
||||
case '`':
|
||||
state = rawStringState
|
||||
case '\n':
|
||||
if strings.HasPrefix(body, oldIndent) {
|
||||
buf.WriteString(newIndent)
|
||||
body = body[len(oldIndent):]
|
||||
}
|
||||
}
|
||||
|
||||
case runeState:
|
||||
switch c {
|
||||
case '\\':
|
||||
r, size := utf8.DecodeRuneInString(body)
|
||||
buf.WriteRune(r)
|
||||
body = body[size:]
|
||||
case '\'':
|
||||
state = codeState
|
||||
}
|
||||
|
||||
case interpretedStringState:
|
||||
switch c {
|
||||
case '\\':
|
||||
r, size := utf8.DecodeRuneInString(body)
|
||||
buf.WriteRune(r)
|
||||
body = body[size:]
|
||||
case '"':
|
||||
state = codeState
|
||||
}
|
||||
|
||||
case rawStringState:
|
||||
switch c {
|
||||
case '`':
|
||||
state = codeState
|
||||
case '\n':
|
||||
buf.WriteString(newIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// writeNode writes the AST node x to w.
|
||||
//
|
||||
// The provided fset must be non-nil. The pageInfo is optional. If
|
||||
// present, the pageInfo is used to add comments to struct fields to
|
||||
// say which version of Go introduced them.
|
||||
func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
|
||||
// convert trailing tabs into spaces using a tconv filter
|
||||
// to ensure a good outcome in most browsers (there may still
|
||||
// be tabs in comments and strings, but converting those into
|
||||
// the right number of spaces is much harder)
|
||||
//
|
||||
// TODO(gri) rethink printer flags - perhaps tconv can be eliminated
|
||||
// with an another printer mode (which is more efficiently
|
||||
// implemented in the printer than here with another layer)
|
||||
|
||||
var pkgName, structName string
|
||||
var apiInfo pkgAPIVersions
|
||||
if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
|
||||
p.Corpus != nil &&
|
||||
gd.Tok == token.TYPE && len(gd.Specs) != 0 {
|
||||
pkgName = pageInfo.PDoc.ImportPath
|
||||
if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
|
||||
if _, ok := ts.Type.(*ast.StructType); ok {
|
||||
structName = ts.Name.Name
|
||||
}
|
||||
}
|
||||
apiInfo = p.Corpus.pkgAPIInfo[pkgName]
|
||||
}
|
||||
|
||||
var out = w
|
||||
var buf bytes.Buffer
|
||||
if structName != "" {
|
||||
out = &buf
|
||||
}
|
||||
|
||||
mode := printer.TabIndent | printer.UseSpaces
|
||||
err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
// Add comments to struct fields saying which Go version introducd them.
|
||||
if structName != "" {
|
||||
fieldSince := apiInfo.fieldSince[structName]
|
||||
typeSince := apiInfo.typeSince[structName]
|
||||
// Add/rewrite comments on struct fields to note which Go version added them.
|
||||
var buf2 bytes.Buffer
|
||||
buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
|
||||
bs := bufio.NewScanner(&buf)
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
field := firstIdent(line)
|
||||
var since string
|
||||
if field != "" {
|
||||
since = fieldSince[field]
|
||||
if since != "" && since == typeSince {
|
||||
// Don't highlight field versions if they were the
|
||||
// same as the struct itself.
|
||||
since = ""
|
||||
}
|
||||
}
|
||||
if since == "" {
|
||||
buf2.Write(line)
|
||||
} else {
|
||||
if bytes.Contains(line, slashSlash) {
|
||||
line = bytes.TrimRight(line, " \t.")
|
||||
buf2.Write(line)
|
||||
buf2.WriteString("; added in Go ")
|
||||
} else {
|
||||
buf2.Write(line)
|
||||
buf2.WriteString(" // Go ")
|
||||
}
|
||||
buf2.WriteString(since)
|
||||
}
|
||||
buf2.WriteByte('\n')
|
||||
}
|
||||
w.Write(buf2.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
var slashSlash = []byte("//")
|
||||
|
||||
// WriteNode writes x to w.
|
||||
// TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode.
|
||||
func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
|
||||
p.writeNode(w, nil, fset, x)
|
||||
}
|
||||
|
||||
// firstIdent returns the first identifier in x.
|
||||
// This actually parses "identifiers" that begin with numbers too, but we
|
||||
// never feed it such input, so it's fine.
|
||||
func firstIdent(x []byte) string {
|
||||
x = bytes.TrimSpace(x)
|
||||
i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
|
||||
if i == -1 {
|
||||
return string(x)
|
||||
}
|
||||
return string(x[:i])
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2017 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.
|
||||
|
||||
// +build go1.7
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Verify that scanIdentifier isn't quadratic.
|
||||
// This doesn't actually measure and fail on its own, but it was previously
|
||||
// very obvious when running by hand.
|
||||
//
|
||||
// TODO: if there's a reliable and non-flaky way to test this, do so.
|
||||
// Maybe count user CPU time instead of wall time? But that's not easy
|
||||
// to do portably in Go.
|
||||
func TestStructField(t *testing.T) {
|
||||
for _, n := range []int{10, 100, 1000, 10000} {
|
||||
n := n
|
||||
t.Run(fmt.Sprint(n), func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "package foo\n\ntype T struct {\n")
|
||||
for i := 0; i < n; i++ {
|
||||
fmt.Fprintf(&buf, "\t// Field%d is foo.\n\tField%d int\n\n", i, i)
|
||||
}
|
||||
fmt.Fprintf(&buf, "}\n")
|
||||
linkifySource(t, buf.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,370 @@
|
|||
// 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPkgLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt", "pkg/fmt"},
|
||||
{"src/fmt", "pkg/fmt"},
|
||||
{"/fmt", "pkg/fmt"},
|
||||
{"fmt", "pkg/fmt"},
|
||||
} {
|
||||
if got := pkgLinkFunc(tc.path); got != tc.want {
|
||||
t.Errorf("pkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcPosLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
line int
|
||||
low int
|
||||
high int
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt/print.go", 42, 30, 50, "/src/fmt/print.go?s=30:50#L32"},
|
||||
{"/src/fmt/print.go", 2, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
|
||||
{"/src/fmt/print.go", 2, 0, 0, "/src/fmt/print.go#L2"},
|
||||
{"/src/fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
|
||||
{"/src/fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
|
||||
{"fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
|
||||
{"fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
|
||||
} {
|
||||
if got := srcPosLinkFunc(tc.src, tc.line, tc.low, tc.high); got != tc.want {
|
||||
t.Errorf("srcLinkFunc(%v, %v, %v, %v) = %v; want %v", tc.src, tc.line, tc.low, tc.high, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt/print.go", "/src/fmt/print.go"},
|
||||
{"src/fmt/print.go", "/src/fmt/print.go"},
|
||||
{"/fmt/print.go", "/src/fmt/print.go"},
|
||||
{"fmt/print.go", "/src/fmt/print.go"},
|
||||
} {
|
||||
if got := srcLinkFunc(tc.src); got != tc.want {
|
||||
t.Errorf("srcLinkFunc(%v) = %v; want %v", tc.src, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
query string
|
||||
line int
|
||||
want string
|
||||
}{
|
||||
{"/src/fmt/print.go", "Sprintf", 33, "/src/fmt/print.go?h=Sprintf#L33"},
|
||||
{"/src/fmt/print.go", "Sprintf", 0, "/src/fmt/print.go?h=Sprintf"},
|
||||
{"src/fmt/print.go", "EOF", 33, "/src/fmt/print.go?h=EOF#L33"},
|
||||
{"src/fmt/print.go", "a%3f+%26b", 1, "/src/fmt/print.go?h=a%3f+%26b#L1"},
|
||||
} {
|
||||
if got := queryLinkFunc(tc.src, tc.query, tc.line); got != tc.want {
|
||||
t.Errorf("queryLinkFunc(%v, %v, %v) = %v; want %v", tc.src, tc.query, tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
ident string
|
||||
want string
|
||||
}{
|
||||
{"fmt", "Sprintf", "/pkg/fmt/#Sprintf"},
|
||||
{"fmt", "EOF", "/pkg/fmt/#EOF"},
|
||||
} {
|
||||
if got := docLinkFunc(tc.src, tc.ident); got != tc.want {
|
||||
t.Errorf("docLinkFunc(%v, %v) = %v; want %v", tc.src, tc.ident, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{},
|
||||
{"foo", "foo"},
|
||||
{"func f()", "func f()"},
|
||||
{"func f(a int,)", "func f(a int)"},
|
||||
{"func f(a int,\n)", "func f(a int)"},
|
||||
{"func f(\n\ta int,\n\tb int,\n\tc int,\n)", "func f(a int, b int, c int)"},
|
||||
{" ( a, b, c ) ", "(a, b, c)"},
|
||||
{"( a, b, c int, foo bar , )", "(a, b, c int, foo bar)"},
|
||||
{"{ a, b}", "{a, b}"},
|
||||
{"[ a, b]", "[a, b]"},
|
||||
} {
|
||||
if got := sanitizeFunc(tc.src); got != tc.want {
|
||||
t.Errorf("sanitizeFunc(%v) = %v; want %v", tc.src, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we add <span id="StructName.FieldName"> elements
|
||||
// to the HTML of struct fields.
|
||||
func TestStructFieldsIDAttributes(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
type T struct {
|
||||
NoDoc string
|
||||
|
||||
// Doc has a comment.
|
||||
Doc string
|
||||
|
||||
// Opt, if non-nil, is an option.
|
||||
Opt *int
|
||||
|
||||
// Опция - другое поле.
|
||||
Опция bool
|
||||
}
|
||||
`))
|
||||
want := `type T struct {
|
||||
<span id="T.NoDoc"></span>NoDoc <a href="/pkg/builtin/#string">string</a>
|
||||
|
||||
<span id="T.Doc"></span><span class="comment">// Doc has a comment.</span>
|
||||
Doc <a href="/pkg/builtin/#string">string</a>
|
||||
|
||||
<span id="T.Opt"></span><span class="comment">// Opt, if non-nil, is an option.</span>
|
||||
Opt *<a href="/pkg/builtin/#int">int</a>
|
||||
|
||||
<span id="T.Опция"></span><span class="comment">// Опция - другое поле.</span>
|
||||
Опция <a href="/pkg/builtin/#bool">bool</a>
|
||||
}`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we add <span id="ConstName"> elements to the HTML
|
||||
// of definitions in const and var specs.
|
||||
func TestValueSpecIDAttributes(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
const (
|
||||
NoDoc string = "NoDoc"
|
||||
|
||||
// Doc has a comment
|
||||
Doc = "Doc"
|
||||
|
||||
NoVal
|
||||
)`))
|
||||
want := `const (
|
||||
<span id="NoDoc">NoDoc</span> <a href="/pkg/builtin/#string">string</a> = "NoDoc"
|
||||
|
||||
<span class="comment">// Doc has a comment</span>
|
||||
<span id="Doc">Doc</span> = "Doc"
|
||||
|
||||
<span id="NoVal">NoVal</span>
|
||||
)`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompositeLitLinkFields(t *testing.T) {
|
||||
got := linkifySource(t, []byte(`
|
||||
package foo
|
||||
|
||||
type T struct {
|
||||
X int
|
||||
}
|
||||
|
||||
var S T = T{X: 12}`))
|
||||
want := `type T struct {
|
||||
<span id="T.X"></span>X <a href="/pkg/builtin/#int">int</a>
|
||||
}
|
||||
var <span id="S">S</span> <a href="#T">T</a> = <a href="#T">T</a>{<a href="#T.X">X</a>: 12}`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncDeclNotLink(t *testing.T) {
|
||||
// Function.
|
||||
got := linkifySource(t, []byte(`
|
||||
package http
|
||||
|
||||
func Get(url string) (resp *Response, err error)`))
|
||||
want := `func Get(url <a href="/pkg/builtin/#string">string</a>) (resp *<a href="#Response">Response</a>, err <a href="/pkg/builtin/#error">error</a>)`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
|
||||
// Method.
|
||||
got = linkifySource(t, []byte(`
|
||||
package http
|
||||
|
||||
func (h Header) Get(key string) string`))
|
||||
want = `func (h <a href="#Header">Header</a>) Get(key <a href="/pkg/builtin/#string">string</a>) <a href="/pkg/builtin/#string">string</a>`
|
||||
if got != want {
|
||||
t.Errorf("got: %s\n\nwant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func linkifySource(t *testing.T, src []byte) string {
|
||||
p := &Presentation{
|
||||
DeclLinks: true,
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
pi := &PageInfo{
|
||||
FSet: fset,
|
||||
}
|
||||
sep := ""
|
||||
for _, decl := range af.Decls {
|
||||
buf.WriteString(sep)
|
||||
sep = "\n"
|
||||
buf.WriteString(p.node_htmlFunc(pi, decl, true))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestScanIdentifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"foo bar", "foo"},
|
||||
{"foo/bar", "foo"},
|
||||
{" foo", ""},
|
||||
{"фоо", "фоо"},
|
||||
{"f123", "f123"},
|
||||
{"123f", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := scanIdentifier([]byte(tt.in))
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("scanIdentifier(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceLeadingIndentation(t *testing.T) {
|
||||
oldIndent := strings.Repeat(" ", 2)
|
||||
newIndent := strings.Repeat(" ", 4)
|
||||
tests := []struct {
|
||||
src, want string
|
||||
}{
|
||||
{" foo\n bar\n baz", " foo\n bar\n baz"},
|
||||
{" '`'\n '`'\n", " '`'\n '`'\n"},
|
||||
{" '\\''\n '`'\n", " '\\''\n '`'\n"},
|
||||
{" \"`\"\n \"`\"\n", " \"`\"\n \"`\"\n"},
|
||||
{" `foo\n bar`", " `foo\n bar`"},
|
||||
{" `foo\\`\n bar", " `foo\\`\n bar"},
|
||||
{" '\\`'`foo\n bar", " '\\`'`foo\n bar"},
|
||||
{
|
||||
" if true {\n foo := `One\n \tTwo\nThree`\n }\n",
|
||||
" if true {\n foo := `One\n \tTwo\n Three`\n }\n",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := replaceLeadingIndentation(tc.src, oldIndent, newIndent); got != tc.want {
|
||||
t.Errorf("replaceLeadingIndentation:\n%v\n---\nhave:\n%v\n---\nwant:\n%v\n",
|
||||
tc.src, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcBreadcrumbFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"src/", `<span class="text-muted">src/</span>`},
|
||||
{"src/fmt/", `<a href="/src">src</a>/<span class="text-muted">fmt/</span>`},
|
||||
{"src/fmt/print.go", `<a href="/src">src</a>/<a href="/src/fmt">fmt</a>/<span class="text-muted">print.go</span>`},
|
||||
} {
|
||||
if got := srcBreadcrumbFunc(tc.path); got != tc.want {
|
||||
t.Errorf("srcBreadcrumbFunc(%v) = %v; want %v", tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcToPkgLinkFunc(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"src/", `<a href="/pkg">Index</a>`},
|
||||
{"src/fmt/", `<a href="/pkg/fmt">fmt</a>`},
|
||||
{"pkg/", `<a href="/pkg">Index</a>`},
|
||||
{"pkg/LICENSE", `<a href="/pkg">Index</a>`},
|
||||
} {
|
||||
if got := srcToPkgLinkFunc(tc.path); got != tc.want {
|
||||
t.Errorf("srcToPkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterOutBuildAnnotations(t *testing.T) {
|
||||
// TODO: simplify this by using a multiline string once we stop
|
||||
// using go vet from 1.10 on the build dashboard.
|
||||
// https://golang.org/issue/26627
|
||||
src := []byte("// +build !foo\n" +
|
||||
"// +build !anothertag\n" +
|
||||
"\n" +
|
||||
"// non-tag comment\n" +
|
||||
"\n" +
|
||||
"package foo\n" +
|
||||
"\n" +
|
||||
"func bar() int {\n" +
|
||||
" return 42\n" +
|
||||
"}\n")
|
||||
|
||||
fset := token.NewFileSet()
|
||||
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, cg := range af.Comments {
|
||||
if strings.HasPrefix(cg.Text(), "+build ") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("TestFilterOutBuildAnnotations is broken: missing build tag in test input")
|
||||
}
|
||||
|
||||
found = false
|
||||
for _, cg := range filterOutBuildAnnotations(af.Comments) {
|
||||
if strings.HasPrefix(cg.Text(), "+build ") {
|
||||
t.Errorf("filterOutBuildAnnotations failed to filter build tag")
|
||||
}
|
||||
|
||||
if strings.Contains(cg.Text(), "non-tag comment") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("filterOutBuildAnnotations should not remove non-build tag comment")
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,323 @@
|
|||
// 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
func newCorpus(t *testing.T) *Corpus {
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"src/foo/foo.go": `// Package foo is an example.
|
||||
package foo
|
||||
|
||||
import "bar"
|
||||
|
||||
const Pi = 3.1415
|
||||
|
||||
var Foos []Foo
|
||||
|
||||
// Foo is stuff.
|
||||
type Foo struct{}
|
||||
|
||||
func New() *Foo {
|
||||
return new(Foo)
|
||||
}
|
||||
`,
|
||||
"src/bar/bar.go": `// Package bar is another example to test races.
|
||||
package bar
|
||||
`,
|
||||
"src/other/bar/bar.go": `// Package bar is another bar package.
|
||||
package bar
|
||||
func X() {}
|
||||
`,
|
||||
"src/skip/skip.go": `// Package skip should be skipped.
|
||||
package skip
|
||||
func Skip() {}
|
||||
`,
|
||||
"src/bar/readme.txt": `Whitelisted text file.
|
||||
`,
|
||||
"src/bar/baz.zzz": `Text file not whitelisted.
|
||||
`,
|
||||
}))
|
||||
c.IndexEnabled = true
|
||||
c.IndexDirectory = func(dir string) bool {
|
||||
return !strings.Contains(dir, "skip")
|
||||
}
|
||||
|
||||
if err := c.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
for _, docs := range []bool{true, false} {
|
||||
for _, goCode := range []bool{true, false} {
|
||||
for _, fullText := range []bool{true, false} {
|
||||
c := newCorpus(t)
|
||||
c.IndexDocs = docs
|
||||
c.IndexGoCode = goCode
|
||||
c.IndexFullText = fullText
|
||||
c.UpdateIndex()
|
||||
ix, _ := c.CurrentIndex()
|
||||
if ix == nil {
|
||||
t.Fatal("no index")
|
||||
}
|
||||
t.Logf("docs, goCode, fullText = %v,%v,%v", docs, goCode, fullText)
|
||||
testIndex(t, c, ix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexWriteRead(t *testing.T) {
|
||||
type key struct {
|
||||
docs, goCode, fullText bool
|
||||
}
|
||||
type val struct {
|
||||
buf *bytes.Buffer
|
||||
c *Corpus
|
||||
}
|
||||
m := map[key]val{}
|
||||
|
||||
for _, docs := range []bool{true, false} {
|
||||
for _, goCode := range []bool{true, false} {
|
||||
for _, fullText := range []bool{true, false} {
|
||||
k := key{docs, goCode, fullText}
|
||||
c := newCorpus(t)
|
||||
c.IndexDocs = docs
|
||||
c.IndexGoCode = goCode
|
||||
c.IndexFullText = fullText
|
||||
c.UpdateIndex()
|
||||
ix, _ := c.CurrentIndex()
|
||||
if ix == nil {
|
||||
t.Fatal("no index")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
nw, err := ix.WriteTo(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Index.WriteTo: %v", err)
|
||||
}
|
||||
m[k] = val{bytes.NewBuffer(buf.Bytes()), c}
|
||||
ix2 := new(Index)
|
||||
nr, err := ix2.ReadFrom(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Index.ReadFrom: %v", err)
|
||||
}
|
||||
if nr != nw {
|
||||
t.Errorf("Wrote %d bytes to index but read %d", nw, nr)
|
||||
}
|
||||
testIndex(t, c, ix)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Test CompatibleWith
|
||||
for k1, v1 := range m {
|
||||
ix := new(Index)
|
||||
if _, err := ix.ReadFrom(v1.buf); err != nil {
|
||||
t.Fatalf("Index.ReadFrom: %v", err)
|
||||
}
|
||||
for k2, v2 := range m {
|
||||
if got, want := ix.CompatibleWith(v2.c), k1 == k2; got != want {
|
||||
t.Errorf("CompatibleWith = %v; want %v for %v, %v", got, want, k1, k2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testIndex(t *testing.T, c *Corpus, ix *Index) {
|
||||
if _, ok := ix.words["Skip"]; ok {
|
||||
t.Errorf("the word Skip was found; expected it to be skipped")
|
||||
}
|
||||
checkStats(t, c, ix)
|
||||
checkImportCount(t, c, ix)
|
||||
checkPackagePath(t, c, ix)
|
||||
checkExports(t, c, ix)
|
||||
checkIdents(t, c, ix)
|
||||
}
|
||||
|
||||
// checkStats checks the Index's statistics.
|
||||
// Some statistics are only set when we're indexing Go code.
|
||||
func checkStats(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := Statistics{}
|
||||
if c.IndexFullText {
|
||||
want.Bytes = 314
|
||||
want.Files = 4
|
||||
want.Lines = 21
|
||||
} else if c.IndexDocs || c.IndexGoCode {
|
||||
want.Bytes = 291
|
||||
want.Files = 3
|
||||
want.Lines = 20
|
||||
}
|
||||
if c.IndexGoCode {
|
||||
want.Words = 8
|
||||
want.Spots = 12
|
||||
}
|
||||
if got := ix.Stats(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Stats = %#v; want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkImportCount checks the Index's import count map.
|
||||
// It is only set when we're indexing Go code.
|
||||
func checkImportCount(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[string]int{}
|
||||
if c.IndexGoCode {
|
||||
want = map[string]int{
|
||||
"bar": 1,
|
||||
}
|
||||
}
|
||||
if got := ix.ImportCount(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ImportCount = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPackagePath checks the Index's package path map.
|
||||
// It is set if at least one of the indexing options is enabled.
|
||||
func checkPackagePath(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[string]map[string]bool{}
|
||||
if c.IndexDocs || c.IndexGoCode || c.IndexFullText {
|
||||
want = map[string]map[string]bool{
|
||||
"foo": {
|
||||
"foo": true,
|
||||
},
|
||||
"bar": {
|
||||
"bar": true,
|
||||
"other/bar": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
if got := ix.PackagePath(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("PackagePath = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkExports checks the Index's exports map.
|
||||
// It is only set when we're indexing Go code.
|
||||
func checkExports(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[string]map[string]SpotKind{}
|
||||
if c.IndexGoCode {
|
||||
want = map[string]map[string]SpotKind{
|
||||
"foo": {
|
||||
"Pi": ConstDecl,
|
||||
"Foos": VarDecl,
|
||||
"Foo": TypeDecl,
|
||||
"New": FuncDecl,
|
||||
},
|
||||
"other/bar": {
|
||||
"X": FuncDecl,
|
||||
},
|
||||
}
|
||||
}
|
||||
if got := ix.Exports(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Exports = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkIdents checks the Index's indents map.
|
||||
// It is only set when we're indexing documentation.
|
||||
func checkIdents(t *testing.T, c *Corpus, ix *Index) {
|
||||
want := map[SpotKind]map[string][]Ident{}
|
||||
if c.IndexDocs {
|
||||
want = map[SpotKind]map[string][]Ident{
|
||||
PackageClause: {
|
||||
"bar": {
|
||||
{"bar", "bar", "bar", "Package bar is another example to test races."},
|
||||
{"other/bar", "bar", "bar", "Package bar is another bar package."},
|
||||
},
|
||||
"foo": {{"foo", "foo", "foo", "Package foo is an example."}},
|
||||
"other": {{"other/bar", "bar", "bar", "Package bar is another bar package."}},
|
||||
},
|
||||
ConstDecl: {
|
||||
"Pi": {{"foo", "foo", "Pi", ""}},
|
||||
},
|
||||
VarDecl: {
|
||||
"Foos": {{"foo", "foo", "Foos", ""}},
|
||||
},
|
||||
TypeDecl: {
|
||||
"Foo": {{"foo", "foo", "Foo", "Foo is stuff."}},
|
||||
},
|
||||
FuncDecl: {
|
||||
"New": {{"foo", "foo", "New", ""}},
|
||||
"X": {{"other/bar", "bar", "X", ""}},
|
||||
},
|
||||
}
|
||||
}
|
||||
if got := ix.Idents(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Idents = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentResultSort(t *testing.T) {
|
||||
ic := map[string]int{
|
||||
"/a/b/pkg1": 10,
|
||||
"/a/b/pkg2": 2,
|
||||
"/b/d/pkg3": 20,
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
ir []Ident
|
||||
exp []Ident
|
||||
}{
|
||||
{
|
||||
ir: []Ident{
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
exp: []Ident{
|
||||
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
ir: []Ident{
|
||||
{"/a/a/pkg1", "pkg1", "MyFunc1", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
exp: []Ident{
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
{"/a/a/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
},
|
||||
} {
|
||||
if sort.Sort(byImportCount{tc.ir, ic}); !reflect.DeepEqual(tc.ir, tc.exp) {
|
||||
t.Errorf("got: %v, want %v", tc.ir, tc.exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentFilter(t *testing.T) {
|
||||
ic := map[string]int{}
|
||||
for _, tc := range []struct {
|
||||
ir []Ident
|
||||
pak string
|
||||
exp []Ident
|
||||
}{
|
||||
{
|
||||
ir: []Ident{
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
|
||||
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
|
||||
},
|
||||
pak: "pkg2",
|
||||
exp: []Ident{
|
||||
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
|
||||
},
|
||||
},
|
||||
} {
|
||||
res := byImportCount{tc.ir, ic}.filter(tc.pak)
|
||||
if !reflect.DeepEqual(res, tc.exp) {
|
||||
t.Errorf("got: %v, want %v", res, tc.exp)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
// 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.
|
||||
|
||||
// This file implements LinkifyText which introduces
|
||||
// links for identifiers pointing to their declarations.
|
||||
// The approach does not cover all cases because godoc
|
||||
// doesn't have complete type information, but it's
|
||||
// reasonably good for browsing.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/token"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// LinkifyText HTML-escapes source text and writes it to w.
|
||||
// Identifiers that are in a "use" position (i.e., that are
|
||||
// not being declared), are wrapped with HTML links pointing
|
||||
// to the respective declaration, if possible. Comments are
|
||||
// formatted the same way as with FormatText.
|
||||
//
|
||||
func LinkifyText(w io.Writer, text []byte, n ast.Node) {
|
||||
links := linksFor(n)
|
||||
|
||||
i := 0 // links index
|
||||
prev := "" // prev HTML tag
|
||||
linkWriter := func(w io.Writer, _ int, start bool) {
|
||||
// end tag
|
||||
if !start {
|
||||
if prev != "" {
|
||||
fmt.Fprintf(w, `</%s>`, prev)
|
||||
prev = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// start tag
|
||||
prev = ""
|
||||
if i < len(links) {
|
||||
switch info := links[i]; {
|
||||
case info.path != "" && info.name == "":
|
||||
// package path
|
||||
fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path)
|
||||
prev = "a"
|
||||
case info.path != "" && info.name != "":
|
||||
// qualified identifier
|
||||
fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.name)
|
||||
prev = "a"
|
||||
case info.path == "" && info.name != "":
|
||||
// local identifier
|
||||
if info.isVal {
|
||||
fmt.Fprintf(w, `<span id="%s">`, info.name)
|
||||
prev = "span"
|
||||
} else if ast.IsExported(info.name) {
|
||||
fmt.Fprintf(w, `<a href="#%s">`, info.name)
|
||||
prev = "a"
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
idents := tokenSelection(text, token.IDENT)
|
||||
comments := tokenSelection(text, token.COMMENT)
|
||||
FormatSelections(w, text, linkWriter, idents, selectionTag, comments)
|
||||
}
|
||||
|
||||
// A link describes the (HTML) link information for an identifier.
|
||||
// The zero value of a link represents "no link".
|
||||
//
|
||||
type link struct {
|
||||
path, name string // package path, identifier name
|
||||
isVal bool // identifier is defined in a const or var declaration
|
||||
}
|
||||
|
||||
// linksFor returns the list of links for the identifiers used
|
||||
// by node in the same order as they appear in the source.
|
||||
//
|
||||
func linksFor(node ast.Node) (links []link) {
|
||||
// linkMap tracks link information for each ast.Ident node. Entries may
|
||||
// be created out of source order (for example, when we visit a parent
|
||||
// definition node). These links are appended to the returned slice when
|
||||
// their ast.Ident nodes are visited.
|
||||
linkMap := make(map[*ast.Ident]link)
|
||||
|
||||
ast.Inspect(node, func(node ast.Node) bool {
|
||||
switch n := node.(type) {
|
||||
case *ast.Field:
|
||||
for _, n := range n.Names {
|
||||
linkMap[n] = link{}
|
||||
}
|
||||
case *ast.ImportSpec:
|
||||
if name := n.Name; name != nil {
|
||||
linkMap[name] = link{}
|
||||
}
|
||||
case *ast.ValueSpec:
|
||||
for _, n := range n.Names {
|
||||
linkMap[n] = link{name: n.Name, isVal: true}
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
linkMap[n.Name] = link{}
|
||||
case *ast.TypeSpec:
|
||||
linkMap[n.Name] = link{}
|
||||
case *ast.AssignStmt:
|
||||
// Short variable declarations only show up if we apply
|
||||
// this code to all source code (as opposed to exported
|
||||
// declarations only).
|
||||
if n.Tok == token.DEFINE {
|
||||
// Some of the lhs variables may be re-declared,
|
||||
// so technically they are not defs. We don't
|
||||
// care for now.
|
||||
for _, x := range n.Lhs {
|
||||
// Each lhs expression should be an
|
||||
// ident, but we are conservative and check.
|
||||
if n, _ := x.(*ast.Ident); n != nil {
|
||||
linkMap[n] = link{isVal: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.SelectorExpr:
|
||||
// Detect qualified identifiers of the form pkg.ident.
|
||||
// If anything fails we return true and collect individual
|
||||
// identifiers instead.
|
||||
if x, _ := n.X.(*ast.Ident); x != nil {
|
||||
// Create links only if x is a qualified identifier.
|
||||
if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg {
|
||||
if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil {
|
||||
// spec.Path.Value is the import path
|
||||
if path, err := strconv.Unquote(spec.Path.Value); err == nil {
|
||||
// Register two links, one for the package
|
||||
// and one for the qualified identifier.
|
||||
linkMap[x] = link{path: path}
|
||||
linkMap[n.Sel] = link{path: path, name: n.Sel.Name}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.CompositeLit:
|
||||
// Detect field names within composite literals. These links should
|
||||
// be prefixed by the type name.
|
||||
fieldPath := ""
|
||||
prefix := ""
|
||||
switch typ := n.Type.(type) {
|
||||
case *ast.Ident:
|
||||
prefix = typ.Name + "."
|
||||
case *ast.SelectorExpr:
|
||||
if x, _ := typ.X.(*ast.Ident); x != nil {
|
||||
// Create links only if x is a qualified identifier.
|
||||
if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg {
|
||||
if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil {
|
||||
// spec.Path.Value is the import path
|
||||
if path, err := strconv.Unquote(spec.Path.Value); err == nil {
|
||||
// Register two links, one for the package
|
||||
// and one for the qualified identifier.
|
||||
linkMap[x] = link{path: path}
|
||||
linkMap[typ.Sel] = link{path: path, name: typ.Sel.Name}
|
||||
fieldPath = path
|
||||
prefix = typ.Sel.Name + "."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range n.Elts {
|
||||
if kv, ok := e.(*ast.KeyValueExpr); ok {
|
||||
if k, ok := kv.Key.(*ast.Ident); ok {
|
||||
// Note: there is some syntactic ambiguity here. We cannot determine
|
||||
// if this is a struct literal or a map literal without type
|
||||
// information. We assume struct literal.
|
||||
name := prefix + k.Name
|
||||
linkMap[k] = link{path: fieldPath, name: name}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.Ident:
|
||||
if l, ok := linkMap[n]; ok {
|
||||
links = append(links, l)
|
||||
} else {
|
||||
l := link{name: n.Name}
|
||||
if n.Obj == nil && doc.IsPredeclared(n.Name) {
|
||||
l.path = builtinPkgPath
|
||||
}
|
||||
links = append(links, l)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
var (
|
||||
doctype = []byte("<!DOCTYPE ")
|
||||
jsonStart = []byte("<!--{")
|
||||
jsonEnd = []byte("}-->")
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Documentation Metadata
|
||||
|
||||
// TODO(adg): why are some exported and some aren't? -brad
|
||||
type Metadata struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
Template bool // execute as template
|
||||
Path string // canonical path for this page
|
||||
filePath string // filesystem path relative to goroot
|
||||
}
|
||||
|
||||
func (m *Metadata) FilePath() string { return m.filePath }
|
||||
|
||||
// extractMetadata extracts the Metadata from a byte slice.
|
||||
// It returns the Metadata value and the remaining data.
|
||||
// If no metadata is present the original byte slice is returned.
|
||||
//
|
||||
func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
|
||||
tail = b
|
||||
if !bytes.HasPrefix(b, jsonStart) {
|
||||
return
|
||||
}
|
||||
end := bytes.Index(b, jsonEnd)
|
||||
if end < 0 {
|
||||
return
|
||||
}
|
||||
b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
|
||||
if err = json.Unmarshal(b, &meta); err != nil {
|
||||
return
|
||||
}
|
||||
tail = tail[end+len(jsonEnd):]
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata,
|
||||
// and updates the DocMetadata map.
|
||||
func (c *Corpus) updateMetadata() {
|
||||
metadata := make(map[string]*Metadata)
|
||||
var scan func(string) // scan is recursive
|
||||
scan = func(dir string) {
|
||||
fis, err := c.fs.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Println("updateMetadata:", err)
|
||||
return
|
||||
}
|
||||
for _, fi := range fis {
|
||||
name := pathpkg.Join(dir, fi.Name())
|
||||
if fi.IsDir() {
|
||||
scan(name) // recurse
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(name, ".html") {
|
||||
continue
|
||||
}
|
||||
// Extract metadata from the file.
|
||||
b, err := vfs.ReadFile(c.fs, name)
|
||||
if err != nil {
|
||||
log.Printf("updateMetadata %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
meta, _, err := extractMetadata(b)
|
||||
if err != nil {
|
||||
log.Printf("updateMetadata: %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
// Store relative filesystem path in Metadata.
|
||||
meta.filePath = name
|
||||
if meta.Path == "" {
|
||||
// If no Path, canonical path is actual path.
|
||||
meta.Path = meta.filePath
|
||||
}
|
||||
// Store under both paths.
|
||||
metadata[meta.Path] = &meta
|
||||
metadata[meta.filePath] = &meta
|
||||
}
|
||||
}
|
||||
scan("/doc")
|
||||
c.docMetadata.Set(metadata)
|
||||
}
|
||||
|
||||
// MetadataFor returns the *Metadata for a given relative path or nil if none
|
||||
// exists.
|
||||
//
|
||||
func (c *Corpus) MetadataFor(relpath string) *Metadata {
|
||||
if m, _ := c.docMetadata.Get(); m != nil {
|
||||
meta := m.(map[string]*Metadata)
|
||||
// If metadata for this relpath exists, return it.
|
||||
if p := meta[relpath]; p != nil {
|
||||
return p
|
||||
}
|
||||
// Try with or without trailing slash.
|
||||
if strings.HasSuffix(relpath, "/") {
|
||||
relpath = relpath[:len(relpath)-1]
|
||||
} else {
|
||||
relpath = relpath + "/"
|
||||
}
|
||||
return meta[relpath]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshMetadata sends a signal to update DocMetadata. If a refresh is in
|
||||
// progress the metadata will be refreshed again afterward.
|
||||
//
|
||||
func (c *Corpus) refreshMetadata() {
|
||||
select {
|
||||
case c.refreshMetadataSignal <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshMetadataLoop runs forever, updating DocMetadata when the underlying
|
||||
// file system changes. It should be launched in a goroutine.
|
||||
func (c *Corpus) refreshMetadataLoop() {
|
||||
for {
|
||||
<-c.refreshMetadataSignal
|
||||
c.updateMetadata()
|
||||
time.Sleep(10 * time.Second) // at most once every 10 seconds
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/env"
|
||||
)
|
||||
|
||||
// Page describes the contents of the top-level godoc webpage.
|
||||
type Page struct {
|
||||
Title string
|
||||
Tabtitle string
|
||||
Subtitle string
|
||||
SrcPath string
|
||||
Query string
|
||||
Body []byte
|
||||
GoogleCN bool // page is being served from golang.google.cn
|
||||
TreeView bool // page needs to contain treeview related js and css
|
||||
|
||||
// filled in by ServePage
|
||||
SearchBox bool
|
||||
Playground bool
|
||||
Version string
|
||||
GoogleAnalytics string
|
||||
}
|
||||
|
||||
func (p *Presentation) ServePage(w http.ResponseWriter, page Page) {
|
||||
if page.Tabtitle == "" {
|
||||
page.Tabtitle = page.Title
|
||||
}
|
||||
page.SearchBox = p.Corpus.IndexEnabled
|
||||
page.Playground = p.ShowPlayground
|
||||
page.Version = runtime.Version()
|
||||
page.GoogleAnalytics = p.GoogleAnalytics
|
||||
applyTemplateToResponseWriter(w, p.GodocHTML, page)
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, relpath string, err error) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
if perr, ok := err.(*os.PathError); ok {
|
||||
rel, err := filepath.Rel(runtime.GOROOT(), perr.Path)
|
||||
if err != nil {
|
||||
perr.Path = "REDACTED"
|
||||
} else {
|
||||
perr.Path = filepath.Join("$GOROOT", rel)
|
||||
}
|
||||
}
|
||||
p.ServePage(w, Page{
|
||||
Title: "File " + relpath,
|
||||
Subtitle: relpath,
|
||||
Body: applyTemplate(p.ErrorHTML, "errorHTML", err),
|
||||
GoogleCN: googleCN(r),
|
||||
GoogleAnalytics: p.GoogleAnalytics,
|
||||
})
|
||||
}
|
||||
|
||||
func googleCN(r *http.Request) bool {
|
||||
if r.FormValue("googlecn") != "" {
|
||||
return true
|
||||
}
|
||||
if !env.IsProd() {
|
||||
return false
|
||||
}
|
||||
if strings.HasSuffix(r.Host, ".cn") {
|
||||
return true
|
||||
}
|
||||
switch r.Header.Get("X-AppEngine-Country") {
|
||||
case "", "ZZ", "CN":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// This file contains support functions for parsing .go files
|
||||
// accessed via godoc's file system fs.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
pathpkg "path"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
var linePrefix = []byte("//line ")
|
||||
|
||||
// This function replaces source lines starting with "//line " with a blank line.
|
||||
// It does this irrespective of whether the line is truly a line comment or not;
|
||||
// e.g., the line may be inside a string, or a /*-style comment; however that is
|
||||
// rather unlikely (proper testing would require a full Go scan which we want to
|
||||
// avoid for performance).
|
||||
func replaceLinePrefixCommentsWithBlankLine(src []byte) {
|
||||
for {
|
||||
i := bytes.Index(src, linePrefix)
|
||||
if i < 0 {
|
||||
break // we're done
|
||||
}
|
||||
// 0 <= i && i+len(linePrefix) <= len(src)
|
||||
if i == 0 || src[i-1] == '\n' {
|
||||
// at beginning of line: blank out line
|
||||
for i < len(src) && src[i] != '\n' {
|
||||
src[i] = ' '
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
// not at beginning of line: skip over prefix
|
||||
i += len(linePrefix)
|
||||
}
|
||||
// i <= len(src)
|
||||
src = src[i:]
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Corpus) parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
|
||||
src, err := vfs.ReadFile(c.fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Temporary ad-hoc fix for issue 5247.
|
||||
// TODO(gri) Remove this in favor of a better fix, eventually (see issue 7702).
|
||||
replaceLinePrefixCommentsWithBlankLine(src)
|
||||
|
||||
return parser.ParseFile(fset, filename, src, mode)
|
||||
}
|
||||
|
||||
func (c *Corpus) parseFiles(fset *token.FileSet, relpath string, abspath string, localnames []string) (map[string]*ast.File, error) {
|
||||
files := make(map[string]*ast.File)
|
||||
for _, f := range localnames {
|
||||
absname := pathpkg.Join(abspath, f)
|
||||
file, err := c.parseFile(fset, absname, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files[pathpkg.Join(relpath, f)] = file
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
// 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/httpfs"
|
||||
)
|
||||
|
||||
// SearchResultFunc functions return an HTML body for displaying search results.
|
||||
type SearchResultFunc func(p *Presentation, result SearchResult) []byte
|
||||
|
||||
// Presentation generates output from a corpus.
|
||||
type Presentation struct {
|
||||
Corpus *Corpus
|
||||
|
||||
mux *http.ServeMux
|
||||
fileServer http.Handler
|
||||
cmdHandler handlerServer
|
||||
pkgHandler handlerServer
|
||||
|
||||
CallGraphHTML,
|
||||
DirlistHTML,
|
||||
ErrorHTML,
|
||||
ExampleHTML,
|
||||
GodocHTML,
|
||||
ImplementsHTML,
|
||||
MethodSetHTML,
|
||||
PackageHTML,
|
||||
PackageRootHTML,
|
||||
SearchHTML,
|
||||
SearchDocHTML,
|
||||
SearchCodeHTML,
|
||||
SearchTxtHTML,
|
||||
SearchDescXML *template.Template
|
||||
|
||||
// TabWidth optionally specifies the tab width.
|
||||
TabWidth int
|
||||
|
||||
ShowTimestamps bool
|
||||
ShowPlayground bool
|
||||
DeclLinks bool
|
||||
|
||||
// SrcMode outputs source code instead of documentation in command-line mode.
|
||||
SrcMode bool
|
||||
// HTMLMode outputs HTML instead of plain text in command-line mode.
|
||||
HTMLMode bool
|
||||
// AllMode includes unexported identifiers in the output in command-line mode.
|
||||
AllMode bool
|
||||
|
||||
// NotesRx optionally specifies a regexp to match
|
||||
// notes to render in the output.
|
||||
NotesRx *regexp.Regexp
|
||||
|
||||
// AdjustPageInfoMode optionally specifies a function to
|
||||
// modify the PageInfoMode of a request. The default chosen
|
||||
// value is provided.
|
||||
AdjustPageInfoMode func(req *http.Request, mode PageInfoMode) PageInfoMode
|
||||
|
||||
// URLForSrc optionally specifies a function that takes a source file and
|
||||
// returns a URL for it.
|
||||
// The source file argument has the form /src/<path>/<filename>.
|
||||
URLForSrc func(src string) string
|
||||
|
||||
// URLForSrcPos optionally specifies a function to create a URL given a
|
||||
// source file, a line from the source file (1-based), and low & high offset
|
||||
// positions (0-based, bytes from beginning of file). Ideally, the returned
|
||||
// URL will be for the specified line of the file, while the high & low
|
||||
// positions will be used to highlight a section of the file.
|
||||
// The source file argument has the form /src/<path>/<filename>.
|
||||
URLForSrcPos func(src string, line, low, high int) string
|
||||
|
||||
// URLForSrcQuery optionally specifies a function to create a URL given a
|
||||
// source file, a query string, and a line from the source file (1-based).
|
||||
// The source file argument has the form /src/<path>/<filename>.
|
||||
// The query argument will be escaped for the purposes of embedding in a URL
|
||||
// query parameter.
|
||||
// Ideally, the returned URL will be for the specified line of the file with
|
||||
// the query string highlighted.
|
||||
URLForSrcQuery func(src, query string, line int) string
|
||||
|
||||
// SearchResults optionally specifies a list of functions returning an HTML
|
||||
// body for displaying search results.
|
||||
SearchResults []SearchResultFunc
|
||||
|
||||
// GoogleAnalytics optionally adds Google Analytics via the provided
|
||||
// tracking ID to each page.
|
||||
GoogleAnalytics string
|
||||
|
||||
initFuncMapOnce sync.Once
|
||||
funcMap template.FuncMap
|
||||
templateFuncs template.FuncMap
|
||||
}
|
||||
|
||||
// NewPresentation returns a new Presentation from a corpus.
|
||||
// It sets SearchResults to:
|
||||
// [SearchResultDoc SearchResultCode SearchResultTxt].
|
||||
func NewPresentation(c *Corpus) *Presentation {
|
||||
if c == nil {
|
||||
panic("nil Corpus")
|
||||
}
|
||||
p := &Presentation{
|
||||
Corpus: c,
|
||||
mux: http.NewServeMux(),
|
||||
fileServer: http.FileServer(httpfs.New(c.fs)),
|
||||
|
||||
TabWidth: 4,
|
||||
DeclLinks: true,
|
||||
SearchResults: []SearchResultFunc{
|
||||
(*Presentation).SearchResultDoc,
|
||||
(*Presentation).SearchResultCode,
|
||||
(*Presentation).SearchResultTxt,
|
||||
},
|
||||
}
|
||||
p.cmdHandler = handlerServer{
|
||||
p: p,
|
||||
c: c,
|
||||
pattern: "/cmd/",
|
||||
fsRoot: "/src",
|
||||
}
|
||||
p.pkgHandler = handlerServer{
|
||||
p: p,
|
||||
c: c,
|
||||
pattern: "/pkg/",
|
||||
stripPrefix: "pkg/",
|
||||
fsRoot: "/src",
|
||||
exclude: []string{"/src/cmd"},
|
||||
}
|
||||
p.cmdHandler.registerWithMux(p.mux)
|
||||
p.pkgHandler.registerWithMux(p.mux)
|
||||
p.mux.HandleFunc("/", p.ServeFile)
|
||||
p.mux.HandleFunc("/search", p.HandleSearch)
|
||||
p.mux.HandleFunc("/opensearch.xml", p.serveSearchDesc)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Presentation) FileServer() http.Handler {
|
||||
return p.fileServer
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) PkgFSRoot() string {
|
||||
return p.pkgHandler.fsRoot
|
||||
}
|
||||
|
||||
func (p *Presentation) CmdFSRoot() string {
|
||||
return p.cmdHandler.fsRoot
|
||||
}
|
||||
|
||||
// TODO(bradfitz): move this to be a method on Corpus. Just moving code around for now,
|
||||
// but this doesn't feel right.
|
||||
func (p *Presentation) GetPkgPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
|
||||
return p.pkgHandler.GetPageInfo(abspath, relpath, mode, "", "")
|
||||
}
|
||||
|
||||
// TODO(bradfitz): move this to be a method on Corpus. Just moving code around for now,
|
||||
// but this doesn't feel right.
|
||||
func (p *Presentation) GetCmdPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
|
||||
return p.cmdHandler.GetPageInfo(abspath, relpath, mode, "", "")
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
// Package proxy proxies requests to the playground's compile and share handlers.
|
||||
// It is designed to run only on the instance of godoc that serves golang.org.
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/env"
|
||||
)
|
||||
|
||||
const playgroundURL = "https://play.golang.org"
|
||||
|
||||
type Request struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Errors string
|
||||
Events []Event
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Message string
|
||||
Kind string // "stdout" or "stderr"
|
||||
Delay time.Duration // time to wait before printing Message
|
||||
}
|
||||
|
||||
const expires = 7 * 24 * time.Hour // 1 week
|
||||
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
|
||||
|
||||
func RegisterHandlers(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/compile", compile)
|
||||
mux.HandleFunc("/share", share)
|
||||
}
|
||||
|
||||
func compile(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "I only answer to POST requests.", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
body := r.FormValue("body")
|
||||
res := &Response{}
|
||||
req := &Request{Body: body}
|
||||
if err := makeCompileRequest(ctx, req, res); err != nil {
|
||||
log.Printf("ERROR compile error: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var out interface{}
|
||||
switch r.FormValue("version") {
|
||||
case "2":
|
||||
out = res
|
||||
default: // "1"
|
||||
out = struct {
|
||||
CompileErrors string `json:"compile_errors"`
|
||||
Output string `json:"output"`
|
||||
}{res.Errors, flatten(res.Events)}
|
||||
}
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
log.Printf("ERROR encoding response: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
expiresTime := time.Now().Add(expires).UTC()
|
||||
w.Header().Set("Expires", expiresTime.Format(time.RFC1123))
|
||||
w.Header().Set("Cache-Control", cacheControlHeader)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// makePlaygroundRequest sends the given Request to the playground compile
|
||||
// endpoint and stores the response in the given Response.
|
||||
func makeCompileRequest(ctx context.Context, req *Request, res *Response) error {
|
||||
reqJ, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling request: %v", err)
|
||||
}
|
||||
hReq, _ := http.NewRequest("POST", playgroundURL+"/compile", bytes.NewReader(reqJ))
|
||||
hReq.Header.Set("Content-Type", "application/json")
|
||||
hReq = hReq.WithContext(ctx)
|
||||
|
||||
r, err := http.DefaultClient.Do(hReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
b, _ := ioutil.ReadAll(r.Body)
|
||||
return fmt.Errorf("bad status: %v body:\n%s", r.Status, b)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(res); err != nil {
|
||||
return fmt.Errorf("unmarshalling response: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flatten takes a sequence of Events and returns their contents, concatenated.
|
||||
func flatten(seq []Event) string {
|
||||
var buf bytes.Buffer
|
||||
for _, e := range seq {
|
||||
buf.WriteString(e.Message)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func share(w http.ResponseWriter, r *http.Request) {
|
||||
if googleCN(r) {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
|
||||
// TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
|
||||
req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
|
||||
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
req = req.WithContext(r.Context())
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("ERROR share error: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
copyHeader := func(k string) {
|
||||
if v := resp.Header.Get(k); v != "" {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
copyHeader("Content-Type")
|
||||
copyHeader("Content-Length")
|
||||
defer resp.Body.Close()
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func googleCN(r *http.Request) bool {
|
||||
if r.FormValue("googlecn") != "" {
|
||||
return true
|
||||
}
|
||||
if !env.IsProd() {
|
||||
return false
|
||||
}
|
||||
if strings.HasSuffix(r.Host, ".cn") {
|
||||
return true
|
||||
}
|
||||
switch r.Header.Get("X-AppEngine-Country") {
|
||||
case "", "ZZ", "CN":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright 2014 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.
|
||||
|
||||
// This file provides a compact encoding of
|
||||
// a map of Mercurial hashes to Git hashes.
|
||||
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// hashMap is a map of Mercurial hashes to Git hashes.
|
||||
type hashMap struct {
|
||||
file *os.File
|
||||
entries int
|
||||
}
|
||||
|
||||
// newHashMap takes a file handle that contains a map of Mercurial to Git
|
||||
// hashes. The file should be a sequence of pairs of little-endian encoded
|
||||
// uint32s, representing a hgHash and a gitHash respectively.
|
||||
// The sequence must be sorted by hgHash.
|
||||
// The file must remain open for as long as the returned hashMap is used.
|
||||
func newHashMap(f *os.File) (*hashMap, error) {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hashMap{file: f, entries: int(fi.Size() / 8)}, nil
|
||||
}
|
||||
|
||||
// Lookup finds an hgHash in the map that matches the given prefix, and returns
|
||||
// its corresponding gitHash. The prefix must be at least 8 characters long.
|
||||
func (m *hashMap) Lookup(s string) gitHash {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
hg, err := hgHashFromString(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var git gitHash
|
||||
b := make([]byte, 8)
|
||||
sort.Search(m.entries, func(i int) bool {
|
||||
n, err := m.file.ReadAt(b, int64(i*8))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if n != 8 {
|
||||
panic(io.ErrUnexpectedEOF)
|
||||
}
|
||||
v := hgHash(binary.LittleEndian.Uint32(b[:4]))
|
||||
if v == hg {
|
||||
git = gitHash(binary.LittleEndian.Uint32(b[4:]))
|
||||
}
|
||||
return v >= hg
|
||||
})
|
||||
return git
|
||||
}
|
||||
|
||||
// hgHash represents the lower (leftmost) 32 bits of a Mercurial hash.
|
||||
type hgHash uint32
|
||||
|
||||
func (h hgHash) String() string {
|
||||
return intToHash(int64(h))
|
||||
}
|
||||
|
||||
func hgHashFromString(s string) (hgHash, error) {
|
||||
if len(s) < 8 {
|
||||
return 0, fmt.Errorf("string too small: len(s) = %d", len(s))
|
||||
}
|
||||
hash := s[:8]
|
||||
i, err := strconv.ParseInt(hash, 16, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hgHash(i), nil
|
||||
}
|
||||
|
||||
// gitHash represents the leftmost 28 bits of a Git hash in its upper 28 bits,
|
||||
// and it encodes hash's repository in the lower 4 bits.
|
||||
type gitHash uint32
|
||||
|
||||
func (h gitHash) Hash() string {
|
||||
return intToHash(int64(h))[:7]
|
||||
}
|
||||
|
||||
func (h gitHash) Repo() string {
|
||||
return repo(h & 0xF).String()
|
||||
}
|
||||
|
||||
func intToHash(i int64) string {
|
||||
s := strconv.FormatInt(i, 16)
|
||||
if len(s) < 8 {
|
||||
s = strings.Repeat("0", 8-len(s)) + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// repo represents a Go Git repository.
|
||||
type repo byte
|
||||
|
||||
const (
|
||||
repoGo repo = iota
|
||||
repoBlog
|
||||
repoCrypto
|
||||
repoExp
|
||||
repoImage
|
||||
repoMobile
|
||||
repoNet
|
||||
repoSys
|
||||
repoTalks
|
||||
repoText
|
||||
repoTools
|
||||
)
|
||||
|
||||
func (r repo) String() string {
|
||||
return map[repo]string{
|
||||
repoGo: "go",
|
||||
repoBlog: "blog",
|
||||
repoCrypto: "crypto",
|
||||
repoExp: "exp",
|
||||
repoImage: "image",
|
||||
repoMobile: "mobile",
|
||||
repoNet: "net",
|
||||
repoSys: "sys",
|
||||
repoTalks: "talks",
|
||||
repoText: "text",
|
||||
repoTools: "tools",
|
||||
}[r]
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
// 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.
|
||||
|
||||
// Package redirect provides hooks to register HTTP handlers that redirect old
|
||||
// godoc paths to their new equivalents and assist in accessing the issue
|
||||
// tracker, wiki, code review system, etc.
|
||||
package redirect // import "golang.org/x/website/cmd/golangorg/godoc/redirect"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Register registers HTTP handlers that redirect old godoc paths to their new
|
||||
// equivalents and assist in accessing the issue tracker, wiki, code review
|
||||
// system, etc. If mux is nil it uses http.DefaultServeMux.
|
||||
func Register(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
handlePathRedirects(mux, pkgRedirects, "/pkg/")
|
||||
handlePathRedirects(mux, cmdRedirects, "/cmd/")
|
||||
for prefix, redirect := range prefixHelpers {
|
||||
p := "/" + prefix + "/"
|
||||
mux.Handle(p, PrefixHandler(p, redirect))
|
||||
}
|
||||
for path, redirect := range redirects {
|
||||
mux.Handle(path, Handler(redirect))
|
||||
}
|
||||
// NB: /src/pkg (sans trailing slash) is the index of packages.
|
||||
mux.HandleFunc("/src/pkg/", srcPkgHandler)
|
||||
mux.HandleFunc("/cl/", clHandler)
|
||||
mux.HandleFunc("/change/", changeHandler)
|
||||
mux.HandleFunc("/design/", designHandler)
|
||||
}
|
||||
|
||||
func handlePathRedirects(mux *http.ServeMux, redirects map[string]string, prefix string) {
|
||||
for source, target := range redirects {
|
||||
h := Handler(prefix + target + "/")
|
||||
p := prefix + source
|
||||
mux.Handle(p, h)
|
||||
mux.Handle(p+"/", h)
|
||||
}
|
||||
}
|
||||
|
||||
// Packages that were renamed between r60 and go1.
|
||||
var pkgRedirects = map[string]string{
|
||||
"asn1": "encoding/asn1",
|
||||
"big": "math/big",
|
||||
"cmath": "math/cmplx",
|
||||
"csv": "encoding/csv",
|
||||
"exec": "os/exec",
|
||||
"exp/template/html": "html/template",
|
||||
"gob": "encoding/gob",
|
||||
"http": "net/http",
|
||||
"http/cgi": "net/http/cgi",
|
||||
"http/fcgi": "net/http/fcgi",
|
||||
"http/httptest": "net/http/httptest",
|
||||
"http/pprof": "net/http/pprof",
|
||||
"json": "encoding/json",
|
||||
"mail": "net/mail",
|
||||
"rand": "math/rand",
|
||||
"rpc": "net/rpc",
|
||||
"rpc/jsonrpc": "net/rpc/jsonrpc",
|
||||
"scanner": "text/scanner",
|
||||
"smtp": "net/smtp",
|
||||
"tabwriter": "text/tabwriter",
|
||||
"template": "text/template",
|
||||
"template/parse": "text/template/parse",
|
||||
"url": "net/url",
|
||||
"utf16": "unicode/utf16",
|
||||
"utf8": "unicode/utf8",
|
||||
"xml": "encoding/xml",
|
||||
}
|
||||
|
||||
// Commands that were renamed between r60 and go1.
|
||||
var cmdRedirects = map[string]string{
|
||||
"gofix": "fix",
|
||||
"goinstall": "go",
|
||||
"gopack": "pack",
|
||||
"gotest": "go",
|
||||
"govet": "vet",
|
||||
"goyacc": "yacc",
|
||||
}
|
||||
|
||||
var redirects = map[string]string{
|
||||
"/blog": "/blog/",
|
||||
"/build": "http://build.golang.org",
|
||||
"/change": "https://go.googlesource.com/go",
|
||||
"/cl": "https://go-review.googlesource.com",
|
||||
"/cmd/godoc/": "http://godoc.org/golang.org/x/website/cmd/golangorg/",
|
||||
"/issue": "https://github.com/golang/go/issues",
|
||||
"/issue/new": "https://github.com/golang/go/issues/new",
|
||||
"/issues": "https://github.com/golang/go/issues",
|
||||
"/issues/new": "https://github.com/golang/go/issues/new",
|
||||
"/play": "http://play.golang.org",
|
||||
"/design": "https://go.googlesource.com/proposal/+/master/design",
|
||||
|
||||
// In Go 1.2 the references page is part of /doc/.
|
||||
"/ref": "/doc/#references",
|
||||
// This next rule clobbers /ref/spec and /ref/mem.
|
||||
// TODO(adg): figure out what to do here, if anything.
|
||||
// "/ref/": "/doc/#references",
|
||||
|
||||
// Be nice to people who are looking in the wrong place.
|
||||
"/doc/mem": "/ref/mem",
|
||||
"/doc/spec": "/ref/spec",
|
||||
|
||||
"/talks": "http://talks.golang.org",
|
||||
"/tour": "http://tour.golang.org",
|
||||
"/wiki": "https://github.com/golang/go/wiki",
|
||||
|
||||
"/doc/articles/c_go_cgo.html": "/blog/c-go-cgo",
|
||||
"/doc/articles/concurrency_patterns.html": "/blog/go-concurrency-patterns-timing-out-and",
|
||||
"/doc/articles/defer_panic_recover.html": "/blog/defer-panic-and-recover",
|
||||
"/doc/articles/error_handling.html": "/blog/error-handling-and-go",
|
||||
"/doc/articles/gobs_of_data.html": "/blog/gobs-of-data",
|
||||
"/doc/articles/godoc_documenting_go_code.html": "/blog/godoc-documenting-go-code",
|
||||
"/doc/articles/gos_declaration_syntax.html": "/blog/gos-declaration-syntax",
|
||||
"/doc/articles/image_draw.html": "/blog/go-imagedraw-package",
|
||||
"/doc/articles/image_package.html": "/blog/go-image-package",
|
||||
"/doc/articles/json_and_go.html": "/blog/json-and-go",
|
||||
"/doc/articles/json_rpc_tale_of_interfaces.html": "/blog/json-rpc-tale-of-interfaces",
|
||||
"/doc/articles/laws_of_reflection.html": "/blog/laws-of-reflection",
|
||||
"/doc/articles/slices_usage_and_internals.html": "/blog/go-slices-usage-and-internals",
|
||||
"/doc/go_for_cpp_programmers.html": "/wiki/GoForCPPProgrammers",
|
||||
"/doc/go_tutorial.html": "http://tour.golang.org/",
|
||||
}
|
||||
|
||||
var prefixHelpers = map[string]string{
|
||||
"issue": "https://github.com/golang/go/issues/",
|
||||
"issues": "https://github.com/golang/go/issues/",
|
||||
"play": "http://play.golang.org/",
|
||||
"talks": "http://talks.golang.org/",
|
||||
"wiki": "https://github.com/golang/go/wiki/",
|
||||
}
|
||||
|
||||
func Handler(target string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
url := target
|
||||
if qs := r.URL.RawQuery; qs != "" {
|
||||
url += "?" + qs
|
||||
}
|
||||
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
||||
})
|
||||
}
|
||||
|
||||
var validId = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
|
||||
|
||||
func PrefixHandler(prefix, baseURL string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.URL.Path; p == prefix {
|
||||
// redirect /prefix/ to /prefix
|
||||
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
|
||||
return
|
||||
}
|
||||
id := r.URL.Path[len(prefix):]
|
||||
if !validId.MatchString(id) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
target := baseURL + id
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
// Redirect requests from the old "/src/pkg/foo" to the new "/src/foo".
|
||||
// See http://golang.org/s/go14nopkg
|
||||
func srcPkgHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):]
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func clHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const prefix = "/cl/"
|
||||
if p := r.URL.Path; p == prefix {
|
||||
// redirect /prefix/ to /prefix
|
||||
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
|
||||
return
|
||||
}
|
||||
id := r.URL.Path[len(prefix):]
|
||||
// support /cl/152700045/, which is used in commit 0edafefc36.
|
||||
id = strings.TrimSuffix(id, "/")
|
||||
if !validId.MatchString(id) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
target := ""
|
||||
|
||||
if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) {
|
||||
// TODO: Issue 28836: if this Rietveld CL happens to
|
||||
// also be a Gerrit CL, render a disambiguation HTML
|
||||
// page with two links instead. We'll need to make an
|
||||
// RPC (to maintner?) to figure that out. For now just
|
||||
// redirect to rietveld.
|
||||
target = "https://codereview.appspot.com/" + id
|
||||
} else {
|
||||
target = "https://go-review.googlesource.com/" + id
|
||||
}
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
}
|
||||
|
||||
var changeMap *hashMap
|
||||
|
||||
// LoadChangeMap loads the specified map of Mercurial to Git revisions,
|
||||
// which is used by the /change/ handler to intelligently map old hg
|
||||
// revisions to their new git equivalents.
|
||||
// It should be called before calling Register.
|
||||
// The file should remain open as long as the process is running.
|
||||
// See the implementation of this package for details.
|
||||
func LoadChangeMap(filename string) error {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := newHashMap(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changeMap = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func changeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const prefix = "/change/"
|
||||
if p := r.URL.Path; p == prefix {
|
||||
// redirect /prefix/ to /prefix
|
||||
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
|
||||
return
|
||||
}
|
||||
hash := r.URL.Path[len(prefix):]
|
||||
target := "https://go.googlesource.com/go/+/" + hash
|
||||
if git := changeMap.Lookup(hash); git > 0 {
|
||||
target = fmt.Sprintf("https://go.googlesource.com/%v/+/%v", git.Repo(), git.Hash())
|
||||
}
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
}
|
||||
|
||||
func designHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const prefix = "/design/"
|
||||
if p := r.URL.Path; p == prefix {
|
||||
// redirect /prefix/ to /prefix
|
||||
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
|
||||
return
|
||||
}
|
||||
name := r.URL.Path[len(prefix):]
|
||||
target := "https://go.googlesource.com/proposal/+/master/design/" + name + ".md"
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type redirectResult struct {
|
||||
status int
|
||||
path string
|
||||
}
|
||||
|
||||
func errorResult(status int) redirectResult {
|
||||
return redirectResult{status, ""}
|
||||
}
|
||||
|
||||
func TestRedirects(t *testing.T) {
|
||||
var tests = map[string]redirectResult{
|
||||
"/build": {301, "http://build.golang.org"},
|
||||
"/ref": {301, "/doc/#references"},
|
||||
"/doc/mem": {301, "/ref/mem"},
|
||||
"/doc/spec": {301, "/ref/spec"},
|
||||
"/tour": {301, "http://tour.golang.org"},
|
||||
"/foo": errorResult(404),
|
||||
|
||||
"/pkg/asn1": {301, "/pkg/encoding/asn1/"},
|
||||
"/pkg/template/parse": {301, "/pkg/text/template/parse/"},
|
||||
|
||||
"/src/pkg/foo": {301, "/src/foo"},
|
||||
|
||||
"/cmd/gofix": {301, "/cmd/fix/"},
|
||||
|
||||
// git commits (/change)
|
||||
// TODO: mercurial tags and LoadChangeMap.
|
||||
"/change": {301, "https://go.googlesource.com/go"},
|
||||
"/change/a": {302, "https://go.googlesource.com/go/+/a"},
|
||||
|
||||
"/issue": {301, "https://github.com/golang/go/issues"},
|
||||
"/issue?": {301, "https://github.com/golang/go/issues"},
|
||||
"/issue/1": {302, "https://github.com/golang/go/issues/1"},
|
||||
"/issue/new": {301, "https://github.com/golang/go/issues/new"},
|
||||
"/issue/new?a=b&c=d%20&e=f": {301, "https://github.com/golang/go/issues/new?a=b&c=d%20&e=f"},
|
||||
"/issues": {301, "https://github.com/golang/go/issues"},
|
||||
"/issues/1": {302, "https://github.com/golang/go/issues/1"},
|
||||
"/issues/new": {301, "https://github.com/golang/go/issues/new"},
|
||||
"/issues/1/2/3": errorResult(404),
|
||||
|
||||
"/wiki/foo": {302, "https://github.com/golang/go/wiki/foo"},
|
||||
"/wiki/foo/": {302, "https://github.com/golang/go/wiki/foo/"},
|
||||
|
||||
"/design": {301, "https://go.googlesource.com/proposal/+/master/design"},
|
||||
"/design/": {302, "/design"},
|
||||
"/design/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/123-foo.md"},
|
||||
"/design/text/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/text/123-foo.md"},
|
||||
|
||||
"/cl/1": {302, "https://go-review.googlesource.com/1"},
|
||||
"/cl/1/": {302, "https://go-review.googlesource.com/1"},
|
||||
"/cl/267120043": {302, "https://codereview.appspot.com/267120043"},
|
||||
"/cl/267120043/": {302, "https://codereview.appspot.com/267120043"},
|
||||
|
||||
// Verify that we're using the Rietveld CL table:
|
||||
"/cl/152046": {302, "https://codereview.appspot.com/152046"},
|
||||
"/cl/152047": {302, "https://go-review.googlesource.com/152047"},
|
||||
"/cl/152048": {302, "https://codereview.appspot.com/152048"},
|
||||
|
||||
// And verify we're using the the "bigEnoughAssumeRietveld" value:
|
||||
"/cl/299999": {302, "https://go-review.googlesource.com/299999"},
|
||||
"/cl/300000": {302, "https://codereview.appspot.com/300000"},
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
Register(mux)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
for path, want := range tests {
|
||||
if want.path != "" && want.path[0] == '/' {
|
||||
// All redirects are absolute.
|
||||
want.path = ts.URL + want.path
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL+path, nil)
|
||||
if err != nil {
|
||||
t.Errorf("(path: %q) unexpected error: %v", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := http.DefaultTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("(path: %q) unexpected error: %v", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != want.status {
|
||||
t.Errorf("(path: %q) got status %d, want %d", path, resp.StatusCode, want.status)
|
||||
}
|
||||
|
||||
if want.status != 301 && want.status != 302 {
|
||||
// Not a redirect. Just check status.
|
||||
continue
|
||||
}
|
||||
|
||||
out, _ := resp.Location()
|
||||
if got := out.String(); got != want.path {
|
||||
t.Errorf("(path: %q) got %s, want %s", path, got, want.path)
|
||||
}
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,186 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
Query string
|
||||
Alert string // error or warning message
|
||||
|
||||
// identifier matches
|
||||
Pak HitList // packages matching Query
|
||||
Hit *LookupResult // identifier matches of Query
|
||||
Alt *AltWords // alternative identifiers to look for
|
||||
|
||||
// textual matches
|
||||
Found int // number of textual occurrences found
|
||||
Textual []FileLines // textual matches of Query
|
||||
Complete bool // true if all textual occurrences of Query are reported
|
||||
Idents map[SpotKind][]Ident
|
||||
}
|
||||
|
||||
func (c *Corpus) Lookup(query string) SearchResult {
|
||||
result := &SearchResult{Query: query}
|
||||
|
||||
index, timestamp := c.CurrentIndex()
|
||||
if index != nil {
|
||||
// identifier search
|
||||
if r, err := index.Lookup(query); err == nil {
|
||||
result = r
|
||||
} else if err != nil && !c.IndexFullText {
|
||||
// ignore the error if full text search is enabled
|
||||
// since the query may be a valid regular expression
|
||||
result.Alert = "Error in query string: " + err.Error()
|
||||
return *result
|
||||
}
|
||||
|
||||
// full text search
|
||||
if c.IndexFullText && query != "" {
|
||||
rx, err := regexp.Compile(query)
|
||||
if err != nil {
|
||||
result.Alert = "Error in query regular expression: " + err.Error()
|
||||
return *result
|
||||
}
|
||||
// If we get maxResults+1 results we know that there are more than
|
||||
// maxResults results and thus the result may be incomplete (to be
|
||||
// precise, we should remove one result from the result set, but
|
||||
// nobody is going to count the results on the result page).
|
||||
result.Found, result.Textual = index.LookupRegexp(rx, c.MaxResults+1)
|
||||
result.Complete = result.Found <= c.MaxResults
|
||||
if !result.Complete {
|
||||
result.Found-- // since we looked for maxResults+1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is the result accurate?
|
||||
if c.IndexEnabled {
|
||||
if ts := c.FSModifiedTime(); timestamp.Before(ts) {
|
||||
// The index is older than the latest file system change under godoc's observation.
|
||||
result.Alert = "Indexing in progress: result may be inaccurate"
|
||||
}
|
||||
} else {
|
||||
result.Alert = "Search index disabled: no results available"
|
||||
}
|
||||
|
||||
return *result
|
||||
}
|
||||
|
||||
// SearchResultDoc optionally specifies a function returning an HTML body
|
||||
// displaying search results matching godoc documentation.
|
||||
func (p *Presentation) SearchResultDoc(result SearchResult) []byte {
|
||||
return applyTemplate(p.SearchDocHTML, "searchDocHTML", result)
|
||||
}
|
||||
|
||||
// SearchResultCode optionally specifies a function returning an HTML body
|
||||
// displaying search results matching source code.
|
||||
func (p *Presentation) SearchResultCode(result SearchResult) []byte {
|
||||
return applyTemplate(p.SearchCodeHTML, "searchCodeHTML", result)
|
||||
}
|
||||
|
||||
// SearchResultTxt optionally specifies a function returning an HTML body
|
||||
// displaying search results of textual matches.
|
||||
func (p *Presentation) SearchResultTxt(result SearchResult) []byte {
|
||||
return applyTemplate(p.SearchTxtHTML, "searchTxtHTML", result)
|
||||
}
|
||||
|
||||
// HandleSearch obtains results for the requested search and returns a page
|
||||
// to display them.
|
||||
func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
query := strings.TrimSpace(r.FormValue("q"))
|
||||
result := p.Corpus.Lookup(query)
|
||||
|
||||
var contents bytes.Buffer
|
||||
for _, f := range p.SearchResults {
|
||||
contents.Write(f(p, result))
|
||||
}
|
||||
|
||||
var title string
|
||||
if haveResults := contents.Len() > 0; haveResults {
|
||||
title = fmt.Sprintf(`Results for query: %v`, query)
|
||||
if !p.Corpus.IndexEnabled {
|
||||
result.Alert = ""
|
||||
}
|
||||
} else {
|
||||
title = fmt.Sprintf(`No results found for query %q`, query)
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(applyTemplate(p.SearchHTML, "searchHTML", result))
|
||||
body.Write(contents.Bytes())
|
||||
|
||||
p.ServePage(w, Page{
|
||||
Title: title,
|
||||
Tabtitle: query,
|
||||
Query: query,
|
||||
Body: body.Bytes(),
|
||||
GoogleCN: googleCN(r),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Presentation) serveSearchDesc(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
|
||||
data := map[string]interface{}{
|
||||
"BaseURL": fmt.Sprintf("http://%s", r.Host),
|
||||
}
|
||||
applyTemplateToResponseWriter(w, p.SearchDescXML, &data)
|
||||
}
|
||||
|
||||
// tocColCount returns the no. of columns
|
||||
// to split the toc table to.
|
||||
func tocColCount(result SearchResult) int {
|
||||
tocLen := tocLen(result)
|
||||
colCount := 0
|
||||
// Simple heuristic based on visual aesthetic in manual testing.
|
||||
switch {
|
||||
case tocLen <= 10:
|
||||
colCount = 1
|
||||
case tocLen <= 20:
|
||||
colCount = 2
|
||||
case tocLen <= 80:
|
||||
colCount = 3
|
||||
default:
|
||||
colCount = 4
|
||||
}
|
||||
return colCount
|
||||
}
|
||||
|
||||
// tocLen calculates the no. of items in the toc table
|
||||
// by going through various fields in the SearchResult
|
||||
// that is rendered in the UI.
|
||||
func tocLen(result SearchResult) int {
|
||||
tocLen := 0
|
||||
for _, val := range result.Idents {
|
||||
if len(val) != 0 {
|
||||
tocLen++
|
||||
}
|
||||
}
|
||||
// If no identifiers, then just one item for the header text "Package <result.Query>".
|
||||
// See searchcode.html for further details.
|
||||
if len(result.Idents) == 0 {
|
||||
tocLen++
|
||||
}
|
||||
if result.Hit != nil {
|
||||
if len(result.Hit.Decls) > 0 {
|
||||
tocLen += len(result.Hit.Decls)
|
||||
// We need one extra item for the header text "Package-level declarations".
|
||||
tocLen++
|
||||
}
|
||||
if len(result.Hit.Others) > 0 {
|
||||
tocLen += len(result.Hit.Others)
|
||||
// We need one extra item for the header text "Local declarations and uses".
|
||||
tocLen++
|
||||
}
|
||||
}
|
||||
// For "textual occurrences".
|
||||
tocLen++
|
||||
return tocLen
|
||||
}
|
|
@ -0,0 +1,836 @@
|
|||
// 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/doc"
|
||||
"go/token"
|
||||
htmlpkg "html"
|
||||
htmltemplate "html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/analysis"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/util"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// handlerServer is a migration from an old godoc http Handler type.
|
||||
// This should probably merge into something else.
|
||||
type handlerServer struct {
|
||||
p *Presentation
|
||||
c *Corpus // copy of p.Corpus
|
||||
pattern string // url pattern; e.g. "/pkg/"
|
||||
stripPrefix string // prefix to strip from import path; e.g. "pkg/"
|
||||
fsRoot string // file system root to which the pattern is mapped; e.g. "/src"
|
||||
exclude []string // file system paths to exclude; e.g. "/src/cmd"
|
||||
}
|
||||
|
||||
func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
|
||||
mux.Handle(s.pattern, s)
|
||||
}
|
||||
|
||||
// getPageInfo returns the PageInfo for a package directory abspath. If the
|
||||
// parameter genAST is set, an AST containing only the package exports is
|
||||
// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
|
||||
// is extracted from the AST. If there is no corresponding package in the
|
||||
// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
|
||||
// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
|
||||
// set to the respective error but the error is not logged.
|
||||
//
|
||||
func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
|
||||
info := &PageInfo{Dirname: abspath, Mode: mode}
|
||||
|
||||
// Restrict to the package files that would be used when building
|
||||
// the package on this system. This makes sure that if there are
|
||||
// separate implementations for, say, Windows vs Unix, we don't
|
||||
// jumble them all together.
|
||||
// Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
|
||||
// are used.
|
||||
ctxt := build.Default
|
||||
ctxt.IsAbsPath = pathpkg.IsAbs
|
||||
ctxt.IsDir = func(path string) bool {
|
||||
fi, err := h.c.fs.Stat(filepath.ToSlash(path))
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
|
||||
f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
|
||||
filtered := make([]os.FileInfo, 0, len(f))
|
||||
for _, i := range f {
|
||||
if mode&NoFiltering != 0 || i.Name() != "internal" {
|
||||
filtered = append(filtered, i)
|
||||
}
|
||||
}
|
||||
return filtered, err
|
||||
}
|
||||
ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
|
||||
data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
|
||||
// Make the syscall/js package always visible by default.
|
||||
// It defaults to the host's GOOS/GOARCH, and golang.org's
|
||||
// linux/amd64 means the wasm syscall/js package was blank.
|
||||
// And you can't run godoc on js/wasm anyway, so host defaults
|
||||
// don't make sense here.
|
||||
if goos == "" && goarch == "" && relpath == "syscall/js" {
|
||||
goos, goarch = "js", "wasm"
|
||||
}
|
||||
if goos != "" {
|
||||
ctxt.GOOS = goos
|
||||
}
|
||||
if goarch != "" {
|
||||
ctxt.GOARCH = goarch
|
||||
}
|
||||
|
||||
pkginfo, err := ctxt.ImportDir(abspath, 0)
|
||||
// continue if there are no Go source files; we still want the directory info
|
||||
if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
|
||||
info.Err = err
|
||||
return info
|
||||
}
|
||||
|
||||
// collect package files
|
||||
pkgname := pkginfo.Name
|
||||
pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
|
||||
if len(pkgfiles) == 0 {
|
||||
// Commands written in C have no .go files in the build.
|
||||
// Instead, documentation may be found in an ignored file.
|
||||
// The file may be ignored via an explicit +build ignore
|
||||
// constraint (recommended), or by defining the package
|
||||
// documentation (historic).
|
||||
pkgname = "main" // assume package main since pkginfo.Name == ""
|
||||
pkgfiles = pkginfo.IgnoredGoFiles
|
||||
}
|
||||
|
||||
// get package information, if any
|
||||
if len(pkgfiles) > 0 {
|
||||
// build package AST
|
||||
fset := token.NewFileSet()
|
||||
files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
|
||||
if err != nil {
|
||||
info.Err = err
|
||||
return info
|
||||
}
|
||||
|
||||
// ignore any errors - they are due to unresolved identifiers
|
||||
pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
|
||||
|
||||
// extract package documentation
|
||||
info.FSet = fset
|
||||
if mode&ShowSource == 0 {
|
||||
// show extracted documentation
|
||||
var m doc.Mode
|
||||
if mode&NoFiltering != 0 {
|
||||
m |= doc.AllDecls
|
||||
}
|
||||
if mode&AllMethods != 0 {
|
||||
m |= doc.AllMethods
|
||||
}
|
||||
info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
|
||||
if mode&NoTypeAssoc != 0 {
|
||||
for _, t := range info.PDoc.Types {
|
||||
info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
|
||||
info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
|
||||
info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
|
||||
t.Consts = nil
|
||||
t.Vars = nil
|
||||
t.Funcs = nil
|
||||
}
|
||||
// for now we cannot easily sort consts and vars since
|
||||
// go/doc.Value doesn't export the order information
|
||||
sort.Sort(funcsByName(info.PDoc.Funcs))
|
||||
}
|
||||
|
||||
// collect examples
|
||||
testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
|
||||
files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
|
||||
if err != nil {
|
||||
log.Println("parsing examples:", err)
|
||||
}
|
||||
info.Examples = collectExamples(h.c, pkg, files)
|
||||
|
||||
// collect any notes that we want to show
|
||||
if info.PDoc.Notes != nil {
|
||||
// could regexp.Compile only once per godoc, but probably not worth it
|
||||
if rx := h.p.NotesRx; rx != nil {
|
||||
for m, n := range info.PDoc.Notes {
|
||||
if rx.MatchString(m) {
|
||||
if info.Notes == nil {
|
||||
info.Notes = make(map[string][]*doc.Note)
|
||||
}
|
||||
info.Notes[m] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// show source code
|
||||
// TODO(gri) Consider eliminating export filtering in this mode,
|
||||
// or perhaps eliminating the mode altogether.
|
||||
if mode&NoFiltering == 0 {
|
||||
packageExports(fset, pkg)
|
||||
}
|
||||
info.PAst = files
|
||||
}
|
||||
info.IsMain = pkgname == "main"
|
||||
}
|
||||
|
||||
// get directory information, if any
|
||||
var dir *Directory
|
||||
var timestamp time.Time
|
||||
if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
|
||||
// directory tree is present; lookup respective directory
|
||||
// (may still fail if the file system was updated and the
|
||||
// new directory tree has not yet been computed)
|
||||
dir = tree.(*Directory).lookup(abspath)
|
||||
timestamp = ts
|
||||
}
|
||||
if dir == nil {
|
||||
// TODO(agnivade): handle this case better, now since there is no CLI mode.
|
||||
// no directory tree present (happens in command-line mode);
|
||||
// compute 2 levels for this page. The second level is to
|
||||
// get the synopses of sub-directories.
|
||||
// note: cannot use path filter here because in general
|
||||
// it doesn't contain the FSTree path
|
||||
dir = h.c.newDirectory(abspath, 2)
|
||||
timestamp = time.Now()
|
||||
}
|
||||
info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
|
||||
|
||||
info.DirTime = timestamp
|
||||
info.DirFlat = mode&FlatDir != 0
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
|
||||
// if the path is under one of the exclusion paths, don't list.
|
||||
for _, e := range h.exclude {
|
||||
if strings.HasPrefix(path, e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
|
||||
if mode&NoFiltering != 0 {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
|
||||
for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
|
||||
if c == "internal" || c == "vendor" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type funcsByName []*doc.Func
|
||||
|
||||
func (s funcsByName) Len() int { return len(s) }
|
||||
func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||
|
||||
func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
|
||||
|
||||
if !h.corpusInitialized() {
|
||||
h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
|
||||
return
|
||||
}
|
||||
|
||||
abspath := pathpkg.Join(h.fsRoot, relpath)
|
||||
mode := h.p.GetPageInfoMode(r)
|
||||
if relpath == builtinPkgPath {
|
||||
mode = NoFiltering | NoTypeAssoc
|
||||
}
|
||||
info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
|
||||
if info.Err != nil {
|
||||
log.Print(info.Err)
|
||||
h.p.ServeError(w, r, relpath, info.Err)
|
||||
return
|
||||
}
|
||||
|
||||
var tabtitle, title, subtitle string
|
||||
switch {
|
||||
case info.PAst != nil:
|
||||
for _, ast := range info.PAst {
|
||||
tabtitle = ast.Name.Name
|
||||
break
|
||||
}
|
||||
case info.PDoc != nil:
|
||||
tabtitle = info.PDoc.Name
|
||||
default:
|
||||
tabtitle = info.Dirname
|
||||
title = "Directory "
|
||||
if h.p.ShowTimestamps {
|
||||
subtitle = "Last update: " + info.DirTime.String()
|
||||
}
|
||||
}
|
||||
if title == "" {
|
||||
if info.IsMain {
|
||||
// assume that the directory name is the command name
|
||||
_, tabtitle = pathpkg.Split(relpath)
|
||||
title = "Command "
|
||||
} else {
|
||||
title = "Package "
|
||||
}
|
||||
}
|
||||
title += tabtitle
|
||||
|
||||
// special cases for top-level package/command directories
|
||||
switch tabtitle {
|
||||
case "/src":
|
||||
title = "Packages"
|
||||
tabtitle = "Packages"
|
||||
case "/src/cmd":
|
||||
title = "Commands"
|
||||
tabtitle = "Commands"
|
||||
}
|
||||
|
||||
// Emit JSON array for type information.
|
||||
pi := h.c.Analysis.PackageInfo(relpath)
|
||||
hasTreeView := len(pi.CallGraph) != 0
|
||||
info.CallGraphIndex = pi.CallGraphIndex
|
||||
info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
|
||||
info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
|
||||
info.TypeInfoIndex = make(map[string]int)
|
||||
for i, ti := range pi.Types {
|
||||
info.TypeInfoIndex[ti.Name] = i
|
||||
}
|
||||
|
||||
info.GoogleCN = googleCN(r)
|
||||
var body []byte
|
||||
if info.Dirname == "/src" {
|
||||
body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
|
||||
} else {
|
||||
body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
|
||||
}
|
||||
h.p.ServePage(w, Page{
|
||||
Title: title,
|
||||
Tabtitle: tabtitle,
|
||||
Subtitle: subtitle,
|
||||
Body: body,
|
||||
GoogleCN: info.GoogleCN,
|
||||
TreeView: hasTreeView,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handlerServer) corpusInitialized() bool {
|
||||
h.c.initMu.RLock()
|
||||
defer h.c.initMu.RUnlock()
|
||||
return h.c.initDone
|
||||
}
|
||||
|
||||
type PageInfoMode uint
|
||||
|
||||
const (
|
||||
PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
|
||||
|
||||
NoFiltering PageInfoMode = 1 << iota // do not filter exports
|
||||
AllMethods // show all embedded methods
|
||||
ShowSource // show source code, do not extract documentation
|
||||
NoHTML // show result in textual form, do not generate HTML
|
||||
FlatDir // show directory in a flat (non-indented) manner
|
||||
NoTypeAssoc // don't associate consts, vars, and factory functions with types
|
||||
)
|
||||
|
||||
// modeNames defines names for each PageInfoMode flag.
|
||||
var modeNames = map[string]PageInfoMode{
|
||||
"all": NoFiltering,
|
||||
"methods": AllMethods,
|
||||
"src": ShowSource,
|
||||
"text": NoHTML,
|
||||
"flat": FlatDir,
|
||||
}
|
||||
|
||||
// generate a query string for persisting PageInfoMode between pages.
|
||||
func modeQueryString(mode PageInfoMode) string {
|
||||
if modeNames := mode.names(); len(modeNames) > 0 {
|
||||
return "?m=" + strings.Join(modeNames, ",")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// alphabetically sorted names of active flags for a PageInfoMode.
|
||||
func (m PageInfoMode) names() []string {
|
||||
var names []string
|
||||
for name, mode := range modeNames {
|
||||
if m&mode != 0 {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
|
||||
// URL form value "m". It is value is a comma-separated list of mode names
|
||||
// as defined by modeNames (e.g.: m=src,text).
|
||||
func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
|
||||
var mode PageInfoMode
|
||||
for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
|
||||
if m, found := modeNames[strings.TrimSpace(k)]; found {
|
||||
mode |= m
|
||||
}
|
||||
}
|
||||
if p.AdjustPageInfoMode != nil {
|
||||
mode = p.AdjustPageInfoMode(r, mode)
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// poorMansImporter returns a (dummy) package object named
|
||||
// by the last path component of the provided package path
|
||||
// (as is the convention for packages). This is sufficient
|
||||
// to resolve package identifiers without doing an actual
|
||||
// import. It never returns an error.
|
||||
//
|
||||
func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
|
||||
pkg := imports[path]
|
||||
if pkg == nil {
|
||||
// note that strings.LastIndex returns -1 if there is no "/"
|
||||
pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
|
||||
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
|
||||
imports[path] = pkg
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// globalNames returns a set of the names declared by all package-level
|
||||
// declarations. Method names are returned in the form Receiver_Method.
|
||||
func globalNames(pkg *ast.Package) map[string]bool {
|
||||
names := make(map[string]bool)
|
||||
for _, file := range pkg.Files {
|
||||
for _, decl := range file.Decls {
|
||||
addNames(names, decl)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// collectExamples collects examples for pkg from testfiles.
|
||||
func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
|
||||
var files []*ast.File
|
||||
for _, f := range testfiles {
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
var examples []*doc.Example
|
||||
globals := globalNames(pkg)
|
||||
for _, e := range doc.Examples(files...) {
|
||||
name := stripExampleSuffix(e.Name)
|
||||
if name == "" || globals[name] {
|
||||
examples = append(examples, e)
|
||||
} else if c.Verbose {
|
||||
log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return examples
|
||||
}
|
||||
|
||||
// addNames adds the names declared by decl to the names set.
|
||||
// Method names are added in the form ReceiverTypeName_Method.
|
||||
func addNames(names map[string]bool, decl ast.Decl) {
|
||||
switch d := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
name := d.Name.Name
|
||||
if d.Recv != nil {
|
||||
var typeName string
|
||||
switch r := d.Recv.List[0].Type.(type) {
|
||||
case *ast.StarExpr:
|
||||
typeName = r.X.(*ast.Ident).Name
|
||||
case *ast.Ident:
|
||||
typeName = r.Name
|
||||
}
|
||||
name = typeName + "_" + name
|
||||
}
|
||||
names[name] = true
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range d.Specs {
|
||||
switch s := spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
names[s.Name.Name] = true
|
||||
case *ast.ValueSpec:
|
||||
for _, id := range s.Names {
|
||||
names[id.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// packageExports is a local implementation of ast.PackageExports
|
||||
// which correctly updates each package file's comment list.
|
||||
// (The ast.PackageExports signature is frozen, hence the local
|
||||
// implementation).
|
||||
//
|
||||
func packageExports(fset *token.FileSet, pkg *ast.Package) {
|
||||
for _, src := range pkg.Files {
|
||||
cmap := ast.NewCommentMap(fset, src, src.Comments)
|
||||
ast.FileExports(src)
|
||||
src.Comments = cmap.Filter(src).Comments()
|
||||
}
|
||||
}
|
||||
|
||||
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
log.Printf("%s.Execute: %s", name, err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
type writerCapturesErr struct {
|
||||
w io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *writerCapturesErr) Write(p []byte) (int, error) {
|
||||
n, err := w.w.Write(p)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
|
||||
// for the call to template.Execute. It uses an io.Writer wrapper to capture
|
||||
// errors from the underlying http.ResponseWriter. Errors are logged only when
|
||||
// they come from the template processing and not the Writer; this avoid
|
||||
// polluting log files with error messages due to networking issues, such as
|
||||
// client disconnects and http HEAD protocol violations.
|
||||
func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
|
||||
w := &writerCapturesErr{w: rw}
|
||||
err := t.Execute(w, data)
|
||||
// There are some cases where template.Execute does not return an error when
|
||||
// rw returns an error, and some where it does. So check w.err first.
|
||||
if w.err == nil && err != nil {
|
||||
// Log template errors.
|
||||
log.Printf("%s.Execute: %s", t.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
canonical := pathpkg.Clean(r.URL.Path)
|
||||
if !strings.HasSuffix(canonical, "/") {
|
||||
canonical += "/"
|
||||
}
|
||||
if r.URL.Path != canonical {
|
||||
url := *r.URL
|
||||
url.Path = canonical
|
||||
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
||||
redirected = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||
c := pathpkg.Clean(r.URL.Path)
|
||||
c = strings.TrimRight(c, "/")
|
||||
if r.URL.Path != c {
|
||||
url := *r.URL
|
||||
url.Path = c
|
||||
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
||||
redirected = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
|
||||
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
|
||||
if err != nil {
|
||||
log.Printf("ReadFile: %s", err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue(PageInfoModeQueryString) == "text" {
|
||||
p.ServeText(w, src)
|
||||
return
|
||||
}
|
||||
|
||||
h := r.FormValue("h")
|
||||
s := RangeSelection(r.FormValue("s"))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if pathpkg.Ext(abspath) == ".go" {
|
||||
// Find markup links for this file (e.g. "/src/fmt/print.go").
|
||||
fi := p.Corpus.Analysis.FileInfo(abspath)
|
||||
buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
|
||||
buf.Write(marshalJSON(fi.Data))
|
||||
buf.WriteString(";</script>\n")
|
||||
|
||||
if status := p.Corpus.Analysis.Status(); status != "" {
|
||||
buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
|
||||
// TODO(adonovan): show analysis status at per-file granularity.
|
||||
fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
|
||||
}
|
||||
|
||||
buf.WriteString("<pre>")
|
||||
formatGoSource(&buf, src, fi.Links, h, s)
|
||||
buf.WriteString("</pre>")
|
||||
} else {
|
||||
buf.WriteString("<pre>")
|
||||
FormatText(&buf, src, 1, false, h, s)
|
||||
buf.WriteString("</pre>")
|
||||
}
|
||||
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
|
||||
|
||||
p.ServePage(w, Page{
|
||||
Title: title,
|
||||
SrcPath: relpath,
|
||||
Tabtitle: relpath,
|
||||
Body: buf.Bytes(),
|
||||
GoogleCN: googleCN(r),
|
||||
})
|
||||
}
|
||||
|
||||
// formatGoSource HTML-escapes Go source text and writes it to w,
|
||||
// decorating it with the specified analysis links.
|
||||
//
|
||||
func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
|
||||
// Emit to a temp buffer so that we can add line anchors at the end.
|
||||
saved, buf := buf, new(bytes.Buffer)
|
||||
|
||||
var i int
|
||||
var link analysis.Link // shared state of the two funcs below
|
||||
segmentIter := func() (seg Segment) {
|
||||
if i < len(links) {
|
||||
link = links[i]
|
||||
i++
|
||||
seg = Segment{link.Start(), link.End()}
|
||||
}
|
||||
return
|
||||
}
|
||||
linkWriter := func(w io.Writer, offs int, start bool) {
|
||||
link.Write(w, offs, start)
|
||||
}
|
||||
|
||||
comments := tokenSelection(text, token.COMMENT)
|
||||
var highlights Selection
|
||||
if pattern != "" {
|
||||
highlights = regexpSelection(text, pattern)
|
||||
}
|
||||
|
||||
FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
|
||||
|
||||
// Now copy buf to saved, adding line anchors.
|
||||
|
||||
// The lineSelection mechanism can't be composed with our
|
||||
// linkWriter, so we have to add line spans as another pass.
|
||||
n := 1
|
||||
for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
|
||||
// The line numbers are inserted into the document via a CSS ::before
|
||||
// pseudo-element. This prevents them from being copied when users
|
||||
// highlight and copy text.
|
||||
// ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
|
||||
// This is also the trick Github uses to hide line numbers.
|
||||
//
|
||||
// The first tab for the code snippet needs to start in column 9, so
|
||||
// it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
|
||||
// character only indents a short amount.
|
||||
//
|
||||
// Due to rounding and font width Firefox might not treat 8 rendered
|
||||
// characters as 8 characters wide, and subsequently may treat the tab
|
||||
// character in the 9th position as moving the width from (7.5 or so) up
|
||||
// to 8. See
|
||||
// https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
|
||||
// for a fuller explanation. The solution is to add a CSS class to
|
||||
// explicitly declare the width to be 8 characters.
|
||||
fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d </span>`, n, n)
|
||||
n++
|
||||
saved.Write(line)
|
||||
saved.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := p.Corpus.fs.ReadDir(abspath)
|
||||
if err != nil {
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.ServePage(w, Page{
|
||||
Title: "Directory",
|
||||
SrcPath: relpath,
|
||||
Tabtitle: relpath,
|
||||
Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
|
||||
GoogleCN: googleCN(r),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
// get HTML body contents
|
||||
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
|
||||
if err != nil {
|
||||
log.Printf("ReadFile: %s", err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// if it begins with "<!DOCTYPE " assume it is standalone
|
||||
// html that doesn't need the template wrapping.
|
||||
if bytes.HasPrefix(src, doctype) {
|
||||
w.Write(src)
|
||||
return
|
||||
}
|
||||
|
||||
// if it begins with a JSON blob, read in the metadata.
|
||||
meta, src, err := extractMetadata(src)
|
||||
if err != nil {
|
||||
log.Printf("decoding metadata %s: %v", relpath, err)
|
||||
}
|
||||
|
||||
page := Page{
|
||||
Title: meta.Title,
|
||||
Subtitle: meta.Subtitle,
|
||||
GoogleCN: googleCN(r),
|
||||
}
|
||||
|
||||
// evaluate as template if indicated
|
||||
if meta.Template {
|
||||
tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
|
||||
if err != nil {
|
||||
log.Printf("parsing template %s: %v", relpath, err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, page); err != nil {
|
||||
log.Printf("executing template %s: %v", relpath, err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
src = buf.Bytes()
|
||||
}
|
||||
|
||||
// if it's the language spec, add tags to EBNF productions
|
||||
if strings.HasSuffix(abspath, "go_spec.html") {
|
||||
var buf bytes.Buffer
|
||||
Linkify(&buf, src)
|
||||
src = buf.Bytes()
|
||||
}
|
||||
|
||||
page.Body = src
|
||||
p.ServePage(w, page)
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
p.serveFile(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
|
||||
relpath := r.URL.Path
|
||||
|
||||
// Check to see if we need to redirect or serve another file.
|
||||
if m := p.Corpus.MetadataFor(relpath); m != nil {
|
||||
if m.Path != relpath {
|
||||
// Redirect to canonical path.
|
||||
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
// Serve from the actual filesystem path.
|
||||
relpath = m.filePath
|
||||
}
|
||||
|
||||
abspath := relpath
|
||||
relpath = relpath[1:] // strip leading slash
|
||||
|
||||
switch pathpkg.Ext(relpath) {
|
||||
case ".html":
|
||||
if strings.HasSuffix(relpath, "/index.html") {
|
||||
// We'll show index.html for the directory.
|
||||
// Use the dir/ version as canonical instead of dir/index.html.
|
||||
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
p.ServeHTMLDoc(w, r, abspath, relpath)
|
||||
return
|
||||
|
||||
case ".go":
|
||||
p.serveTextFile(w, r, abspath, relpath, "Source file")
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := p.Corpus.fs.Lstat(abspath)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
p.ServeError(w, r, relpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if dir != nil && dir.IsDir() {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
}
|
||||
if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
|
||||
p.ServeHTMLDoc(w, r, index, index)
|
||||
return
|
||||
}
|
||||
p.serveDirectory(w, r, abspath, relpath)
|
||||
return
|
||||
}
|
||||
|
||||
if util.IsTextFile(p.Corpus.fs, abspath) {
|
||||
if redirectFile(w, r) {
|
||||
return
|
||||
}
|
||||
p.serveTextFile(w, r, abspath, relpath, "Text file")
|
||||
return
|
||||
}
|
||||
|
||||
p.fileServer.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write(text)
|
||||
}
|
||||
|
||||
func marshalJSON(x interface{}) []byte {
|
||||
var data []byte
|
||||
var err error
|
||||
const indentJSON = false // for easier debugging
|
||||
if indentJSON {
|
||||
data, err = json.MarshalIndent(x, "", " ")
|
||||
} else {
|
||||
data, err = json.Marshal(x)
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("json.Marshal failed: %s", err))
|
||||
}
|
||||
return data
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
// TestIgnoredGoFiles tests the scenario where a folder has no .go or .c files,
|
||||
// but has an ignored go file.
|
||||
func TestIgnoredGoFiles(t *testing.T) {
|
||||
packagePath := "github.com/package"
|
||||
packageComment := "main is documented in an ignored .go file"
|
||||
|
||||
c := NewCorpus(mapfs.New(map[string]string{
|
||||
"src/" + packagePath + "/ignored.go": `// +build ignore
|
||||
|
||||
// ` + packageComment + `
|
||||
package main`}))
|
||||
srv := &handlerServer{
|
||||
p: &Presentation{
|
||||
Corpus: c,
|
||||
},
|
||||
c: c,
|
||||
}
|
||||
pInfo := srv.GetPageInfo("/src/"+packagePath, packagePath, NoFiltering, "linux", "amd64")
|
||||
|
||||
if pInfo.PDoc == nil {
|
||||
t.Error("pInfo.PDoc = nil; want non-nil.")
|
||||
} else {
|
||||
if got, want := pInfo.PDoc.Doc, packageComment+"\n"; got != want {
|
||||
t.Errorf("pInfo.PDoc.Doc = %q; want %q.", got, want)
|
||||
}
|
||||
if got, want := pInfo.PDoc.Name, "main"; got != want {
|
||||
t.Errorf("pInfo.PDoc.Name = %q; want %q.", got, want)
|
||||
}
|
||||
if got, want := pInfo.PDoc.ImportPath, packagePath; got != want {
|
||||
t.Errorf("pInfo.PDoc.ImportPath = %q; want %q.", got, want)
|
||||
}
|
||||
}
|
||||
if pInfo.FSet == nil {
|
||||
t.Error("pInfo.FSet = nil; want non-nil.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build golangorg
|
||||
|
||||
// Package short implements a simple URL shortener, serving an administrative
|
||||
// interface at /s and shortened urls from /s/key.
|
||||
// It is designed to run only on the instance of godoc that serves golang.org.
|
||||
package short
|
||||
|
||||
// TODO(adg): collect statistics on URL visits
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"cloud.google.com/go/datastore"
|
||||
"golang.org/x/website/internal/memcache"
|
||||
"google.golang.org/appengine/user"
|
||||
)
|
||||
|
||||
const (
|
||||
prefix = "/s"
|
||||
kind = "Link"
|
||||
baseURL = "https://golang.org" + prefix
|
||||
)
|
||||
|
||||
// Link represents a short link.
|
||||
type Link struct {
|
||||
Key, Target string
|
||||
}
|
||||
|
||||
var validKey = regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`)
|
||||
|
||||
type server struct {
|
||||
datastore *datastore.Client
|
||||
memcache *memcache.CodecClient
|
||||
}
|
||||
|
||||
func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
|
||||
s := server{dc, mc.WithCodec(memcache.JSON)}
|
||||
mux.HandleFunc(prefix+"/", s.linkHandler)
|
||||
|
||||
// TODO(cbro): move storage of the links to a text file in Gerrit.
|
||||
// Disable the admin handler until that happens, since GAE Flex doesn't support
|
||||
// the "google.golang.org/appengine/user" package.
|
||||
// See golang.org/issue/27205#issuecomment-418673218
|
||||
// mux.HandleFunc(prefix, adminHandler)
|
||||
mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
io.WriteString(w, "Link creation temporarily unavailable. See golang.org/issue/27205.")
|
||||
})
|
||||
}
|
||||
|
||||
// linkHandler services requests to short URLs.
|
||||
// http://golang.org/s/key
|
||||
// It consults memcache and datastore for the Link for key.
|
||||
// It then sends a redirects or an error message.
|
||||
func (h server) linkHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
key := r.URL.Path[len(prefix)+1:]
|
||||
if !validKey.MatchString(key) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var link Link
|
||||
if err := h.memcache.Get(ctx, cacheKey(key), &link); err != nil {
|
||||
k := datastore.NameKey(kind, key, nil)
|
||||
err = h.datastore.Get(ctx, k, &link)
|
||||
switch err {
|
||||
case datastore.ErrNoSuchEntity:
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
default: // != nil
|
||||
log.Printf("ERROR %q: %v", key, err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
case nil:
|
||||
item := &memcache.Item{
|
||||
Key: cacheKey(key),
|
||||
Object: &link,
|
||||
}
|
||||
if err := h.memcache.Set(ctx, item); err != nil {
|
||||
log.Printf("WARNING %q: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, link.Target, http.StatusFound)
|
||||
}
|
||||
|
||||
var adminTemplate = template.Must(template.New("admin").Parse(templateHTML))
|
||||
|
||||
// adminHandler serves an administrative interface.
|
||||
func (h server) adminHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if !user.IsAdmin(ctx) {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var newLink *Link
|
||||
var doErr error
|
||||
if r.Method == "POST" {
|
||||
key := r.FormValue("key")
|
||||
switch r.FormValue("do") {
|
||||
case "Add":
|
||||
newLink = &Link{key, r.FormValue("target")}
|
||||
doErr = h.putLink(ctx, newLink)
|
||||
case "Delete":
|
||||
k := datastore.NameKey(kind, key, nil)
|
||||
doErr = h.datastore.Delete(ctx, k)
|
||||
default:
|
||||
http.Error(w, "unknown action", http.StatusBadRequest)
|
||||
}
|
||||
err := h.memcache.Delete(ctx, cacheKey(key))
|
||||
if err != nil && err != memcache.ErrCacheMiss {
|
||||
log.Printf("WARNING %q: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
var links []*Link
|
||||
q := datastore.NewQuery(kind).Order("Key")
|
||||
if _, err := h.datastore.GetAll(ctx, q, &links); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Printf("ERROR %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Put the new link in the list if it's not there already.
|
||||
// (Eventual consistency means that it might not show up
|
||||
// immediately, which might be confusing for the user.)
|
||||
if newLink != nil && doErr == nil {
|
||||
found := false
|
||||
for i := range links {
|
||||
if links[i].Key == newLink.Key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
links = append([]*Link{newLink}, links...)
|
||||
}
|
||||
newLink = nil
|
||||
}
|
||||
|
||||
var data = struct {
|
||||
BaseURL string
|
||||
Prefix string
|
||||
Links []*Link
|
||||
New *Link
|
||||
Error error
|
||||
}{baseURL, prefix, links, newLink, doErr}
|
||||
if err := adminTemplate.Execute(w, &data); err != nil {
|
||||
log.Printf("ERROR adminTemplate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// putLink validates the provided link and puts it into the datastore.
|
||||
func (h server) putLink(ctx context.Context, link *Link) error {
|
||||
if !validKey.MatchString(link.Key) {
|
||||
return errors.New("invalid key; must match " + validKey.String())
|
||||
}
|
||||
if _, err := url.Parse(link.Target); err != nil {
|
||||
return fmt.Errorf("bad target: %v", err)
|
||||
}
|
||||
k := datastore.NameKey(kind, link.Key, nil)
|
||||
_, err := h.datastore.Put(ctx, k, link)
|
||||
return err
|
||||
}
|
||||
|
||||
// cacheKey returns a short URL key as a memcache key.
|
||||
func cacheKey(key string) string {
|
||||
return "link-" + key
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package short
|
||||
|
||||
const templateHTML = `
|
||||
<!doctype HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>golang.org URL shortener</title>
|
||||
<style>
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
input {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
input[type=text] {
|
||||
width: 400px;
|
||||
}
|
||||
input, td, th {
|
||||
color: #333;
|
||||
font-family: Georgia, Times New Roman, serif;
|
||||
}
|
||||
input, td {
|
||||
font-size: 14pt;
|
||||
}
|
||||
th {
|
||||
font-size: 16pt;
|
||||
text-align: left;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.autoselect {
|
||||
border: none;
|
||||
}
|
||||
.error {
|
||||
color: #900;
|
||||
}
|
||||
table {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<table>
|
||||
|
||||
{{with .Error}}
|
||||
<tr>
|
||||
<th colspan="3">Error</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="error" colspan="3">{{.}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Target</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<form method="POST" action="{{.Prefix}}">
|
||||
<tr>
|
||||
<td><input type="text" name="key"{{with .New}} value="{{.Key}}"{{end}}></td>
|
||||
<td><input type="text" name="target"{{with .New}} value="{{.Target}}"{{end}}></td>
|
||||
<td><input type="submit" name="do" value="Add">
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{{with .Links}}
|
||||
<tr>
|
||||
<th>Short Link</th>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td><input class="autoselect" type="text" orig="{{$.BaseURL}}/{{.Key}}" value="{{$.BaseURL}}/{{.Key}}"></td>
|
||||
<td><input class="autoselect" type="text" orig="{{.Target}}" value="{{.Target}}"></td>
|
||||
<td>
|
||||
<form method="POST" action="{{$.Prefix}}">
|
||||
<input type="hidden" name="key" value="{{.Key}}">
|
||||
<input type="submit" name="do" value="Delete" class="delete">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
</table>
|
||||
|
||||
</body>
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
|
||||
<script type="text/javascript">window.jQuery || document.write(unescape("%3Cscript src='/doc/jquery.js' type='text/javascript'%3E%3C/script%3E"));</script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.autoselect').each(function() {
|
||||
$(this).click(function() {
|
||||
$(this).select();
|
||||
});
|
||||
$(this).change(function() {
|
||||
$(this).val($(this).attr('orig'));
|
||||
});
|
||||
});
|
||||
$('.delete').click(function(e) {
|
||||
var link = $(this).closest('tr').find('input').first().val();
|
||||
var ok = confirm('Delete this link?\n' + link);
|
||||
if (!ok) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
`
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
// This file contains the infrastructure to create a code
|
||||
// snippet for search results.
|
||||
//
|
||||
// Note: At the moment, this only creates HTML snippets.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
type Snippet struct {
|
||||
Line int
|
||||
Text string // HTML-escaped
|
||||
}
|
||||
|
||||
func (p *Presentation) newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
||||
// TODO instead of pretty-printing the node, should use the original source instead
|
||||
var buf1 bytes.Buffer
|
||||
p.writeNode(&buf1, nil, fset, decl)
|
||||
// wrap text with <pre> tag
|
||||
var buf2 bytes.Buffer
|
||||
buf2.WriteString("<pre>")
|
||||
FormatText(&buf2, buf1.Bytes(), -1, true, id.Name, nil)
|
||||
buf2.WriteString("</pre>")
|
||||
return &Snippet{fset.Position(id.Pos()).Line, buf2.String()}
|
||||
}
|
||||
|
||||
func findSpec(list []ast.Spec, id *ast.Ident) ast.Spec {
|
||||
for _, spec := range list {
|
||||
switch s := spec.(type) {
|
||||
case *ast.ImportSpec:
|
||||
if s.Name == id {
|
||||
return s
|
||||
}
|
||||
case *ast.ValueSpec:
|
||||
for _, n := range s.Names {
|
||||
if n == id {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case *ast.TypeSpec:
|
||||
if s.Name == id {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Presentation) genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
|
||||
s := findSpec(d.Specs, id)
|
||||
if s == nil {
|
||||
return nil // declaration doesn't contain id - exit gracefully
|
||||
}
|
||||
|
||||
// only use the spec containing the id for the snippet
|
||||
dd := &ast.GenDecl{
|
||||
Doc: d.Doc,
|
||||
TokPos: d.Pos(),
|
||||
Tok: d.Tok,
|
||||
Lparen: d.Lparen,
|
||||
Specs: []ast.Spec{s},
|
||||
Rparen: d.Rparen,
|
||||
}
|
||||
|
||||
return p.newSnippet(fset, dd, id)
|
||||
}
|
||||
|
||||
func (p *Presentation) funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
|
||||
if d.Name != id {
|
||||
return nil // declaration doesn't contain id - exit gracefully
|
||||
}
|
||||
|
||||
// only use the function signature for the snippet
|
||||
dd := &ast.FuncDecl{
|
||||
Doc: d.Doc,
|
||||
Recv: d.Recv,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
}
|
||||
|
||||
return p.newSnippet(fset, dd, id)
|
||||
}
|
||||
|
||||
// NewSnippet creates a text snippet from a declaration decl containing an
|
||||
// identifier id. Parts of the declaration not containing the identifier
|
||||
// may be removed for a more compact snippet.
|
||||
func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
||||
// TODO(bradfitz, adg): remove this function. But it's used by indexer, which
|
||||
// doesn't have a *Presentation, and NewSnippet needs a TabWidth.
|
||||
var p Presentation
|
||||
p.TabWidth = 4
|
||||
return p.NewSnippet(fset, decl, id)
|
||||
}
|
||||
|
||||
// NewSnippet creates a text snippet from a declaration decl containing an
|
||||
// identifier id. Parts of the declaration not containing the identifier
|
||||
// may be removed for a more compact snippet.
|
||||
func (p *Presentation) NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
||||
var s *Snippet
|
||||
switch d := decl.(type) {
|
||||
case *ast.GenDecl:
|
||||
s = p.genSnippet(fset, d, id)
|
||||
case *ast.FuncDecl:
|
||||
s = p.funcSnippet(fset, d, id)
|
||||
}
|
||||
|
||||
// handle failure gracefully
|
||||
if s == nil {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, `<span class="alert">could not generate a snippet for <span class="highlight">%s</span></span>`, id.Name)
|
||||
s = &Snippet{fset.Position(id.Pos()).Line, buf.String()}
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
package godoc
|
||||
|
||||
// This file contains the mechanism to "linkify" html source
|
||||
// text containing EBNF sections (as found in go_spec.html).
|
||||
// The result is the input source text with the EBNF sections
|
||||
// modified such that identifiers are linked to the respective
|
||||
// definitions.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"text/scanner"
|
||||
)
|
||||
|
||||
type ebnfParser struct {
|
||||
out io.Writer // parser output
|
||||
src []byte // parser input
|
||||
scanner scanner.Scanner
|
||||
prev int // offset of previous token
|
||||
pos int // offset of current token
|
||||
tok rune // one token look-ahead
|
||||
lit string // token literal
|
||||
}
|
||||
|
||||
func (p *ebnfParser) flush() {
|
||||
p.out.Write(p.src[p.prev:p.pos])
|
||||
p.prev = p.pos
|
||||
}
|
||||
|
||||
func (p *ebnfParser) next() {
|
||||
p.tok = p.scanner.Scan()
|
||||
p.pos = p.scanner.Position.Offset
|
||||
p.lit = p.scanner.TokenText()
|
||||
}
|
||||
|
||||
func (p *ebnfParser) printf(format string, args ...interface{}) {
|
||||
p.flush()
|
||||
fmt.Fprintf(p.out, format, args...)
|
||||
}
|
||||
|
||||
func (p *ebnfParser) errorExpected(msg string) {
|
||||
p.printf(`<span class="highlight">error: expected %s, found %s</span>`, msg, scanner.TokenString(p.tok))
|
||||
}
|
||||
|
||||
func (p *ebnfParser) expect(tok rune) {
|
||||
if p.tok != tok {
|
||||
p.errorExpected(scanner.TokenString(tok))
|
||||
}
|
||||
p.next() // make progress in any case
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseIdentifier(def bool) {
|
||||
if p.tok == scanner.Ident {
|
||||
name := p.lit
|
||||
if def {
|
||||
p.printf(`<a id="%s">%s</a>`, name, name)
|
||||
} else {
|
||||
p.printf(`<a href="#%s" class="noline">%s</a>`, name, name)
|
||||
}
|
||||
p.prev += len(name) // skip identifier when printing next time
|
||||
p.next()
|
||||
} else {
|
||||
p.expect(scanner.Ident)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseTerm() bool {
|
||||
switch p.tok {
|
||||
case scanner.Ident:
|
||||
p.parseIdentifier(false)
|
||||
|
||||
case scanner.String, scanner.RawString:
|
||||
p.next()
|
||||
const ellipsis = '…' // U+2026, the horizontal ellipsis character
|
||||
if p.tok == ellipsis {
|
||||
p.next()
|
||||
p.expect(scanner.String)
|
||||
}
|
||||
|
||||
case '(':
|
||||
p.next()
|
||||
p.parseExpression()
|
||||
p.expect(')')
|
||||
|
||||
case '[':
|
||||
p.next()
|
||||
p.parseExpression()
|
||||
p.expect(']')
|
||||
|
||||
case '{':
|
||||
p.next()
|
||||
p.parseExpression()
|
||||
p.expect('}')
|
||||
|
||||
default:
|
||||
return false // no term found
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseSequence() {
|
||||
if !p.parseTerm() {
|
||||
p.errorExpected("term")
|
||||
}
|
||||
for p.parseTerm() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseExpression() {
|
||||
for {
|
||||
p.parseSequence()
|
||||
if p.tok != '|' {
|
||||
break
|
||||
}
|
||||
p.next()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parseProduction() {
|
||||
p.parseIdentifier(true)
|
||||
p.expect('=')
|
||||
if p.tok != '.' {
|
||||
p.parseExpression()
|
||||
}
|
||||
p.expect('.')
|
||||
}
|
||||
|
||||
func (p *ebnfParser) parse(out io.Writer, src []byte) {
|
||||
// initialize ebnfParser
|
||||
p.out = out
|
||||
p.src = src
|
||||
p.scanner.Init(bytes.NewBuffer(src))
|
||||
p.next() // initializes pos, tok, lit
|
||||
|
||||
// process source
|
||||
for p.tok != scanner.EOF {
|
||||
p.parseProduction()
|
||||
}
|
||||
p.flush()
|
||||
}
|
||||
|
||||
// Markers around EBNF sections
|
||||
var (
|
||||
openTag = []byte(`<pre class="ebnf">`)
|
||||
closeTag = []byte(`</pre>`)
|
||||
)
|
||||
|
||||
func Linkify(out io.Writer, src []byte) {
|
||||
for len(src) > 0 {
|
||||
// i: beginning of EBNF text (or end of source)
|
||||
i := bytes.Index(src, openTag)
|
||||
if i < 0 {
|
||||
i = len(src) - len(openTag)
|
||||
}
|
||||
i += len(openTag)
|
||||
|
||||
// j: end of EBNF text (or end of source)
|
||||
j := bytes.Index(src[i:], closeTag) // close marker
|
||||
if j < 0 {
|
||||
j = len(src) - i
|
||||
}
|
||||
j += i
|
||||
|
||||
// write text before EBNF
|
||||
out.Write(src[0:i])
|
||||
// process EBNF
|
||||
var p ebnfParser
|
||||
p.parse(out, src[i:j])
|
||||
|
||||
// advance
|
||||
src = src[j:]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEBNFString(t *testing.T) {
|
||||
var p ebnfParser
|
||||
var buf bytes.Buffer
|
||||
src := []byte("octal_byte_value = `\\` octal_digit octal_digit octal_digit .")
|
||||
p.parse(&buf, src)
|
||||
|
||||
if strings.Contains(buf.String(), "error") {
|
||||
t.Error(buf.String())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// 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.
|
||||
|
||||
package godoc
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SpotInfo
|
||||
|
||||
// A SpotInfo value describes a particular identifier spot in a given file;
|
||||
// It encodes three values: the SpotKind (declaration or use), a line or
|
||||
// snippet index "lori", and whether it's a line or index.
|
||||
//
|
||||
// The following encoding is used:
|
||||
//
|
||||
// bits 32 4 1 0
|
||||
// value [lori|kind|isIndex]
|
||||
//
|
||||
type SpotInfo uint32
|
||||
|
||||
// SpotKind describes whether an identifier is declared (and what kind of
|
||||
// declaration) or used.
|
||||
type SpotKind uint32
|
||||
|
||||
const (
|
||||
PackageClause SpotKind = iota
|
||||
ImportDecl
|
||||
ConstDecl
|
||||
TypeDecl
|
||||
VarDecl
|
||||
FuncDecl
|
||||
MethodDecl
|
||||
Use
|
||||
nKinds
|
||||
)
|
||||
|
||||
var (
|
||||
// These must match the SpotKind values above.
|
||||
name = []string{
|
||||
"Packages",
|
||||
"Imports",
|
||||
"Constants",
|
||||
"Types",
|
||||
"Variables",
|
||||
"Functions",
|
||||
"Methods",
|
||||
"Uses",
|
||||
"Unknown",
|
||||
}
|
||||
)
|
||||
|
||||
func (x SpotKind) Name() string { return name[x] }
|
||||
|
||||
func init() {
|
||||
// sanity check: if nKinds is too large, the SpotInfo
|
||||
// accessor functions may need to be updated
|
||||
if nKinds > 8 {
|
||||
panic("internal error: nKinds > 8")
|
||||
}
|
||||
}
|
||||
|
||||
// makeSpotInfo makes a SpotInfo.
|
||||
func makeSpotInfo(kind SpotKind, lori int, isIndex bool) SpotInfo {
|
||||
// encode lori: bits [4..32)
|
||||
x := SpotInfo(lori) << 4
|
||||
if int(x>>4) != lori {
|
||||
// lori value doesn't fit - since snippet indices are
|
||||
// most certainly always smaller then 1<<28, this can
|
||||
// only happen for line numbers; give it no line number (= 0)
|
||||
x = 0
|
||||
}
|
||||
// encode kind: bits [1..4)
|
||||
x |= SpotInfo(kind) << 1
|
||||
// encode isIndex: bit 0
|
||||
if isIndex {
|
||||
x |= 1
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (x SpotInfo) Kind() SpotKind { return SpotKind(x >> 1 & 7) }
|
||||
func (x SpotInfo) Lori() int { return int(x >> 4) }
|
||||
func (x SpotInfo) IsIndex() bool { return x&1 != 0 }
|
|
@ -0,0 +1,82 @@
|
|||
// 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.
|
||||
|
||||
// TODO(bradfitz,adg): move to util
|
||||
|
||||
package godoc
|
||||
|
||||
import "io"
|
||||
|
||||
var spaces = []byte(" ") // 32 spaces seems like a good number
|
||||
|
||||
const (
|
||||
indenting = iota
|
||||
collecting
|
||||
)
|
||||
|
||||
// A tconv is an io.Writer filter for converting leading tabs into spaces.
|
||||
type tconv struct {
|
||||
output io.Writer
|
||||
state int // indenting or collecting
|
||||
indent int // valid if state == indenting
|
||||
p *Presentation
|
||||
}
|
||||
|
||||
func (p *tconv) writeIndent() (err error) {
|
||||
i := p.indent
|
||||
for i >= len(spaces) {
|
||||
i -= len(spaces)
|
||||
if _, err = p.output.Write(spaces); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// i < len(spaces)
|
||||
if i > 0 {
|
||||
_, err = p.output.Write(spaces[0:i])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *tconv) Write(data []byte) (n int, err error) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
pos := 0 // valid if p.state == collecting
|
||||
var b byte
|
||||
for n, b = range data {
|
||||
switch p.state {
|
||||
case indenting:
|
||||
switch b {
|
||||
case '\t':
|
||||
p.indent += p.p.TabWidth
|
||||
case '\n':
|
||||
p.indent = 0
|
||||
if _, err = p.output.Write(data[n : n+1]); err != nil {
|
||||
return
|
||||
}
|
||||
case ' ':
|
||||
p.indent++
|
||||
default:
|
||||
p.state = collecting
|
||||
pos = n
|
||||
if err = p.writeIndent(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case collecting:
|
||||
if b == '\n' {
|
||||
p.state = indenting
|
||||
p.indent = 0
|
||||
if _, err = p.output.Write(data[pos : n+1]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
n = len(data)
|
||||
if pos < n && p.state == collecting {
|
||||
_, err = p.output.Write(data[pos:])
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// Template support for writing HTML documents.
|
||||
// Documents that include Template: true in their
|
||||
// metadata are executed as input to text/template.
|
||||
//
|
||||
// This file defines functions for those templates to invoke.
|
||||
|
||||
// The template uses the function "code" to inject program
|
||||
// source into the output by extracting code from files and
|
||||
// injecting them as HTML-escaped <pre> blocks.
|
||||
//
|
||||
// The syntax is simple: 1, 2, or 3 space-separated arguments:
|
||||
//
|
||||
// Whole file:
|
||||
// {{code "foo.go"}}
|
||||
// One line (here the signature of main):
|
||||
// {{code "foo.go" `/^func.main/`}}
|
||||
// Block of text, determined by start and end (here the body of main):
|
||||
// {{code "foo.go" `/^func.main/` `/^}/`
|
||||
//
|
||||
// Patterns can be `/regular expression/`, a decimal number, or "$"
|
||||
// to signify the end of the file. In multi-line matches,
|
||||
// lines that end with the four characters
|
||||
// OMIT
|
||||
// are omitted from the output, making it easy to provide marker
|
||||
// lines in the input that will not appear in the output but are easy
|
||||
// to identify by pattern.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// Functions in this file panic on error, but the panic is recovered
|
||||
// to an error by 'code'.
|
||||
|
||||
// contents reads and returns the content of the named file
|
||||
// (from the virtual file system, so for example /doc refers to $GOROOT/doc).
|
||||
func (c *Corpus) contents(name string) string {
|
||||
file, err := vfs.ReadFile(c.fs, name)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return string(file)
|
||||
}
|
||||
|
||||
// stringFor returns a textual representation of the arg, formatted according to its nature.
|
||||
func stringFor(arg interface{}) string {
|
||||
switch arg := arg.(type) {
|
||||
case int:
|
||||
return fmt.Sprintf("%d", arg)
|
||||
case string:
|
||||
if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
|
||||
return fmt.Sprintf("%#q", arg)
|
||||
}
|
||||
return fmt.Sprintf("%q", arg)
|
||||
default:
|
||||
log.Panicf("unrecognized argument: %v type %T", arg, arg)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
text := p.Corpus.contents(file)
|
||||
var command string
|
||||
switch len(arg) {
|
||||
case 0:
|
||||
// text is already whole file.
|
||||
command = fmt.Sprintf("code %q", file)
|
||||
case 1:
|
||||
command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
|
||||
text = p.Corpus.oneLine(file, text, arg[0])
|
||||
case 2:
|
||||
command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
|
||||
text = p.Corpus.multipleLines(file, text, arg[0], arg[1])
|
||||
default:
|
||||
return "", fmt.Errorf("incorrect code invocation: code %q [%v, ...] (%d arguments)", file, arg[0], len(arg))
|
||||
}
|
||||
// Trim spaces from output.
|
||||
text = strings.Trim(text, "\n")
|
||||
// Replace tabs by spaces, which work better in HTML.
|
||||
text = strings.Replace(text, "\t", " ", -1)
|
||||
var buf bytes.Buffer
|
||||
// HTML-escape text and syntax-color comments like elsewhere.
|
||||
FormatText(&buf, []byte(text), -1, true, "", nil)
|
||||
// Include the command as a comment.
|
||||
text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// parseArg returns the integer or string value of the argument and tells which it is.
|
||||
func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) {
|
||||
switch n := arg.(type) {
|
||||
case int:
|
||||
if n <= 0 || n > max {
|
||||
log.Panicf("%q:%d is out of range", file, n)
|
||||
}
|
||||
return n, "", true
|
||||
case string:
|
||||
return 0, n, false
|
||||
}
|
||||
log.Panicf("unrecognized argument %v type %T", arg, arg)
|
||||
return
|
||||
}
|
||||
|
||||
// oneLine returns the single line generated by a two-argument code invocation.
|
||||
func (c *Corpus) oneLine(file, text string, arg interface{}) string {
|
||||
lines := strings.SplitAfter(c.contents(file), "\n")
|
||||
line, pattern, isInt := parseArg(arg, file, len(lines))
|
||||
if isInt {
|
||||
return lines[line-1]
|
||||
}
|
||||
return lines[match(file, 0, lines, pattern)-1]
|
||||
}
|
||||
|
||||
// multipleLines returns the text generated by a three-argument code invocation.
|
||||
func (c *Corpus) multipleLines(file, text string, arg1, arg2 interface{}) string {
|
||||
lines := strings.SplitAfter(c.contents(file), "\n")
|
||||
line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
|
||||
line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
|
||||
if !isInt1 {
|
||||
line1 = match(file, 0, lines, pattern1)
|
||||
}
|
||||
if !isInt2 {
|
||||
line2 = match(file, line1, lines, pattern2)
|
||||
} else if line2 < line1 {
|
||||
log.Panicf("lines out of order for %q: %d %d", text, line1, line2)
|
||||
}
|
||||
for k := line1 - 1; k < line2; k++ {
|
||||
if strings.HasSuffix(lines[k], "OMIT\n") {
|
||||
lines[k] = ""
|
||||
}
|
||||
}
|
||||
return strings.Join(lines[line1-1:line2], "")
|
||||
}
|
||||
|
||||
// match identifies the input line that matches the pattern in a code invocation.
|
||||
// If start>0, match lines starting there rather than at the beginning.
|
||||
// The return value is 1-indexed.
|
||||
func match(file string, start int, lines []string, pattern string) int {
|
||||
// $ matches the end of the file.
|
||||
if pattern == "$" {
|
||||
if len(lines) == 0 {
|
||||
log.Panicf("%q: empty file", file)
|
||||
}
|
||||
return len(lines)
|
||||
}
|
||||
// /regexp/ matches the line that matches the regexp.
|
||||
if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
|
||||
re, err := regexp.Compile(pattern[1 : len(pattern)-1])
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
for i := start; i < len(lines); i++ {
|
||||
if re.MatchString(lines[i]) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
log.Panicf("%s: no match for %#q", file, pattern)
|
||||
}
|
||||
log.Panicf("unrecognized pattern: %q", pattern)
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
package util
|
||||
|
||||
import "time"
|
||||
|
||||
// A Throttle permits throttling of a goroutine by
|
||||
// calling the Throttle method repeatedly.
|
||||
//
|
||||
type Throttle struct {
|
||||
f float64 // f = (1-r)/r for 0 < r < 1
|
||||
dt time.Duration // minimum run time slice; >= 0
|
||||
tr time.Duration // accumulated time running
|
||||
ts time.Duration // accumulated time stopped
|
||||
tt time.Time // earliest throttle time (= time Throttle returned + tm)
|
||||
}
|
||||
|
||||
// NewThrottle creates a new Throttle with a throttle value r and
|
||||
// a minimum allocated run time slice of dt:
|
||||
//
|
||||
// r == 0: "empty" throttle; the goroutine is always sleeping
|
||||
// r == 1: full throttle; the goroutine is never sleeping
|
||||
//
|
||||
// A value of r == 0.6 throttles a goroutine such that it runs
|
||||
// approx. 60% of the time, and sleeps approx. 40% of the time.
|
||||
// Values of r < 0 or r > 1 are clamped down to values between 0 and 1.
|
||||
// Values of dt < 0 are set to 0.
|
||||
//
|
||||
func NewThrottle(r float64, dt time.Duration) *Throttle {
|
||||
var f float64
|
||||
switch {
|
||||
case r <= 0:
|
||||
f = -1 // indicates always sleep
|
||||
case r >= 1:
|
||||
f = 0 // assume r == 1 (never sleep)
|
||||
default:
|
||||
// 0 < r < 1
|
||||
f = (1 - r) / r
|
||||
}
|
||||
if dt < 0 {
|
||||
dt = 0
|
||||
}
|
||||
return &Throttle{f: f, dt: dt, tt: time.Now().Add(dt)}
|
||||
}
|
||||
|
||||
// Throttle calls time.Sleep such that over time the ratio tr/ts between
|
||||
// accumulated run (tr) and sleep times (ts) approximates the value 1/(1-r)
|
||||
// where r is the throttle value. Throttle returns immediately (w/o sleeping)
|
||||
// if less than tm ns have passed since the last call to Throttle.
|
||||
//
|
||||
func (p *Throttle) Throttle() {
|
||||
if p.f < 0 {
|
||||
select {} // always sleep
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
if t0.Before(p.tt) {
|
||||
return // keep running (minimum time slice not exhausted yet)
|
||||
}
|
||||
|
||||
// accumulate running time
|
||||
p.tr += t0.Sub(p.tt) + p.dt
|
||||
|
||||
// compute sleep time
|
||||
// Over time we want:
|
||||
//
|
||||
// tr/ts = r/(1-r)
|
||||
//
|
||||
// Thus:
|
||||
//
|
||||
// ts = tr*f with f = (1-r)/r
|
||||
//
|
||||
// After some incremental run time δr added to the total run time
|
||||
// tr, the incremental sleep-time δs to get to the same ratio again
|
||||
// after waking up from time.Sleep is:
|
||||
if δs := time.Duration(float64(p.tr)*p.f) - p.ts; δs > 0 {
|
||||
time.Sleep(δs)
|
||||
}
|
||||
|
||||
// accumulate (actual) sleep time
|
||||
t1 := time.Now()
|
||||
p.ts += t1.Sub(t0)
|
||||
|
||||
// set earliest next throttle time
|
||||
p.tt = t1.Add(p.dt)
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
// 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.
|
||||
|
||||
// Package util contains utility types and functions for godoc.
|
||||
package util // import "golang.org/x/website/cmd/golangorg/godoc/util"
|
||||
|
||||
import (
|
||||
pathpkg "path"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// An RWValue wraps a value and permits mutually exclusive
|
||||
// access to it and records the time the value was last set.
|
||||
type RWValue struct {
|
||||
mutex sync.RWMutex
|
||||
value interface{}
|
||||
timestamp time.Time // time of last set()
|
||||
}
|
||||
|
||||
func (v *RWValue) Set(value interface{}) {
|
||||
v.mutex.Lock()
|
||||
v.value = value
|
||||
v.timestamp = time.Now()
|
||||
v.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (v *RWValue) Get() (interface{}, time.Time) {
|
||||
v.mutex.RLock()
|
||||
defer v.mutex.RUnlock()
|
||||
return v.value, v.timestamp
|
||||
}
|
||||
|
||||
// IsText reports whether a significant prefix of s looks like correct UTF-8;
|
||||
// that is, if it is likely that s is human-readable text.
|
||||
func IsText(s []byte) bool {
|
||||
const max = 1024 // at least utf8.UTFMax
|
||||
if len(s) > max {
|
||||
s = s[0:max]
|
||||
}
|
||||
for i, c := range string(s) {
|
||||
if i+utf8.UTFMax > len(s) {
|
||||
// last char may be incomplete - ignore
|
||||
break
|
||||
}
|
||||
if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' {
|
||||
// decoding error or control character - not a text file
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// textExt[x] is true if the extension x indicates a text file, and false otherwise.
|
||||
var textExt = map[string]bool{
|
||||
".css": false, // must be served raw
|
||||
".js": false, // must be served raw
|
||||
}
|
||||
|
||||
// IsTextFile reports whether the file has a known extension indicating
|
||||
// a text file, or if a significant chunk of the specified file looks like
|
||||
// correct UTF-8; that is, if it is likely that the file contains human-
|
||||
// readable text.
|
||||
func IsTextFile(fs vfs.Opener, filename string) bool {
|
||||
// if the extension is known, use it for decision making
|
||||
if isText, found := textExt[pathpkg.Ext(filename)]; found {
|
||||
return isText
|
||||
}
|
||||
|
||||
// the extension is not known; read an initial chunk
|
||||
// of the file and check if it looks like text
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var buf [1024]byte
|
||||
n, err := f.Read(buf[0:])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsText(buf[0:n])
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// This file caches information about which standard library types, methods,
|
||||
// and functions appeared in what version of Go
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// apiVersions is a map of packages to information about those packages'
|
||||
// symbols and when they were added to Go.
|
||||
//
|
||||
// Only things added after Go1 are tracked. Version strings are of the
|
||||
// form "1.1", "1.2", etc.
|
||||
type apiVersions map[string]pkgAPIVersions // keyed by Go package ("net/http")
|
||||
|
||||
// pkgAPIVersions contains information about which version of Go added
|
||||
// certain package symbols.
|
||||
//
|
||||
// Only things added after Go1 are tracked. Version strings are of the
|
||||
// form "1.1", "1.2", etc.
|
||||
type pkgAPIVersions struct {
|
||||
typeSince map[string]string // "Server" -> "1.7"
|
||||
methodSince map[string]map[string]string // "*Server" ->"Shutdown"->1.8
|
||||
funcSince map[string]string // "NewServer" -> "1.7"
|
||||
fieldSince map[string]map[string]string // "ClientTrace" -> "Got1xxResponse" -> "1.11"
|
||||
}
|
||||
|
||||
// sinceVersionFunc returns a string (such as "1.7") specifying which Go
|
||||
// version introduced a symbol, unless it was introduced in Go1, in
|
||||
// which case it returns the empty string.
|
||||
//
|
||||
// The kind is one of "type", "method", or "func".
|
||||
//
|
||||
// The receiver is only used for "methods" and specifies the receiver type,
|
||||
// such as "*Server".
|
||||
//
|
||||
// The name is the symbol name ("Server") and the pkg is the package
|
||||
// ("net/http").
|
||||
func (v apiVersions) sinceVersionFunc(kind, receiver, name, pkg string) string {
|
||||
pv := v[pkg]
|
||||
switch kind {
|
||||
case "func":
|
||||
return pv.funcSince[name]
|
||||
case "type":
|
||||
return pv.typeSince[name]
|
||||
case "method":
|
||||
return pv.methodSince[receiver][name]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// versionedRow represents an API feature, a parsed line of a
|
||||
// $GOROOT/api/go.*txt file.
|
||||
type versionedRow struct {
|
||||
pkg string // "net/http"
|
||||
kind string // "type", "func", "method", "field" TODO: "const", "var"
|
||||
recv string // for methods, the receiver type ("Server", "*Server")
|
||||
name string // name of type, (struct) field, func, method
|
||||
structName string // for struct fields, the outer struct name
|
||||
}
|
||||
|
||||
// versionParser parses $GOROOT/api/go*.txt files and stores them in in its rows field.
|
||||
type versionParser struct {
|
||||
res apiVersions // initialized lazily
|
||||
}
|
||||
|
||||
func (vp *versionParser) parseFile(name string) error {
|
||||
base := filepath.Base(name)
|
||||
ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go")
|
||||
if ver == "1" {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
row, ok := parseRow(sc.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if vp.res == nil {
|
||||
vp.res = make(apiVersions)
|
||||
}
|
||||
pkgi, ok := vp.res[row.pkg]
|
||||
if !ok {
|
||||
pkgi = pkgAPIVersions{
|
||||
typeSince: make(map[string]string),
|
||||
methodSince: make(map[string]map[string]string),
|
||||
funcSince: make(map[string]string),
|
||||
fieldSince: make(map[string]map[string]string),
|
||||
}
|
||||
vp.res[row.pkg] = pkgi
|
||||
}
|
||||
switch row.kind {
|
||||
case "func":
|
||||
pkgi.funcSince[row.name] = ver
|
||||
case "type":
|
||||
pkgi.typeSince[row.name] = ver
|
||||
case "method":
|
||||
if _, ok := pkgi.methodSince[row.recv]; !ok {
|
||||
pkgi.methodSince[row.recv] = make(map[string]string)
|
||||
}
|
||||
pkgi.methodSince[row.recv][row.name] = ver
|
||||
case "field":
|
||||
if _, ok := pkgi.fieldSince[row.structName]; !ok {
|
||||
pkgi.fieldSince[row.structName] = make(map[string]string)
|
||||
}
|
||||
pkgi.fieldSince[row.structName][row.name] = ver
|
||||
}
|
||||
}
|
||||
return sc.Err()
|
||||
}
|
||||
|
||||
func parseRow(s string) (vr versionedRow, ok bool) {
|
||||
if !strings.HasPrefix(s, "pkg ") {
|
||||
// Skip comments, blank lines, etc.
|
||||
return
|
||||
}
|
||||
rest := s[len("pkg "):]
|
||||
endPkg := strings.IndexFunc(rest, func(r rune) bool { return !(unicode.IsLetter(r) || r == '/' || unicode.IsDigit(r)) })
|
||||
if endPkg == -1 {
|
||||
return
|
||||
}
|
||||
vr.pkg, rest = rest[:endPkg], rest[endPkg:]
|
||||
if !strings.HasPrefix(rest, ", ") {
|
||||
// If the part after the pkg name isn't ", ", then it's a OS/ARCH-dependent line of the form:
|
||||
// pkg syscall (darwin-amd64), const ImplementsGetwd = false
|
||||
// We skip those for now.
|
||||
return
|
||||
}
|
||||
rest = rest[len(", "):]
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(rest, "type "):
|
||||
rest = rest[len("type "):]
|
||||
sp := strings.IndexByte(rest, ' ')
|
||||
if sp == -1 {
|
||||
return
|
||||
}
|
||||
vr.name, rest = rest[:sp], rest[sp+1:]
|
||||
if !strings.HasPrefix(rest, "struct, ") {
|
||||
vr.kind = "type"
|
||||
return vr, true
|
||||
}
|
||||
rest = rest[len("struct, "):]
|
||||
if i := strings.IndexByte(rest, ' '); i != -1 {
|
||||
vr.kind = "field"
|
||||
vr.structName = vr.name
|
||||
vr.name = rest[:i]
|
||||
return vr, true
|
||||
}
|
||||
case strings.HasPrefix(rest, "func "):
|
||||
vr.kind = "func"
|
||||
rest = rest[len("func "):]
|
||||
if i := strings.IndexByte(rest, '('); i != -1 {
|
||||
vr.name = rest[:i]
|
||||
return vr, true
|
||||
}
|
||||
case strings.HasPrefix(rest, "method "): // "method (*File) SetModTime(time.Time)"
|
||||
vr.kind = "method"
|
||||
rest = rest[len("method "):] // "(*File) SetModTime(time.Time)"
|
||||
sp := strings.IndexByte(rest, ' ')
|
||||
if sp == -1 {
|
||||
return
|
||||
}
|
||||
vr.recv = strings.Trim(rest[:sp], "()") // "*File"
|
||||
rest = rest[sp+1:] // SetMode(os.FileMode)
|
||||
paren := strings.IndexByte(rest, '(')
|
||||
if paren == -1 {
|
||||
return
|
||||
}
|
||||
vr.name = rest[:paren]
|
||||
return vr, true
|
||||
}
|
||||
return // TODO: handle more cases
|
||||
}
|
||||
|
||||
// InitVersionInfo parses the $GOROOT/api/go*.txt API definition files to discover
|
||||
// which API features were added in which Go releases.
|
||||
func (c *Corpus) InitVersionInfo() {
|
||||
var err error
|
||||
c.pkgAPIInfo, err = parsePackageAPIInfo()
|
||||
if err != nil {
|
||||
// TODO: consider making this fatal, after the Go 1.11 cycle.
|
||||
log.Printf("godoc: error parsing API version files: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parsePackageAPIInfo() (apiVersions, error) {
|
||||
var apiGlob string
|
||||
if os.Getenv("GOROOT") == "" {
|
||||
apiGlob = filepath.Join(build.Default.GOROOT, "api", "go*.txt")
|
||||
} else {
|
||||
apiGlob = filepath.Join(os.Getenv("GOROOT"), "api", "go*.txt")
|
||||
}
|
||||
|
||||
files, err := filepath.Glob(apiGlob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vp := new(versionParser)
|
||||
for _, f := range files {
|
||||
if err := vp.parseFile(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return vp.res, nil
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package godoc
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVersionRow(t *testing.T) {
|
||||
tests := []struct {
|
||||
row string
|
||||
want versionedRow
|
||||
}{
|
||||
{
|
||||
row: "# comment",
|
||||
},
|
||||
{
|
||||
row: "",
|
||||
},
|
||||
{
|
||||
row: "pkg archive/tar, type Writer struct",
|
||||
want: versionedRow{
|
||||
pkg: "archive/tar",
|
||||
kind: "type",
|
||||
name: "Writer",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg archive/tar, type Header struct, AccessTime time.Time",
|
||||
want: versionedRow{
|
||||
pkg: "archive/tar",
|
||||
kind: "field",
|
||||
structName: "Header",
|
||||
name: "AccessTime",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg archive/tar, method (*Reader) Read([]uint8) (int, error)",
|
||||
want: versionedRow{
|
||||
pkg: "archive/tar",
|
||||
kind: "method",
|
||||
name: "Read",
|
||||
recv: "*Reader",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg archive/zip, func FileInfoHeader(os.FileInfo) (*FileHeader, error)",
|
||||
want: versionedRow{
|
||||
pkg: "archive/zip",
|
||||
kind: "func",
|
||||
name: "FileInfoHeader",
|
||||
},
|
||||
},
|
||||
{
|
||||
row: "pkg encoding/base32, method (Encoding) WithPadding(int32) *Encoding",
|
||||
want: versionedRow{
|
||||
pkg: "encoding/base32",
|
||||
kind: "method",
|
||||
name: "WithPadding",
|
||||
recv: "Encoding",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got, ok := parseRow(tt.row)
|
||||
if !ok {
|
||||
got = versionedRow{}
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("%d. parseRow(%q) = %+v; want %+v", i, tt.row, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasTag checks whether a given release tag is contained in the current version
|
||||
// of the go binary.
|
||||
func hasTag(t string) bool {
|
||||
for _, v := range build.Default.ReleaseTags {
|
||||
if t == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestAPIVersion(t *testing.T) {
|
||||
av, err := parsePackageAPIInfo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
kind string
|
||||
pkg string
|
||||
name string
|
||||
receiver string
|
||||
want string
|
||||
}{
|
||||
// Things that were added post-1.0 should appear
|
||||
{"func", "archive/tar", "FileInfoHeader", "", "1.1"},
|
||||
{"type", "bufio", "Scanner", "", "1.1"},
|
||||
{"method", "bufio", "WriteTo", "*Reader", "1.1"},
|
||||
|
||||
{"func", "bytes", "LastIndexByte", "", "1.5"},
|
||||
{"type", "crypto", "Decrypter", "", "1.5"},
|
||||
{"method", "crypto/rsa", "Decrypt", "*PrivateKey", "1.5"},
|
||||
{"method", "debug/dwarf", "GoString", "Class", "1.5"},
|
||||
|
||||
{"func", "os", "IsTimeout", "", "1.10"},
|
||||
{"type", "strings", "Builder", "", "1.10"},
|
||||
{"method", "strings", "WriteString", "*Builder", "1.10"},
|
||||
|
||||
// Things from package syscall should never appear
|
||||
{"func", "syscall", "FchFlags", "", ""},
|
||||
{"type", "syscall", "Inet4Pktinfo", "", ""},
|
||||
|
||||
// Things added in Go 1 should never appear
|
||||
{"func", "archive/tar", "NewReader", "", ""},
|
||||
{"type", "archive/tar", "Header", "", ""},
|
||||
{"method", "archive/tar", "Next", "*Reader", ""},
|
||||
} {
|
||||
if tc.want != "" && !hasTag("go"+tc.want) {
|
||||
continue
|
||||
}
|
||||
if got := av.sinceVersionFunc(tc.kind, tc.receiver, tc.name, tc.pkg); got != tc.want {
|
||||
t.Errorf(`sinceFunc("%s", "%s", "%s", "%s") = "%s"; want "%s"`, tc.kind, tc.receiver, tc.name, tc.pkg, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewNameSpace returns a NameSpace pre-initialized with an empty
|
||||
// emulated directory mounted on the root mount point "/". This
|
||||
// allows directory traversal routines to work properly even if
|
||||
// a folder is not explicitly mounted at root by the user.
|
||||
func NewNameSpace() NameSpace {
|
||||
ns := NameSpace{}
|
||||
ns.Bind("/", &emptyVFS{}, "/", BindReplace)
|
||||
return ns
|
||||
}
|
||||
|
||||
// type emptyVFS emulates a FileSystem consisting of an empty directory
|
||||
type emptyVFS struct{}
|
||||
|
||||
// Open implements Opener. Since emptyVFS is an empty directory, all
|
||||
// attempts to open a file should returns errors.
|
||||
func (e *emptyVFS) Open(path string) (ReadSeekCloser, error) {
|
||||
if path == "/" {
|
||||
return nil, fmt.Errorf("open: / is a directory")
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// Stat returns os.FileInfo for an empty directory if the path is
|
||||
// is root "/" or error. os.FileInfo is implemented by emptyVFS
|
||||
func (e *emptyVFS) Stat(path string) (os.FileInfo, error) {
|
||||
if path == "/" {
|
||||
return e, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Lstat(path string) (os.FileInfo, error) {
|
||||
return e.Stat(path)
|
||||
}
|
||||
|
||||
// ReadDir returns an empty os.FileInfo slice for "/", else error.
|
||||
func (e *emptyVFS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
if path == "/" {
|
||||
return []os.FileInfo{}, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (e *emptyVFS) String() string {
|
||||
return "emptyVFS(/)"
|
||||
}
|
||||
|
||||
func (e *emptyVFS) RootType(path string) RootType {
|
||||
return ""
|
||||
}
|
||||
|
||||
// These functions below implement os.FileInfo for the single
|
||||
// empty emulated directory.
|
||||
|
||||
func (e *emptyVFS) Name() string {
|
||||
return "/"
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Mode() os.FileMode {
|
||||
return os.ModeDir | os.ModePerm
|
||||
}
|
||||
|
||||
func (e *emptyVFS) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (e *emptyVFS) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *emptyVFS) Sys() interface{} {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
package vfs_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
|
||||
)
|
||||
|
||||
func TestNewNameSpace(t *testing.T) {
|
||||
|
||||
// We will mount this filesystem under /fs1
|
||||
mount := mapfs.New(map[string]string{"fs1file": "abcdefgh"})
|
||||
|
||||
// Existing process. This should give error on Stat("/")
|
||||
t1 := vfs.NameSpace{}
|
||||
t1.Bind("/fs1", mount, "/", vfs.BindReplace)
|
||||
|
||||
// using NewNameSpace. This should work fine.
|
||||
t2 := vfs.NewNameSpace()
|
||||
t2.Bind("/fs1", mount, "/", vfs.BindReplace)
|
||||
|
||||
testcases := map[string][]bool{
|
||||
"/": {false, true},
|
||||
"/fs1": {true, true},
|
||||
"/fs1/fs1file": {true, true},
|
||||
}
|
||||
|
||||
fss := []vfs.FileSystem{t1, t2}
|
||||
|
||||
for j, fs := range fss {
|
||||
for k, v := range testcases {
|
||||
_, err := fs.Stat(k)
|
||||
result := err == nil
|
||||
if result != v[j] {
|
||||
t.Errorf("fs: %d, testcase: %s, want: %v, got: %v, err: %s", j, k, v[j], result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := t2.Stat("/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if fi.Name() != "/" {
|
||||
t.Errorf("t2.Name() : want:%s got:%s", "/", fi.Name())
|
||||
}
|
||||
|
||||
if !fi.ModTime().IsZero() {
|
||||
t.Errorf("t2.Modime() : want:%v got:%v", time.Time{}, fi.ModTime())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// 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.
|
||||
|
||||
// Package gatefs provides an implementation of the FileSystem
|
||||
// interface that wraps another FileSystem and limits its concurrency.
|
||||
package gatefs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// New returns a new FileSystem that delegates to fs.
|
||||
// If gateCh is non-nil and buffered, it's used as a gate
|
||||
// to limit concurrency on calls to fs.
|
||||
func New(fs vfs.FileSystem, gateCh chan bool) vfs.FileSystem {
|
||||
if cap(gateCh) == 0 {
|
||||
return fs
|
||||
}
|
||||
return gatefs{fs, gate(gateCh)}
|
||||
}
|
||||
|
||||
type gate chan bool
|
||||
|
||||
func (g gate) enter() { g <- true }
|
||||
func (g gate) leave() { <-g }
|
||||
|
||||
type gatefs struct {
|
||||
fs vfs.FileSystem
|
||||
gate
|
||||
}
|
||||
|
||||
func (fs gatefs) String() string {
|
||||
return fmt.Sprintf("gated(%s, %d)", fs.fs.String(), cap(fs.gate))
|
||||
}
|
||||
|
||||
func (fs gatefs) RootType(path string) vfs.RootType {
|
||||
return fs.fs.RootType(path)
|
||||
}
|
||||
|
||||
func (fs gatefs) Open(p string) (vfs.ReadSeekCloser, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
rsc, err := fs.fs.Open(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gatef{rsc, fs.gate}, nil
|
||||
}
|
||||
|
||||
func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
return fs.fs.Lstat(p)
|
||||
}
|
||||
|
||||
func (fs gatefs) Stat(p string) (os.FileInfo, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
return fs.fs.Stat(p)
|
||||
}
|
||||
|
||||
func (fs gatefs) ReadDir(p string) ([]os.FileInfo, error) {
|
||||
fs.enter()
|
||||
defer fs.leave()
|
||||
return fs.fs.ReadDir(p)
|
||||
}
|
||||
|
||||
type gatef struct {
|
||||
rsc vfs.ReadSeekCloser
|
||||
gate
|
||||
}
|
||||
|
||||
func (f gatef) Read(p []byte) (n int, err error) {
|
||||
f.enter()
|
||||
defer f.leave()
|
||||
return f.rsc.Read(p)
|
||||
}
|
||||
|
||||
func (f gatef) Seek(offset int64, whence int) (ret int64, err error) {
|
||||
f.enter()
|
||||
defer f.leave()
|
||||
return f.rsc.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f gatef) Close() error {
|
||||
f.enter()
|
||||
defer f.leave()
|
||||
return f.rsc.Close()
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package gatefs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
|
||||
)
|
||||
|
||||
func TestRootType(t *testing.T) {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
var expectedType vfs.RootType
|
||||
if goPath == "" {
|
||||
expectedType = ""
|
||||
} else {
|
||||
expectedType = vfs.RootTypeGoPath
|
||||
}
|
||||
tests := []struct {
|
||||
path string
|
||||
fsType vfs.RootType
|
||||
}{
|
||||
{runtime.GOROOT(), vfs.RootTypeGoRoot},
|
||||
{goPath, expectedType},
|
||||
{"/tmp/", ""},
|
||||
}
|
||||
|
||||
for _, item := range tests {
|
||||
fs := gatefs.New(vfs.OS(item.path), make(chan bool, 1))
|
||||
if fs.RootType("path") != item.fsType {
|
||||
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType("path"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// 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.
|
||||
|
||||
// Package httpfs implements http.FileSystem using a godoc vfs.FileSystem.
|
||||
package httpfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/httpfs"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
func New(fs vfs.FileSystem) http.FileSystem {
|
||||
return &httpFS{fs}
|
||||
}
|
||||
|
||||
type httpFS struct {
|
||||
fs vfs.FileSystem
|
||||
}
|
||||
|
||||
func (h *httpFS) Open(name string) (http.File, error) {
|
||||
fi, err := h.fs.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return &httpDir{h.fs, name, nil}, nil
|
||||
}
|
||||
f, err := h.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpFile{h.fs, f, name}, nil
|
||||
}
|
||||
|
||||
// httpDir implements http.File for a directory in a FileSystem.
|
||||
type httpDir struct {
|
||||
fs vfs.FileSystem
|
||||
name string
|
||||
pending []os.FileInfo
|
||||
}
|
||||
|
||||
func (h *httpDir) Close() error { return nil }
|
||||
func (h *httpDir) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
|
||||
func (h *httpDir) Read([]byte) (int, error) {
|
||||
return 0, fmt.Errorf("cannot Read from directory %s", h.name)
|
||||
}
|
||||
|
||||
func (h *httpDir) Seek(offset int64, whence int) (int64, error) {
|
||||
if offset == 0 && whence == 0 {
|
||||
h.pending = nil
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported Seek in directory %s", h.name)
|
||||
}
|
||||
|
||||
func (h *httpDir) Readdir(count int) ([]os.FileInfo, error) {
|
||||
if h.pending == nil {
|
||||
d, err := h.fs.ReadDir(h.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d == nil {
|
||||
d = []os.FileInfo{} // not nil
|
||||
}
|
||||
h.pending = d
|
||||
}
|
||||
|
||||
if len(h.pending) == 0 && count > 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
if count <= 0 || count > len(h.pending) {
|
||||
count = len(h.pending)
|
||||
}
|
||||
d := h.pending[:count]
|
||||
h.pending = h.pending[count:]
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// httpFile implements http.File for a file (not directory) in a FileSystem.
|
||||
type httpFile struct {
|
||||
fs vfs.FileSystem
|
||||
vfs.ReadSeekCloser
|
||||
name string
|
||||
}
|
||||
|
||||
func (h *httpFile) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
|
||||
func (h *httpFile) Readdir(int) ([]os.FileInfo, error) {
|
||||
return nil, fmt.Errorf("cannot Readdir from file %s", h.name)
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
// 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.
|
||||
|
||||
// Package mapfs file provides an implementation of the FileSystem
|
||||
// interface based on the contents of a map[string]string.
|
||||
package mapfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// New returns a new FileSystem from the provided map.
|
||||
// Map keys should be forward slash-separated pathnames
|
||||
// and not contain a leading slash.
|
||||
func New(m map[string]string) vfs.FileSystem {
|
||||
return mapFS(m)
|
||||
}
|
||||
|
||||
// mapFS is the map based implementation of FileSystem
|
||||
type mapFS map[string]string
|
||||
|
||||
func (fs mapFS) String() string { return "mapfs" }
|
||||
|
||||
func (fs mapFS) RootType(p string) vfs.RootType {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fs mapFS) Close() error { return nil }
|
||||
|
||||
func filename(p string) string {
|
||||
return strings.TrimPrefix(p, "/")
|
||||
}
|
||||
|
||||
func (fs mapFS) Open(p string) (vfs.ReadSeekCloser, error) {
|
||||
b, ok := fs[filename(p)]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return nopCloser{strings.NewReader(b)}, nil
|
||||
}
|
||||
|
||||
func fileInfo(name, contents string) os.FileInfo {
|
||||
return mapFI{name: pathpkg.Base(name), size: len(contents)}
|
||||
}
|
||||
|
||||
func dirInfo(name string) os.FileInfo {
|
||||
return mapFI{name: pathpkg.Base(name), dir: true}
|
||||
}
|
||||
|
||||
func (fs mapFS) Lstat(p string) (os.FileInfo, error) {
|
||||
b, ok := fs[filename(p)]
|
||||
if ok {
|
||||
return fileInfo(p, b), nil
|
||||
}
|
||||
ents, _ := fs.ReadDir(p)
|
||||
if len(ents) > 0 {
|
||||
return dirInfo(p), nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (fs mapFS) Stat(p string) (os.FileInfo, error) {
|
||||
return fs.Lstat(p)
|
||||
}
|
||||
|
||||
// slashdir returns path.Dir(p), but special-cases paths not beginning
|
||||
// with a slash to be in the root.
|
||||
func slashdir(p string) string {
|
||||
d := pathpkg.Dir(p)
|
||||
if d == "." {
|
||||
return "/"
|
||||
}
|
||||
if strings.HasPrefix(p, "/") {
|
||||
return d
|
||||
}
|
||||
return "/" + d
|
||||
}
|
||||
|
||||
func (fs mapFS) ReadDir(p string) ([]os.FileInfo, error) {
|
||||
p = pathpkg.Clean(p)
|
||||
var ents []string
|
||||
fim := make(map[string]os.FileInfo) // base -> fi
|
||||
for fn, b := range fs {
|
||||
dir := slashdir(fn)
|
||||
isFile := true
|
||||
var lastBase string
|
||||
for {
|
||||
if dir == p {
|
||||
base := lastBase
|
||||
if isFile {
|
||||
base = pathpkg.Base(fn)
|
||||
}
|
||||
if fim[base] == nil {
|
||||
var fi os.FileInfo
|
||||
if isFile {
|
||||
fi = fileInfo(fn, b)
|
||||
} else {
|
||||
fi = dirInfo(base)
|
||||
}
|
||||
ents = append(ents, base)
|
||||
fim[base] = fi
|
||||
}
|
||||
}
|
||||
if dir == "/" {
|
||||
break
|
||||
} else {
|
||||
isFile = false
|
||||
lastBase = pathpkg.Base(dir)
|
||||
dir = pathpkg.Dir(dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ents) == 0 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
sort.Strings(ents)
|
||||
var list []os.FileInfo
|
||||
for _, dir := range ents {
|
||||
list = append(list, fim[dir])
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// mapFI is the map-based implementation of FileInfo.
|
||||
type mapFI struct {
|
||||
name string
|
||||
size int
|
||||
dir bool
|
||||
}
|
||||
|
||||
func (fi mapFI) IsDir() bool { return fi.dir }
|
||||
func (fi mapFI) ModTime() time.Time { return time.Time{} }
|
||||
func (fi mapFI) Mode() os.FileMode {
|
||||
if fi.IsDir() {
|
||||
return 0755 | os.ModeDir
|
||||
}
|
||||
return 0444
|
||||
}
|
||||
func (fi mapFI) Name() string { return pathpkg.Base(fi.name) }
|
||||
func (fi mapFI) Size() int64 { return int64(fi.size) }
|
||||
func (fi mapFI) Sys() interface{} { return nil }
|
||||
|
||||
type nopCloser struct {
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
func (nc nopCloser) Close() error { return nil }
|
|
@ -0,0 +1,111 @@
|
|||
// 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.
|
||||
|
||||
package mapfs
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenRoot(t *testing.T) {
|
||||
fs := New(map[string]string{
|
||||
"foo/bar/three.txt": "a",
|
||||
"foo/bar.txt": "b",
|
||||
"top.txt": "c",
|
||||
"other-top.txt": "d",
|
||||
})
|
||||
tests := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"/foo/bar/three.txt", "a"},
|
||||
{"foo/bar/three.txt", "a"},
|
||||
{"foo/bar.txt", "b"},
|
||||
{"top.txt", "c"},
|
||||
{"/top.txt", "c"},
|
||||
{"other-top.txt", "d"},
|
||||
{"/other-top.txt", "d"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
rsc, err := fs.Open(tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("Open(%q) = %v", tt.path, err)
|
||||
continue
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(rsc)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(slurp) != tt.want {
|
||||
t.Errorf("Read(%q) = %q; want %q", tt.path, tt.want, slurp)
|
||||
}
|
||||
rsc.Close()
|
||||
}
|
||||
|
||||
_, err := fs.Open("/xxxx")
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("ReadDir /xxxx = %v; want os.IsNotExist error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaddir(t *testing.T) {
|
||||
fs := New(map[string]string{
|
||||
"foo/bar/three.txt": "333",
|
||||
"foo/bar.txt": "22",
|
||||
"top.txt": "top.txt file",
|
||||
"other-top.txt": "other-top.txt file",
|
||||
})
|
||||
tests := []struct {
|
||||
dir string
|
||||
want []os.FileInfo
|
||||
}{
|
||||
{
|
||||
dir: "/",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "foo", dir: true},
|
||||
mapFI{name: "other-top.txt", size: len("other-top.txt file")},
|
||||
mapFI{name: "top.txt", size: len("top.txt file")},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "/foo",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "bar", dir: true},
|
||||
mapFI{name: "bar.txt", size: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "/foo/",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "bar", dir: true},
|
||||
mapFI{name: "bar.txt", size: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "/foo/bar",
|
||||
want: []os.FileInfo{
|
||||
mapFI{name: "three.txt", size: 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
fis, err := fs.ReadDir(tt.dir)
|
||||
if err != nil {
|
||||
t.Errorf("ReadDir(%q) = %v", tt.dir, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(fis, tt.want) {
|
||||
t.Errorf("ReadDir(%q) = %#v; want %#v", tt.dir, fis, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_, err := fs.ReadDir("/xxxx")
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("ReadDir /xxxx = %v; want os.IsNotExist error", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,403 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Setting debugNS = true will enable debugging prints about
|
||||
// name space translations.
|
||||
const debugNS = false
|
||||
|
||||
// A NameSpace is a file system made up of other file systems
|
||||
// mounted at specific locations in the name space.
|
||||
//
|
||||
// The representation is a map from mount point locations
|
||||
// to the list of file systems mounted at that location. A traditional
|
||||
// Unix mount table would use a single file system per mount point,
|
||||
// but we want to be able to mount multiple file systems on a single
|
||||
// mount point and have the system behave as if the union of those
|
||||
// file systems were present at the mount point.
|
||||
// For example, if the OS file system has a Go installation in
|
||||
// c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then
|
||||
// this name space creates the view we want for the godoc server:
|
||||
//
|
||||
// NameSpace{
|
||||
// "/": {
|
||||
// {old: "/", fs: OS(`c:\Go`), new: "/"},
|
||||
// },
|
||||
// "/src/pkg": {
|
||||
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// This is created by executing:
|
||||
//
|
||||
// ns := NameSpace{}
|
||||
// ns.Bind("/", OS(`c:\Go`), "/", BindReplace)
|
||||
// ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", BindAfter)
|
||||
// ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", BindAfter)
|
||||
//
|
||||
// A particular mount point entry is a triple (old, fs, new), meaning that to
|
||||
// operate on a path beginning with old, replace that prefix (old) with new
|
||||
// and then pass that path to the FileSystem implementation fs.
|
||||
//
|
||||
// If you do not explicitly mount a FileSystem at the root mountpoint "/" of the
|
||||
// NameSpace like above, Stat("/") will return a "not found" error which could
|
||||
// break typical directory traversal routines. In such cases, use NewNameSpace()
|
||||
// to get a NameSpace pre-initialized with an emulated empty directory at root.
|
||||
//
|
||||
// Given this name space, a ReadDir of /src/pkg/code will check each prefix
|
||||
// of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src,
|
||||
// then /), stopping when it finds one. For the above example, /src/pkg/code
|
||||
// will find the mount point at /src/pkg:
|
||||
//
|
||||
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
|
||||
//
|
||||
// ReadDir will when execute these three calls and merge the results:
|
||||
//
|
||||
// OS(`c:\Go`).ReadDir("/src/pkg/code")
|
||||
// OS(`d:\Work1').ReadDir("/src/code")
|
||||
// OS(`d:\Work2').ReadDir("/src/code")
|
||||
//
|
||||
// Note that the "/src/pkg" in "/src/pkg/code" has been replaced by
|
||||
// just "/src" in the final two calls.
|
||||
//
|
||||
// OS is itself an implementation of a file system: it implements
|
||||
// OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`).
|
||||
//
|
||||
// Because the new path is evaluated by fs (here OS(root)), another way
|
||||
// to read the mount table is to mentally combine fs+new, so that this table:
|
||||
//
|
||||
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
|
||||
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
|
||||
//
|
||||
// reads as:
|
||||
//
|
||||
// "/src/pkg" -> c:\Go\src\pkg
|
||||
// "/src/pkg" -> d:\Work1\src
|
||||
// "/src/pkg" -> d:\Work2\src
|
||||
//
|
||||
// An invariant (a redundancy) of the name space representation is that
|
||||
// ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s
|
||||
// mount table entries always have old == "/src/pkg"). The 'old' field is
|
||||
// useful to callers, because they receive just a []mountedFS and not any
|
||||
// other indication of which mount point was found.
|
||||
//
|
||||
type NameSpace map[string][]mountedFS
|
||||
|
||||
// A mountedFS handles requests for path by replacing
|
||||
// a prefix 'old' with 'new' and then calling the fs methods.
|
||||
type mountedFS struct {
|
||||
old string
|
||||
fs FileSystem
|
||||
new string
|
||||
}
|
||||
|
||||
// hasPathPrefix returns true if x == y or x == y + "/" + more
|
||||
func hasPathPrefix(x, y string) bool {
|
||||
return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/"))
|
||||
}
|
||||
|
||||
// translate translates path for use in m, replacing old with new.
|
||||
//
|
||||
// mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code".
|
||||
func (m mountedFS) translate(path string) string {
|
||||
path = pathpkg.Clean("/" + path)
|
||||
if !hasPathPrefix(path, m.old) {
|
||||
panic("translate " + path + " but old=" + m.old)
|
||||
}
|
||||
return pathpkg.Join(m.new, path[len(m.old):])
|
||||
}
|
||||
|
||||
func (NameSpace) String() string {
|
||||
return "ns"
|
||||
}
|
||||
|
||||
// Fprint writes a text representation of the name space to w.
|
||||
func (ns NameSpace) Fprint(w io.Writer) {
|
||||
fmt.Fprint(w, "name space {\n")
|
||||
var all []string
|
||||
for mtpt := range ns {
|
||||
all = append(all, mtpt)
|
||||
}
|
||||
sort.Strings(all)
|
||||
for _, mtpt := range all {
|
||||
fmt.Fprintf(w, "\t%s:\n", mtpt)
|
||||
for _, m := range ns[mtpt] {
|
||||
fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, "}\n")
|
||||
}
|
||||
|
||||
// clean returns a cleaned, rooted path for evaluation.
|
||||
// It canonicalizes the path so that we can use string operations
|
||||
// to analyze it.
|
||||
func (NameSpace) clean(path string) string {
|
||||
return pathpkg.Clean("/" + path)
|
||||
}
|
||||
|
||||
type BindMode int
|
||||
|
||||
const (
|
||||
BindReplace BindMode = iota
|
||||
BindBefore
|
||||
BindAfter
|
||||
)
|
||||
|
||||
// Bind causes references to old to redirect to the path new in newfs.
|
||||
// If mode is BindReplace, old redirections are discarded.
|
||||
// If mode is BindBefore, this redirection takes priority over existing ones,
|
||||
// but earlier ones are still consulted for paths that do not exist in newfs.
|
||||
// If mode is BindAfter, this redirection happens only after existing ones
|
||||
// have been tried and failed.
|
||||
func (ns NameSpace) Bind(old string, newfs FileSystem, new string, mode BindMode) {
|
||||
old = ns.clean(old)
|
||||
new = ns.clean(new)
|
||||
m := mountedFS{old, newfs, new}
|
||||
var mtpt []mountedFS
|
||||
switch mode {
|
||||
case BindReplace:
|
||||
mtpt = append(mtpt, m)
|
||||
case BindAfter:
|
||||
mtpt = append(mtpt, ns.resolve(old)...)
|
||||
mtpt = append(mtpt, m)
|
||||
case BindBefore:
|
||||
mtpt = append(mtpt, m)
|
||||
mtpt = append(mtpt, ns.resolve(old)...)
|
||||
}
|
||||
|
||||
// Extend m.old, m.new in inherited mount point entries.
|
||||
for i := range mtpt {
|
||||
m := &mtpt[i]
|
||||
if m.old != old {
|
||||
if !hasPathPrefix(old, m.old) {
|
||||
// This should not happen. If it does, panic so
|
||||
// that we can see the call trace that led to it.
|
||||
panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new))
|
||||
}
|
||||
suffix := old[len(m.old):]
|
||||
m.old = pathpkg.Join(m.old, suffix)
|
||||
m.new = pathpkg.Join(m.new, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
ns[old] = mtpt
|
||||
}
|
||||
|
||||
// resolve resolves a path to the list of mountedFS to use for path.
|
||||
func (ns NameSpace) resolve(path string) []mountedFS {
|
||||
path = ns.clean(path)
|
||||
for {
|
||||
if m := ns[path]; m != nil {
|
||||
if debugNS {
|
||||
fmt.Printf("resolve %s: %v\n", path, m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
if path == "/" {
|
||||
break
|
||||
}
|
||||
path = pathpkg.Dir(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open implements the FileSystem Open method.
|
||||
func (ns NameSpace) Open(path string) (ReadSeekCloser, error) {
|
||||
var err error
|
||||
for _, m := range ns.resolve(path) {
|
||||
if debugNS {
|
||||
fmt.Printf("tx %s: %v\n", path, m.translate(path))
|
||||
}
|
||||
tp := m.translate(path)
|
||||
r, err1 := m.fs.Open(tp)
|
||||
if err1 == nil {
|
||||
return r, nil
|
||||
}
|
||||
// IsNotExist errors in overlay FSes can mask real errors in
|
||||
// the underlying FS, so ignore them if there is another error.
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
err = err1
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// stat implements the FileSystem Stat and Lstat methods.
|
||||
func (ns NameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) {
|
||||
var err error
|
||||
for _, m := range ns.resolve(path) {
|
||||
fi, err1 := f(m.fs, m.translate(path))
|
||||
if err1 == nil {
|
||||
return fi, nil
|
||||
}
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (ns NameSpace) Stat(path string) (os.FileInfo, error) {
|
||||
return ns.stat(path, FileSystem.Stat)
|
||||
}
|
||||
|
||||
func (ns NameSpace) Lstat(path string) (os.FileInfo, error) {
|
||||
return ns.stat(path, FileSystem.Lstat)
|
||||
}
|
||||
|
||||
// dirInfo is a trivial implementation of os.FileInfo for a directory.
|
||||
type dirInfo string
|
||||
|
||||
func (d dirInfo) Name() string { return string(d) }
|
||||
func (d dirInfo) Size() int64 { return 0 }
|
||||
func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 }
|
||||
func (d dirInfo) ModTime() time.Time { return startTime }
|
||||
func (d dirInfo) IsDir() bool { return true }
|
||||
func (d dirInfo) Sys() interface{} { return nil }
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
// ReadDir implements the FileSystem ReadDir method. It's where most of the magic is.
|
||||
// (The rest is in resolve.)
|
||||
//
|
||||
// Logically, ReadDir must return the union of all the directories that are named
|
||||
// by path. In order to avoid misinterpreting Go packages, of all the directories
|
||||
// that contain Go source code, we only include the files from the first,
|
||||
// but we include subdirectories from all.
|
||||
//
|
||||
// ReadDir must also return directory entries needed to reach mount points.
|
||||
// If the name space looks like the example in the type NameSpace comment,
|
||||
// but c:\Go does not have a src/pkg subdirectory, we still want to be able
|
||||
// to find that subdirectory, because we've mounted d:\Work1 and d:\Work2
|
||||
// there. So if we don't see "src" in the directory listing for c:\Go, we add an
|
||||
// entry for it before returning.
|
||||
//
|
||||
func (ns NameSpace) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
path = ns.clean(path)
|
||||
|
||||
var (
|
||||
haveGo = false
|
||||
haveName = map[string]bool{}
|
||||
all []os.FileInfo
|
||||
err error
|
||||
first []os.FileInfo
|
||||
)
|
||||
|
||||
for _, m := range ns.resolve(path) {
|
||||
dir, err1 := m.fs.ReadDir(m.translate(path))
|
||||
if err1 != nil {
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if dir == nil {
|
||||
dir = []os.FileInfo{}
|
||||
}
|
||||
|
||||
if first == nil {
|
||||
first = dir
|
||||
}
|
||||
|
||||
// If we don't yet have Go files in 'all' and this directory
|
||||
// has some, add all the files from this directory.
|
||||
// Otherwise, only add subdirectories.
|
||||
useFiles := false
|
||||
if !haveGo {
|
||||
for _, d := range dir {
|
||||
if strings.HasSuffix(d.Name(), ".go") {
|
||||
useFiles = true
|
||||
haveGo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range dir {
|
||||
name := d.Name()
|
||||
if (d.IsDir() || useFiles) && !haveName[name] {
|
||||
haveName[name] = true
|
||||
all = append(all, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find any directories containing Go files.
|
||||
// If some directory returned successfully, use that.
|
||||
if !haveGo {
|
||||
for _, d := range first {
|
||||
if !haveName[d.Name()] {
|
||||
haveName[d.Name()] = true
|
||||
all = append(all, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Built union. Add any missing directories needed to reach mount points.
|
||||
for old := range ns {
|
||||
if hasPathPrefix(old, path) && old != path {
|
||||
// Find next element after path in old.
|
||||
elem := old[len(path):]
|
||||
elem = strings.TrimPrefix(elem, "/")
|
||||
if i := strings.Index(elem, "/"); i >= 0 {
|
||||
elem = elem[:i]
|
||||
}
|
||||
if !haveName[elem] {
|
||||
haveName[elem] = true
|
||||
all = append(all, dirInfo(elem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Sort(byName(all))
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// RootType returns the RootType for the given path in the namespace.
|
||||
func (ns NameSpace) RootType(path string) RootType {
|
||||
// We resolve the given path to a list of mountedFS and then return
|
||||
// the root type for the filesystem which contains the path.
|
||||
for _, m := range ns.resolve(path) {
|
||||
_, err := m.fs.ReadDir(m.translate(path))
|
||||
// Found a match, return the filesystem's root type
|
||||
if err == nil {
|
||||
return m.fs.RootType(path)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// byName implements sort.Interface.
|
||||
type byName []os.FileInfo
|
||||
|
||||
func (f byName) Len() int { return len(f) }
|
||||
func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
|
||||
func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
|
@ -0,0 +1,105 @@
|
|||
// 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.
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// We expose a new variable because otherwise we need to copy the findGOROOT logic again
|
||||
// from cmd/godoc which is already copied twice from the standard library.
|
||||
|
||||
// GOROOT is the GOROOT path under which the godoc binary is running.
|
||||
// It is needed to check whether a filesystem root is under GOROOT or not.
|
||||
// This is set from cmd/godoc/main.go.
|
||||
var GOROOT = runtime.GOROOT()
|
||||
|
||||
// OS returns an implementation of FileSystem reading from the
|
||||
// tree rooted at root. Recording a root is convenient everywhere
|
||||
// but necessary on Windows, because the slash-separated path
|
||||
// passed to Open has no way to specify a drive letter. Using a root
|
||||
// lets code refer to OS(`c:\`), OS(`d:\`) and so on.
|
||||
func OS(root string) FileSystem {
|
||||
var t RootType
|
||||
switch {
|
||||
case root == GOROOT:
|
||||
t = RootTypeGoRoot
|
||||
case isGoPath(root):
|
||||
t = RootTypeGoPath
|
||||
}
|
||||
return osFS{rootPath: root, rootType: t}
|
||||
}
|
||||
|
||||
type osFS struct {
|
||||
rootPath string
|
||||
rootType RootType
|
||||
}
|
||||
|
||||
func isGoPath(path string) bool {
|
||||
for _, bp := range filepath.SplitList(build.Default.GOPATH) {
|
||||
for _, gp := range filepath.SplitList(path) {
|
||||
if bp == gp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (root osFS) String() string { return "os(" + root.rootPath + ")" }
|
||||
|
||||
// RootType returns the root type for the filesystem.
|
||||
//
|
||||
// Note that we ignore the path argument because roottype is a property of
|
||||
// this filesystem. But for other filesystems, the roottype might need to be
|
||||
// dynamically deduced at call time.
|
||||
func (root osFS) RootType(path string) RootType {
|
||||
return root.rootType
|
||||
}
|
||||
|
||||
func (root osFS) resolve(path string) string {
|
||||
// Clean the path so that it cannot possibly begin with ../.
|
||||
// If it did, the result of filepath.Join would be outside the
|
||||
// tree rooted at root. We probably won't ever see a path
|
||||
// with .. in it, but be safe anyway.
|
||||
path = pathpkg.Clean("/" + path)
|
||||
|
||||
return filepath.Join(root.rootPath, path)
|
||||
}
|
||||
|
||||
func (root osFS) Open(path string) (ReadSeekCloser, error) {
|
||||
f, err := os.Open(root.resolve(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("Open: %s is a directory", path)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (root osFS) Lstat(path string) (os.FileInfo, error) {
|
||||
return os.Lstat(root.resolve(path))
|
||||
}
|
||||
|
||||
func (root osFS) Stat(path string) (os.FileInfo, error) {
|
||||
return os.Stat(root.resolve(path))
|
||||
}
|
||||
|
||||
func (root osFS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
return ioutil.ReadDir(root.resolve(path)) // is sorted
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package vfs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
func TestRootType(t *testing.T) {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
var expectedType vfs.RootType
|
||||
if goPath == "" {
|
||||
expectedType = ""
|
||||
} else {
|
||||
expectedType = vfs.RootTypeGoPath
|
||||
}
|
||||
tests := []struct {
|
||||
path string
|
||||
fsType vfs.RootType
|
||||
}{
|
||||
{runtime.GOROOT(), vfs.RootTypeGoRoot},
|
||||
{goPath, expectedType},
|
||||
{"/tmp/", ""},
|
||||
}
|
||||
|
||||
for _, item := range tests {
|
||||
fs := vfs.OS(item.path)
|
||||
if fs.RootType("path") != item.fsType {
|
||||
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType("path"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// 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.
|
||||
|
||||
// Package vfs defines types for abstract file system access and provides an
|
||||
// implementation accessing the file system of the underlying OS.
|
||||
package vfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// RootType indicates the type of files contained within a directory.
|
||||
//
|
||||
// It is used to indicate whether a directory is the root
|
||||
// of a GOROOT, a GOPATH, or neither.
|
||||
// An empty string represents the case when a directory is neither.
|
||||
type RootType string
|
||||
|
||||
const (
|
||||
RootTypeGoRoot RootType = "GOROOT"
|
||||
RootTypeGoPath RootType = "GOPATH"
|
||||
)
|
||||
|
||||
// The FileSystem interface specifies the methods godoc is using
|
||||
// to access the file system for which it serves documentation.
|
||||
type FileSystem interface {
|
||||
Opener
|
||||
Lstat(path string) (os.FileInfo, error)
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
ReadDir(path string) ([]os.FileInfo, error)
|
||||
RootType(path string) RootType
|
||||
String() string
|
||||
}
|
||||
|
||||
// Opener is a minimal virtual filesystem that can only open regular files.
|
||||
type Opener interface {
|
||||
Open(name string) (ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
// A ReadSeekCloser can Read, Seek, and Close.
|
||||
type ReadSeekCloser interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// ReadFile reads the file named by path from fs and returns the contents.
|
||||
func ReadFile(fs Opener, path string) ([]byte, error) {
|
||||
rc, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
return ioutil.ReadAll(rc)
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
// Package zipfs file provides an implementation of the FileSystem
|
||||
// interface based on the contents of a .zip file.
|
||||
//
|
||||
// Assumptions:
|
||||
//
|
||||
// - The file paths stored in the zip file must use a slash ('/') as path
|
||||
// separator; and they must be relative (i.e., they must not start with
|
||||
// a '/' - this is usually the case if the file was created w/o special
|
||||
// options).
|
||||
// - The zip file system treats the file paths found in the zip internally
|
||||
// like absolute paths w/o a leading '/'; i.e., the paths are considered
|
||||
// relative to the root of the file system.
|
||||
// - All path arguments to file system methods must be absolute paths.
|
||||
package zipfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/zipfs"
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
// zipFI is the zip-file based implementation of FileInfo
|
||||
type zipFI struct {
|
||||
name string // directory-local name
|
||||
file *zip.File // nil for a directory
|
||||
}
|
||||
|
||||
func (fi zipFI) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
|
||||
func (fi zipFI) Size() int64 {
|
||||
if f := fi.file; f != nil {
|
||||
return int64(f.UncompressedSize)
|
||||
}
|
||||
return 0 // directory
|
||||
}
|
||||
|
||||
func (fi zipFI) ModTime() time.Time {
|
||||
if f := fi.file; f != nil {
|
||||
return f.ModTime()
|
||||
}
|
||||
return time.Time{} // directory has no modified time entry
|
||||
}
|
||||
|
||||
func (fi zipFI) Mode() os.FileMode {
|
||||
if fi.file == nil {
|
||||
// Unix directories typically are executable, hence 555.
|
||||
return os.ModeDir | 0555
|
||||
}
|
||||
return 0444
|
||||
}
|
||||
|
||||
func (fi zipFI) IsDir() bool {
|
||||
return fi.file == nil
|
||||
}
|
||||
|
||||
func (fi zipFI) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// zipFS is the zip-file based implementation of FileSystem
|
||||
type zipFS struct {
|
||||
*zip.ReadCloser
|
||||
list zipList
|
||||
name string
|
||||
}
|
||||
|
||||
func (fs *zipFS) String() string {
|
||||
return "zip(" + fs.name + ")"
|
||||
}
|
||||
|
||||
func (fs *zipFS) RootType(abspath string) vfs.RootType {
|
||||
var t vfs.RootType
|
||||
switch {
|
||||
case exists(path.Join(vfs.GOROOT, abspath)):
|
||||
t = vfs.RootTypeGoRoot
|
||||
case isGoPath(abspath):
|
||||
t = vfs.RootTypeGoPath
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func isGoPath(abspath string) bool {
|
||||
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
||||
if exists(path.Join(p, abspath)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (fs *zipFS) Close() error {
|
||||
fs.list = nil
|
||||
return fs.ReadCloser.Close()
|
||||
}
|
||||
|
||||
func zipPath(name string) (string, error) {
|
||||
name = path.Clean(name)
|
||||
if !path.IsAbs(name) {
|
||||
return "", fmt.Errorf("stat: not an absolute path: %s", name)
|
||||
}
|
||||
return name[1:], nil // strip leading '/'
|
||||
}
|
||||
|
||||
func isRoot(abspath string) bool {
|
||||
return path.Clean(abspath) == "/"
|
||||
}
|
||||
|
||||
func (fs *zipFS) stat(abspath string) (int, zipFI, error) {
|
||||
if isRoot(abspath) {
|
||||
return 0, zipFI{
|
||||
name: "",
|
||||
file: nil,
|
||||
}, nil
|
||||
}
|
||||
zippath, err := zipPath(abspath)
|
||||
if err != nil {
|
||||
return 0, zipFI{}, err
|
||||
}
|
||||
i, exact := fs.list.lookup(zippath)
|
||||
if i < 0 {
|
||||
// zippath has leading '/' stripped - print it explicitly
|
||||
return -1, zipFI{}, &os.PathError{Path: "/" + zippath, Err: os.ErrNotExist}
|
||||
}
|
||||
_, name := path.Split(zippath)
|
||||
var file *zip.File
|
||||
if exact {
|
||||
file = fs.list[i] // exact match found - must be a file
|
||||
}
|
||||
return i, zipFI{name, file}, nil
|
||||
}
|
||||
|
||||
func (fs *zipFS) Open(abspath string) (vfs.ReadSeekCloser, error) {
|
||||
_, fi, err := fs.stat(abspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return nil, fmt.Errorf("Open: %s is a directory", abspath)
|
||||
}
|
||||
r, err := fi.file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &zipSeek{fi.file, r}, nil
|
||||
}
|
||||
|
||||
type zipSeek struct {
|
||||
file *zip.File
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (f *zipSeek) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == 0 && offset == 0 {
|
||||
r, err := f.file.Open()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.Close()
|
||||
f.ReadCloser = r
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name)
|
||||
}
|
||||
|
||||
func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) {
|
||||
_, fi, err := fs.stat(abspath)
|
||||
return fi, err
|
||||
}
|
||||
|
||||
func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) {
|
||||
_, fi, err := fs.stat(abspath)
|
||||
return fi, err
|
||||
}
|
||||
|
||||
func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) {
|
||||
i, fi, err := fs.stat(abspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath)
|
||||
}
|
||||
|
||||
var list []os.FileInfo
|
||||
|
||||
// make dirname the prefix that file names must start with to be considered
|
||||
// in this directory. we must special case the root directory because, per
|
||||
// the spec of this package, zip file entries MUST NOT start with /, so we
|
||||
// should not append /, as we would in every other case.
|
||||
var dirname string
|
||||
if isRoot(abspath) {
|
||||
dirname = ""
|
||||
} else {
|
||||
zippath, err := zipPath(abspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirname = zippath + "/"
|
||||
}
|
||||
prevname := ""
|
||||
for _, e := range fs.list[i:] {
|
||||
if !strings.HasPrefix(e.Name, dirname) {
|
||||
break // not in the same directory anymore
|
||||
}
|
||||
name := e.Name[len(dirname):] // local name
|
||||
file := e
|
||||
if i := strings.IndexRune(name, '/'); i >= 0 {
|
||||
// We infer directories from files in subdirectories.
|
||||
// If we have x/y, return a directory entry for x.
|
||||
name = name[0:i] // keep local directory name only
|
||||
file = nil
|
||||
}
|
||||
// If we have x/y and x/z, don't return two directory entries for x.
|
||||
// TODO(gri): It should be possible to do this more efficiently
|
||||
// by determining the (fs.list) range of local directory entries
|
||||
// (via two binary searches).
|
||||
if name != prevname {
|
||||
list = append(list, zipFI{name, file})
|
||||
prevname = name
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func New(rc *zip.ReadCloser, name string) vfs.FileSystem {
|
||||
list := make(zipList, len(rc.File))
|
||||
copy(list, rc.File) // sort a copy of rc.File
|
||||
sort.Sort(list)
|
||||
return &zipFS{rc, list, name}
|
||||
}
|
||||
|
||||
type zipList []*zip.File
|
||||
|
||||
// zipList implements sort.Interface
|
||||
func (z zipList) Len() int { return len(z) }
|
||||
func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name }
|
||||
func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] }
|
||||
|
||||
// lookup returns the smallest index of an entry with an exact match
|
||||
// for name, or an inexact match starting with name/. If there is no
|
||||
// such entry, the result is -1, false.
|
||||
func (z zipList) lookup(name string) (index int, exact bool) {
|
||||
// look for exact match first (name comes before name/ in z)
|
||||
i := sort.Search(len(z), func(i int) bool {
|
||||
return name <= z[i].Name
|
||||
})
|
||||
if i >= len(z) {
|
||||
return -1, false
|
||||
}
|
||||
// 0 <= i < len(z)
|
||||
if z[i].Name == name {
|
||||
return i, true
|
||||
}
|
||||
|
||||
// look for inexact match (must be in z[i:], if present)
|
||||
z = z[i:]
|
||||
name += "/"
|
||||
j := sort.Search(len(z), func(i int) bool {
|
||||
return name <= z[i].Name
|
||||
})
|
||||
if j >= len(z) {
|
||||
return -1, false
|
||||
}
|
||||
// 0 <= j < len(z)
|
||||
if strings.HasPrefix(z[j].Name, name) {
|
||||
return i + j, false
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright 2015 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.package zipfs
|
||||
package zipfs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// files to use to build zip used by zipfs in testing; maps path : contents
|
||||
files = map[string]string{"foo": "foo", "bar/baz": "baz", "a/b/c": "c"}
|
||||
|
||||
// expected info for each entry in a file system described by files
|
||||
tests = []struct {
|
||||
Path string
|
||||
IsDir bool
|
||||
IsRegular bool
|
||||
Name string
|
||||
Contents string
|
||||
Files map[string]bool
|
||||
}{
|
||||
{"/", true, false, "", "", map[string]bool{"foo": true, "bar": true, "a": true}},
|
||||
{"//", true, false, "", "", map[string]bool{"foo": true, "bar": true, "a": true}},
|
||||
{"/foo", false, true, "foo", "foo", nil},
|
||||
{"/foo/", false, true, "foo", "foo", nil},
|
||||
{"/foo//", false, true, "foo", "foo", nil},
|
||||
{"/bar", true, false, "bar", "", map[string]bool{"baz": true}},
|
||||
{"/bar/", true, false, "bar", "", map[string]bool{"baz": true}},
|
||||
{"/bar/baz", false, true, "baz", "baz", nil},
|
||||
{"//bar//baz", false, true, "baz", "baz", nil},
|
||||
{"/a/b", true, false, "b", "", map[string]bool{"c": true}},
|
||||
}
|
||||
|
||||
// to be initialized in setup()
|
||||
fs vfs.FileSystem
|
||||
statFuncs []statFunc
|
||||
)
|
||||
|
||||
type statFunc struct {
|
||||
Name string
|
||||
Func func(string) (os.FileInfo, error)
|
||||
}
|
||||
|
||||
func TestMain(t *testing.M) {
|
||||
if err := setup(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error setting up zipfs testing state: %v.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(t.Run())
|
||||
}
|
||||
|
||||
// setups state each of the tests uses
|
||||
func setup() error {
|
||||
// create zipfs
|
||||
b := new(bytes.Buffer)
|
||||
zw := zip.NewWriter(b)
|
||||
for file, contents := range files {
|
||||
w, err := zw.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, contents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
zw.Close()
|
||||
zr, err := zip.NewReader(bytes.NewReader(b.Bytes()), int64(b.Len()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc := &zip.ReadCloser{
|
||||
Reader: *zr,
|
||||
}
|
||||
fs = New(rc, "foo")
|
||||
|
||||
// pull out different stat functions
|
||||
statFuncs = []statFunc{
|
||||
{"Stat", fs.Stat},
|
||||
{"Lstat", fs.Lstat},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestZipFSReadDir(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
if test.IsDir {
|
||||
infos, err := fs.ReadDir(test.Path)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read directory %v\n", test.Path)
|
||||
continue
|
||||
}
|
||||
got := make(map[string]bool)
|
||||
for _, info := range infos {
|
||||
got[info.Name()] = true
|
||||
}
|
||||
if want := test.Files; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ReadDir %v got %v\nwanted %v\n", test.Path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFSStatFuncs(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
for _, statFunc := range statFuncs {
|
||||
|
||||
// test can stat
|
||||
info, err := statFunc.Func(test.Path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error using %v for %v: %v\n", statFunc.Name, test.Path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// test info.Name()
|
||||
if got, want := info.Name(), test.Name; got != want {
|
||||
t.Errorf("Using %v for %v info.Name() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.IsDir()
|
||||
if got, want := info.IsDir(), test.IsDir; got != want {
|
||||
t.Errorf("Using %v for %v info.IsDir() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.Mode().IsDir()
|
||||
if got, want := info.Mode().IsDir(), test.IsDir; got != want {
|
||||
t.Errorf("Using %v for %v info.Mode().IsDir() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.Mode().IsRegular()
|
||||
if got, want := info.Mode().IsRegular(), test.IsRegular; got != want {
|
||||
t.Errorf("Using %v for %v info.Mode().IsRegular() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
// test info.Size()
|
||||
if test.IsRegular {
|
||||
if got, want := info.Size(), int64(len(test.Contents)); got != want {
|
||||
t.Errorf("Using %v for %v inf.Size() got %v wanted %v", statFunc.Name, test.Path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFSNotExist(t *testing.T) {
|
||||
_, err := fs.Open("/does-not-exist")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected an error.\n")
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("Expected an error satisfying os.IsNotExist: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipFSOpenSeek(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
if test.IsRegular {
|
||||
|
||||
// test Open()
|
||||
f, err := fs.Open(test.Path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// test Seek() multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
all, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if got, want := string(all), test.Contents; got != want {
|
||||
t.Errorf("File contents for %v got %v wanted %v\n", test.Path, got, want)
|
||||
}
|
||||
f.Seek(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootType(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
fsType vfs.RootType
|
||||
}{
|
||||
{"/src/net/http", vfs.RootTypeGoRoot},
|
||||
{"/src/badpath", ""},
|
||||
{"/", vfs.RootTypeGoRoot},
|
||||
}
|
||||
|
||||
for _, item := range tests {
|
||||
if fs.RootType(item.path) != item.fsType {
|
||||
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType(item.path))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,464 @@
|
|||
// 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.
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// buildGodoc builds the godoc executable.
|
||||
// It returns its path, and a cleanup function.
|
||||
//
|
||||
// TODO(adonovan): opt: do this at most once, and do the cleanup
|
||||
// exactly once. How though? There's no atexit.
|
||||
func buildGodoc(t *testing.T) (bin string, cleanup func()) {
|
||||
if runtime.GOARCH == "arm" {
|
||||
t.Skip("skipping test on arm platforms; too slow")
|
||||
}
|
||||
tmp, err := ioutil.TempDir("", "godoc-regtest-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil { // probably, go build failed.
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
bin = filepath.Join(tmp, "godoc")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", bin)
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Building godoc: %v", err)
|
||||
}
|
||||
|
||||
return bin, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
func serverAddress(t *testing.T) string {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
ln, err = net.Listen("tcp6", "[::1]:0")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
func waitForServerReady(t *testing.T, addr string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v/", addr),
|
||||
"The Go Programming Language",
|
||||
15*time.Second,
|
||||
false)
|
||||
}
|
||||
|
||||
func waitForSearchReady(t *testing.T, addr string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
|
||||
"The list of tokens.",
|
||||
2*time.Minute,
|
||||
false)
|
||||
}
|
||||
|
||||
func waitUntilScanComplete(t *testing.T, addr string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v/pkg", addr),
|
||||
"Scan is not yet complete",
|
||||
2*time.Minute,
|
||||
true,
|
||||
)
|
||||
// setting reverse as true, which means this waits
|
||||
// until the string is not returned in the response anymore
|
||||
}
|
||||
|
||||
const pollInterval = 200 * time.Millisecond
|
||||
|
||||
func waitForServer(t *testing.T, url, match string, timeout time.Duration, reverse bool) {
|
||||
// "health check" duplicated from x/tools/cmd/tipgodoc/tip.go
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
time.Sleep(pollInterval)
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rbody, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err == nil && res.StatusCode == http.StatusOK {
|
||||
if bytes.Contains(rbody, []byte(match)) && !reverse {
|
||||
return
|
||||
}
|
||||
if !bytes.Contains(rbody, []byte(match)) && reverse {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("Server failed to respond in %v", timeout)
|
||||
}
|
||||
|
||||
// hasTag checks whether a given release tag is contained in the current version
|
||||
// of the go binary.
|
||||
func hasTag(t string) bool {
|
||||
for _, v := range build.Default.ReleaseTags {
|
||||
if t == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func killAndWait(cmd *exec.Cmd) {
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWeb(t *testing.T) {
|
||||
testWeb(t, false)
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func TestWebIndex(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in -short mode")
|
||||
}
|
||||
testWeb(t, true)
|
||||
}
|
||||
|
||||
// Basic integration test for godoc HTTP interface.
|
||||
func testWeb(t *testing.T, withIndex bool) {
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("skipping on plan9; files to start up quickly enough")
|
||||
}
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
addr := serverAddress(t)
|
||||
args := []string{fmt.Sprintf("-http=%s", addr)}
|
||||
if withIndex {
|
||||
args = append(args, "-index", "-index_interval=-1s")
|
||||
}
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Args[0] = "godoc"
|
||||
|
||||
// Set GOPATH variable to non-existing path
|
||||
// and GOPROXY=off to disable module fetches.
|
||||
// We cannot just unset GOPATH variable because godoc would default it to ~/go.
|
||||
// (We don't want the indexer looking at the local workspace during tests.)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOPATH=does_not_exist",
|
||||
"GOPROXY=off",
|
||||
"GO111MODULE=off")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start godoc: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
|
||||
if withIndex {
|
||||
waitForSearchReady(t, addr)
|
||||
} else {
|
||||
waitForServerReady(t, addr)
|
||||
waitUntilScanComplete(t, addr)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
contains []string // substring
|
||||
match []string // regexp
|
||||
notContains []string
|
||||
needIndex bool
|
||||
releaseTag string // optional release tag that must be in go/build.ReleaseTags
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
contains: []string{"Go is an open source programming language"},
|
||||
},
|
||||
{
|
||||
path: "/pkg/fmt/",
|
||||
contains: []string{"Package fmt implements formatted I/O"},
|
||||
},
|
||||
{
|
||||
path: "/src/fmt/",
|
||||
contains: []string{"scan_test.go"},
|
||||
},
|
||||
{
|
||||
path: "/src/fmt/print.go",
|
||||
contains: []string{"// Println formats using"},
|
||||
},
|
||||
{
|
||||
path: "/pkg",
|
||||
contains: []string{
|
||||
"Standard library",
|
||||
"Package fmt implements formatted I/O",
|
||||
},
|
||||
notContains: []string{
|
||||
"internal/syscall",
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/?m=all",
|
||||
contains: []string{
|
||||
"Standard library",
|
||||
"Package fmt implements formatted I/O",
|
||||
"internal/syscall/?m=all",
|
||||
},
|
||||
notContains: []string{
|
||||
"cmd/gc",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/search?q=ListenAndServe",
|
||||
contains: []string{
|
||||
"/src",
|
||||
},
|
||||
notContains: []string{
|
||||
"/pkg/bootstrap",
|
||||
},
|
||||
needIndex: true,
|
||||
},
|
||||
{
|
||||
path: "/pkg/strings/",
|
||||
contains: []string{
|
||||
`href="/src/strings/strings.go"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/cmd/compile/internal/amd64/",
|
||||
contains: []string{
|
||||
`href="/src/cmd/compile/internal/amd64/ssa.go"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/math/bits/",
|
||||
contains: []string{
|
||||
`Added in Go 1.9`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/net/",
|
||||
contains: []string{
|
||||
`// IPv6 scoped addressing zone; added in Go 1.1`,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/pkg/net/http/httptrace/",
|
||||
match: []string{
|
||||
`Got1xxResponse.*// Go 1\.11`,
|
||||
},
|
||||
releaseTag: "go1.11",
|
||||
},
|
||||
// Verify we don't add version info to a struct field added the same time
|
||||
// as the struct itself:
|
||||
{
|
||||
path: "/pkg/net/http/httptrace/",
|
||||
match: []string{
|
||||
`(?m)GotFirstResponseByte func\(\)\s*$`,
|
||||
},
|
||||
},
|
||||
// Remove trailing periods before adding semicolons:
|
||||
{
|
||||
path: "/pkg/database/sql/",
|
||||
contains: []string{
|
||||
"The number of connections currently in use; added in Go 1.11",
|
||||
"The number of idle connections; added in Go 1.11",
|
||||
},
|
||||
releaseTag: "go1.11",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if test.needIndex && !withIndex {
|
||||
continue
|
||||
}
|
||||
url := fmt.Sprintf("http://%s%s", addr, test.path)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Errorf("GET %s failed: %s", url, err)
|
||||
continue
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
strBody := string(body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
|
||||
}
|
||||
isErr := false
|
||||
for _, substr := range test.contains {
|
||||
if test.releaseTag != "" && !hasTag(test.releaseTag) {
|
||||
continue
|
||||
}
|
||||
if !bytes.Contains(body, []byte(substr)) {
|
||||
t.Errorf("GET %s: wanted substring %q in body", url, substr)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
for _, re := range test.match {
|
||||
if test.releaseTag != "" && !hasTag(test.releaseTag) {
|
||||
continue
|
||||
}
|
||||
if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
|
||||
if err != nil {
|
||||
t.Fatalf("Bad regexp %q: %v", re, err)
|
||||
}
|
||||
t.Errorf("GET %s: wanted to match %s in body", url, re)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
for _, substr := range test.notContains {
|
||||
if bytes.Contains(body, []byte(substr)) {
|
||||
t.Errorf("GET %s: didn't want substring %q in body", url, substr)
|
||||
isErr = true
|
||||
}
|
||||
}
|
||||
if isErr {
|
||||
t.Errorf("GET %s: got:\n%s", url, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic integration test for godoc -analysis=type (via HTTP interface).
|
||||
func TestTypeAnalysis(t *testing.T) {
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below
|
||||
}
|
||||
|
||||
// Write a fake GOROOT/GOPATH.
|
||||
tmpdir, err := ioutil.TempDir("", "godoc-analysis")
|
||||
if err != nil {
|
||||
t.Fatalf("ioutil.TempDir failed: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
for _, f := range []struct{ file, content string }{
|
||||
{"goroot/src/lib/lib.go", `
|
||||
package lib
|
||||
type T struct{}
|
||||
const C = 3
|
||||
var V T
|
||||
func (T) F() int { return C }
|
||||
`},
|
||||
{"gopath/src/app/main.go", `
|
||||
package main
|
||||
import "lib"
|
||||
func main() { print(lib.V) }
|
||||
`},
|
||||
} {
|
||||
file := filepath.Join(tmpdir, f.file)
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll(%s) failed: %s", filepath.Dir(file), err)
|
||||
}
|
||||
if err := ioutil.WriteFile(file, []byte(f.content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server.
|
||||
bin, cleanup := buildGodoc(t)
|
||||
defer cleanup()
|
||||
addr := serverAddress(t)
|
||||
cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type")
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOROOT=%s", filepath.Join(tmpdir, "goroot")))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%s", filepath.Join(tmpdir, "gopath")))
|
||||
cmd.Env = append(cmd.Env, "GO111MODULE=off")
|
||||
cmd.Env = append(cmd.Env, "GOPROXY=off")
|
||||
cmd.Stdout = os.Stderr
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Args[0] = "godoc"
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start godoc: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
waitForServerReady(t, addr)
|
||||
|
||||
// Wait for type analysis to complete.
|
||||
reader := bufio.NewReader(stderr)
|
||||
for {
|
||||
s, err := reader.ReadString('\n') // on Plan 9 this fails
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Fprint(os.Stderr, s)
|
||||
if strings.Contains(s, "Type analysis complete.") {
|
||||
break
|
||||
}
|
||||
}
|
||||
go io.Copy(os.Stderr, reader)
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
// Make an HTTP request and check for a regular expression match.
|
||||
// The patterns are very crude checks that basic type information
|
||||
// has been annotated onto the source view.
|
||||
tryagain:
|
||||
for _, test := range []struct{ url, pattern string }{
|
||||
{"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"},
|
||||
{"/src/lib/lib.go", "L3.*type .*type info for T.*struct"},
|
||||
{"/src/lib/lib.go", "L5.*var V .*type T struct"},
|
||||
{"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"},
|
||||
|
||||
{"/src/app/main.go", "L2.*package .*Package docs for app"},
|
||||
{"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"},
|
||||
{"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"},
|
||||
} {
|
||||
url := fmt.Sprintf("http://%s%s", addr, test.url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Errorf("GET %s failed: %s", url, err)
|
||||
continue
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Contains(body, []byte("Static analysis features")) {
|
||||
// Type analysis results usually become available within
|
||||
// ~4ms after godoc startup (for this input on my machine).
|
||||
if elapsed := time.Since(t0); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("type analysis results still unavailable after %s", elapsed)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
goto tryagain
|
||||
}
|
||||
|
||||
match, err := regexp.Match(test.pattern, body)
|
||||
if err != nil {
|
||||
t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err)
|
||||
continue
|
||||
}
|
||||
if !match {
|
||||
// This is a really ugly failure message.
|
||||
t.Errorf("GET %s: body doesn't match %q, got:\n%s",
|
||||
url, test.pattern, string(body))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Copies of functions from src/cmd/go/internal/cfg/cfg.go for
|
||||
// finding the GOROOT.
|
||||
// Keep them in sync until support is moved to a common place, if ever.
|
||||
|
||||
func findGOROOT() string {
|
||||
if env := os.Getenv("GOROOT"); env != "" {
|
||||
return filepath.Clean(env)
|
||||
}
|
||||
def := filepath.Clean(runtime.GOROOT())
|
||||
if runtime.Compiler == "gccgo" {
|
||||
// gccgo has no real GOROOT, and it certainly doesn't
|
||||
// depend on the executable's location.
|
||||
return def
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
exe, err = filepath.Abs(exe)
|
||||
if err == nil {
|
||||
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
|
||||
// If def (runtime.GOROOT()) and dir are the same
|
||||
// directory, prefer the spelling used in def.
|
||||
if isSameDir(def, dir) {
|
||||
return def
|
||||
}
|
||||
return dir
|
||||
}
|
||||
exe, err = filepath.EvalSymlinks(exe)
|
||||
if err == nil {
|
||||
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
|
||||
if isSameDir(def, dir) {
|
||||
return def
|
||||
}
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// isGOROOT reports whether path looks like a GOROOT.
|
||||
//
|
||||
// It does this by looking for the path/pkg/tool directory,
|
||||
// which is necessary for useful operation of the cmd/go tool,
|
||||
// and is not typically present in a GOPATH.
|
||||
func isGOROOT(path string) bool {
|
||||
stat, err := os.Stat(filepath.Join(path, "pkg", "tool"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return stat.IsDir()
|
||||
}
|
||||
|
||||
// isSameDir reports whether dir1 and dir2 are the same directory.
|
||||
func isSameDir(dir1, dir2 string) bool {
|
||||
if dir1 == dir2 {
|
||||
return true
|
||||
}
|
||||
info1, err1 := os.Stat(dir1)
|
||||
info2, err2 := os.Stat(dir2)
|
||||
return err1 == nil && err2 == nil && os.SameFile(info1, info2)
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
|
||||
// files named $GOROOT/doc/codewalk/*.xml.
|
||||
// For an example and a description of the format, see
|
||||
// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
|
||||
// and see http://localhost:6060/doc/codewalk/codewalk .
|
||||
// That page is itself a codewalk; the source code for it is
|
||||
// $GOROOT/doc/codewalk/codewalk.xml.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go/format"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/env"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/redirect"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
)
|
||||
|
||||
var (
|
||||
pres *godoc.Presentation
|
||||
fs = vfs.NameSpace{}
|
||||
)
|
||||
|
||||
// hostEnforcerHandler redirects requests to "http://foo.golang.org/bar"
|
||||
// to "https://golang.org/bar".
|
||||
// It permits requests to the host "godoc-test.golang.org" for testing and
|
||||
// golang.google.cn for Chinese users.
|
||||
type hostEnforcerHandler struct {
|
||||
h http.Handler
|
||||
}
|
||||
|
||||
func (h hostEnforcerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !env.EnforceHosts() {
|
||||
h.h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if !h.isHTTPS(r) || !h.validHost(r.Host) {
|
||||
r.URL.Scheme = "https"
|
||||
if h.validHost(r.Host) {
|
||||
r.URL.Host = r.Host
|
||||
} else {
|
||||
r.URL.Host = "golang.org"
|
||||
}
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
||||
h.h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h hostEnforcerHandler) isHTTPS(r *http.Request) bool {
|
||||
return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
|
||||
}
|
||||
|
||||
func (h hostEnforcerHandler) validHost(host string) bool {
|
||||
switch strings.ToLower(host) {
|
||||
case "golang.org", "golang.google.cn":
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(host, "-dot-golang-org.appspot.com") {
|
||||
// staging/test
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func registerHandlers(pres *godoc.Presentation) *http.ServeMux {
|
||||
if pres == nil {
|
||||
panic("nil Presentation")
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/doc/codewalk/", codewalk)
|
||||
mux.Handle("/doc/play/", pres.FileServer())
|
||||
mux.Handle("/robots.txt", pres.FileServer())
|
||||
mux.Handle("/", pres)
|
||||
mux.Handle("/pkg/C/", redirect.Handler("/cmd/cgo/"))
|
||||
mux.HandleFunc("/fmt", fmtHandler)
|
||||
redirect.Register(mux)
|
||||
|
||||
http.Handle("/", hostEnforcerHandler{mux})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func readTemplate(name string) *template.Template {
|
||||
if pres == nil {
|
||||
panic("no global Presentation set yet")
|
||||
}
|
||||
path := "lib/godoc/" + name
|
||||
|
||||
// use underlying file system fs to read the template file
|
||||
// (cannot use template ParseFile functions directly)
|
||||
data, err := vfs.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
log.Fatal("readTemplate: ", err)
|
||||
}
|
||||
// be explicit with errors (for app engine use)
|
||||
t, err := template.New(name).Funcs(pres.FuncMap()).Parse(string(data))
|
||||
if err != nil {
|
||||
log.Fatal("readTemplate: ", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func readTemplates(p *godoc.Presentation) {
|
||||
codewalkHTML = readTemplate("codewalk.html")
|
||||
codewalkdirHTML = readTemplate("codewalkdir.html")
|
||||
p.CallGraphHTML = readTemplate("callgraph.html")
|
||||
p.DirlistHTML = readTemplate("dirlist.html")
|
||||
p.ErrorHTML = readTemplate("error.html")
|
||||
p.ExampleHTML = readTemplate("example.html")
|
||||
p.GodocHTML = readTemplate("godoc.html")
|
||||
p.ImplementsHTML = readTemplate("implements.html")
|
||||
p.MethodSetHTML = readTemplate("methodset.html")
|
||||
p.PackageHTML = readTemplate("package.html")
|
||||
p.PackageRootHTML = readTemplate("packageroot.html")
|
||||
p.SearchHTML = readTemplate("search.html")
|
||||
p.SearchDocHTML = readTemplate("searchdoc.html")
|
||||
p.SearchCodeHTML = readTemplate("searchcode.html")
|
||||
p.SearchTxtHTML = readTemplate("searchtxt.html")
|
||||
p.SearchDescXML = readTemplate("opensearch.xml")
|
||||
}
|
||||
|
||||
type fmtResponse struct {
|
||||
Body string
|
||||
Error string
|
||||
}
|
||||
|
||||
// fmtHandler takes a Go program in its "body" form value, formats it with
|
||||
// standard gofmt formatting, and writes a fmtResponse as a JSON object.
|
||||
func fmtHandler(w http.ResponseWriter, r *http.Request) {
|
||||
resp := new(fmtResponse)
|
||||
body, err := format.Source([]byte(r.FormValue("body")))
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Body = string(body)
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
Двоичный файл не отображается.
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2015 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.
|
||||
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
func indexDirectoryDefault(dir string) bool {
|
||||
return dir != "/pkg" && !strings.HasPrefix(dir, "/pkg/")
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
// Copyright 2009 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.
|
||||
|
||||
// godoc: Go Documentation Server
|
||||
|
||||
// Web server tree:
|
||||
//
|
||||
// http://godoc/ main landing page
|
||||
// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, etc.
|
||||
// http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed
|
||||
// http://godoc/cmd/ serve documentation about commands
|
||||
// http://godoc/pkg/ serve documentation about packages
|
||||
// (idea is if you say import "compress/zlib", you go to
|
||||
// http://godoc/pkg/compress/zlib)
|
||||
//
|
||||
|
||||
// +build !golangorg
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
_ "expvar" // to serve /debug/vars
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // to serve /debug/pprof/*
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/website/cmd/golangorg/godoc"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/analysis"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
|
||||
"golang.org/x/website/cmd/golangorg/godoc/vfs/zipfs"
|
||||
"golang.org/x/website/content/static"
|
||||
)
|
||||
|
||||
const defaultAddr = "localhost:6060" // default webserver address
|
||||
|
||||
var (
|
||||
// file system to serve
|
||||
// (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh -i favicon.ico)
|
||||
zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty")
|
||||
|
||||
// file-based index
|
||||
writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files")
|
||||
|
||||
analysisFlag = flag.String("analysis", "", `comma-separated list of analyses to perform (supported: type, pointer). See http://golang.org/lib/godoc/analysis/help.html`)
|
||||
|
||||
// network
|
||||
httpAddr = flag.String("http", defaultAddr, "HTTP service address")
|
||||
|
||||
// layout control
|
||||
urlFlag = flag.String("url", "", "print HTML for named URL")
|
||||
|
||||
verbose = flag.Bool("v", false, "verbose mode")
|
||||
|
||||
// file system roots
|
||||
// TODO(gri) consider the invariant that goroot always end in '/'
|
||||
goroot = flag.String("goroot", findGOROOT(), "Go root directory")
|
||||
|
||||
// layout control
|
||||
showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings")
|
||||
templateDir = flag.String("templates", "", "load templates/JS/CSS from disk in this directory")
|
||||
showPlayground = flag.Bool("play", false, "enable playground")
|
||||
declLinks = flag.Bool("links", true, "link identifiers to their declarations")
|
||||
|
||||
// search index
|
||||
indexEnabled = flag.Bool("index", false, "enable search index")
|
||||
indexFiles = flag.String("index_files", "", "glob pattern specifying index files; if not empty, the index is read from these files in sorted order")
|
||||
indexInterval = flag.Duration("index_interval", 0, "interval of indexing; 0 for default (5m), negative to only index once at startup")
|
||||
maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown")
|
||||
indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle")
|
||||
|
||||
// source code notes
|
||||
notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show")
|
||||
)
|
||||
|
||||
func getFullPath(relPath string) string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
if gopath == "" {
|
||||
gopath = build.Default.GOPATH
|
||||
}
|
||||
return gopath + relPath
|
||||
}
|
||||
|
||||
// An httpResponseRecorder is an http.ResponseWriter
|
||||
type httpResponseRecorder struct {
|
||||
body *bytes.Buffer
|
||||
header http.Header
|
||||
code int
|
||||
}
|
||||
|
||||
func (w *httpResponseRecorder) Header() http.Header { return w.header }
|
||||
func (w *httpResponseRecorder) Write(b []byte) (int, error) { return len(b), nil }
|
||||
func (w *httpResponseRecorder) WriteHeader(code int) { w.code = code }
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: godoc -http="+defaultAddr+"\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func loggingHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("%s\t%s", req.RemoteAddr, req.URL)
|
||||
h.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func handleURLFlag() {
|
||||
// Try up to 10 fetches, following redirects.
|
||||
urlstr := *urlFlag
|
||||
for i := 0; i < 10; i++ {
|
||||
// Prepare request.
|
||||
u, err := url.Parse(urlstr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
}
|
||||
|
||||
// Invoke default HTTP handler to serve request
|
||||
// to our buffering httpWriter.
|
||||
w := &httpResponseRecorder{code: 200, header: make(http.Header), body: new(bytes.Buffer)}
|
||||
http.DefaultServeMux.ServeHTTP(w, req)
|
||||
|
||||
// Return data, error, or follow redirect.
|
||||
switch w.code {
|
||||
case 200: // ok
|
||||
os.Stdout.Write(w.body.Bytes())
|
||||
return
|
||||
case 301, 302, 303, 307: // redirect
|
||||
redirect := w.header.Get("Location")
|
||||
if redirect == "" {
|
||||
log.Fatalf("HTTP %d without Location header", w.code)
|
||||
}
|
||||
urlstr = redirect
|
||||
default:
|
||||
log.Fatalf("HTTP error %d", w.code)
|
||||
}
|
||||
}
|
||||
log.Fatalf("too many redirects")
|
||||
}
|
||||
|
||||
func initCorpus(corpus *godoc.Corpus) {
|
||||
err := corpus.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if certInit != nil {
|
||||
certInit()
|
||||
}
|
||||
|
||||
playEnabled = *showPlayground
|
||||
|
||||
// Check usage: server and no args.
|
||||
if (*httpAddr != "" || *urlFlag != "") && (flag.NArg() > 0) {
|
||||
fmt.Fprintln(os.Stderr, "Unexpected arguments.")
|
||||
usage()
|
||||
}
|
||||
|
||||
// Check usage: command line args or index creation mode.
|
||||
if (*httpAddr != "" || *urlFlag != "") != (flag.NArg() == 0) && !*writeIndex {
|
||||
fmt.Fprintln(os.Stderr, "missing args.")
|
||||
usage()
|
||||
}
|
||||
|
||||
// Set the resolved goroot.
|
||||
vfs.GOROOT = *goroot
|
||||
|
||||
fsGate := make(chan bool, 20)
|
||||
|
||||
// Determine file system to use.
|
||||
if *zipfile == "" {
|
||||
// use file system of underlying OS
|
||||
rootfs := gatefs.New(vfs.OS(*goroot), fsGate)
|
||||
fs.Bind("/", rootfs, "/", vfs.BindReplace)
|
||||
} else {
|
||||
// use file system specified via .zip file (path separator must be '/')
|
||||
rc, err := zip.OpenReader(*zipfile)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %s\n", *zipfile, err)
|
||||
}
|
||||
defer rc.Close() // be nice (e.g., -writeIndex mode)
|
||||
fs.Bind("/", zipfs.New(rc, *zipfile), *goroot, vfs.BindReplace)
|
||||
}
|
||||
if *templateDir != "" {
|
||||
fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore)
|
||||
} else {
|
||||
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
|
||||
}
|
||||
|
||||
// Bind $GOPATH trees into Go root.
|
||||
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
||||
fs.Bind("/src", gatefs.New(vfs.OS(p), fsGate), "/src", vfs.BindAfter)
|
||||
}
|
||||
|
||||
// adding ability to specify local doc directory
|
||||
docPath := getFullPath("/src/golang.org/x/website/content/doc")
|
||||
fs.Bind("/doc", gatefs.New(vfs.OS(docPath), fsGate), "/", vfs.BindBefore)
|
||||
|
||||
webroot := getFullPath("/src/golang.org/x/website")
|
||||
fs.Bind("/robots.txt", gatefs.New(vfs.OS(webroot), fsGate), "/robots.txt", vfs.BindBefore)
|
||||
fs.Bind("/favicon.ico", gatefs.New(vfs.OS(webroot), fsGate), "/favicon.ico", vfs.BindBefore)
|
||||
|
||||
var typeAnalysis, pointerAnalysis bool
|
||||
if *analysisFlag != "" {
|
||||
for _, a := range strings.Split(*analysisFlag, ",") {
|
||||
switch a {
|
||||
case "type":
|
||||
typeAnalysis = true
|
||||
case "pointer":
|
||||
pointerAnalysis = true
|
||||
default:
|
||||
log.Fatalf("unknown analysis: %s", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
corpus := godoc.NewCorpus(fs)
|
||||
corpus.Verbose = *verbose
|
||||
corpus.MaxResults = *maxResults
|
||||
corpus.IndexEnabled = *indexEnabled
|
||||
if *maxResults == 0 {
|
||||
corpus.IndexFullText = false
|
||||
}
|
||||
corpus.IndexFiles = *indexFiles
|
||||
corpus.IndexDirectory = indexDirectoryDefault
|
||||
corpus.IndexThrottle = *indexThrottle
|
||||
corpus.IndexInterval = *indexInterval
|
||||
if *writeIndex {
|
||||
corpus.IndexThrottle = 1.0
|
||||
corpus.IndexEnabled = true
|
||||
initCorpus(corpus)
|
||||
} else {
|
||||
go initCorpus(corpus)
|
||||
}
|
||||
|
||||
// Initialize the version info before readTemplates, which saves
|
||||
// the map value in a method value.
|
||||
corpus.InitVersionInfo()
|
||||
|
||||
pres = godoc.NewPresentation(corpus)
|
||||
pres.ShowTimestamps = *showTimestamps
|
||||
pres.ShowPlayground = *showPlayground
|
||||
pres.DeclLinks = *declLinks
|
||||
if *notesRx != "" {
|
||||
pres.NotesRx = regexp.MustCompile(*notesRx)
|
||||
}
|
||||
|
||||
readTemplates(pres)
|
||||
registerHandlers(pres)
|
||||
|
||||
if *writeIndex {
|
||||
// Write search index and exit.
|
||||
if *indexFiles == "" {
|
||||
log.Fatal("no index file specified")
|
||||
}
|
||||
|
||||
log.Println("initialize file systems")
|
||||
*verbose = true // want to see what happens
|
||||
|
||||
corpus.UpdateIndex()
|
||||
|
||||
log.Println("writing index file", *indexFiles)
|
||||
f, err := os.Create(*indexFiles)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
index, _ := corpus.CurrentIndex()
|
||||
_, err = index.WriteTo(f)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
return
|
||||
}
|
||||
|
||||
// Print content that would be served at the URL *urlFlag.
|
||||
if *urlFlag != "" {
|
||||
handleURLFlag()
|
||||
return
|
||||
}
|
||||
|
||||
var handler http.Handler = http.DefaultServeMux
|
||||
if *verbose {
|
||||
log.Printf("Go Documentation Server")
|
||||
log.Printf("version = %s", runtime.Version())
|
||||
log.Printf("address = %s", *httpAddr)
|
||||
log.Printf("goroot = %s", *goroot)
|
||||
switch {
|
||||
case !*indexEnabled:
|
||||
log.Print("search index disabled")
|
||||
case *maxResults > 0:
|
||||
log.Printf("full text index enabled (maxresults = %d)", *maxResults)
|
||||
default:
|
||||
log.Print("identifier search index enabled")
|
||||
}
|
||||
fs.Fprint(os.Stderr)
|
||||
handler = loggingHandler(handler)
|
||||
}
|
||||
|
||||
// Initialize search index.
|
||||
if *indexEnabled {
|
||||
go corpus.RunIndexer()
|
||||
}
|
||||
|
||||
// Start type/pointer analysis.
|
||||
if typeAnalysis || pointerAnalysis {
|
||||
go analysis.Run(pointerAnalysis, &corpus.Analysis)
|
||||
}
|
||||
|
||||
if runHTTPS != nil {
|
||||
go func() {
|
||||
if err := runHTTPS(handler); err != nil {
|
||||
log.Fatalf("ListenAndServe TLS: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start http server.
|
||||
if *verbose {
|
||||
log.Println("starting HTTP server")
|
||||
}
|
||||
if wrapHTTPMux != nil {
|
||||
handler = wrapHTTPMux(handler)
|
||||
}
|
||||
if err := http.ListenAndServe(*httpAddr, handler); err != nil {
|
||||
log.Fatalf("ListenAndServe %s: %v", *httpAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks that are set non-nil in autocert.go if the "autocert" build tag
|
||||
// is used.
|
||||
var (
|
||||
certInit func()
|
||||
runHTTPS func(http.Handler) error
|
||||
wrapHTTPMux func(http.Handler) http.Handler
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2012 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.
|
||||
|
||||
// +build !golangorg
|
||||
|
||||
package main
|
||||
|
||||
// This package registers "/compile" and "/share" handlers
|
||||
// that redirect to the golang.org playground.
|
||||
import _ "golang.org/x/tools/playground"
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// Regression tests to run against a production instance of godoc.
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var host = flag.String("regtest.host", "", "host to run regression test against")
|
||||
|
||||
func init() {
|
||||
flag.Parse()
|
||||
*host = strings.TrimSuffix(*host, "/")
|
||||
}
|
||||
|
||||
func TestLiveServer(t *testing.T) {
|
||||
if *host == "" {
|
||||
t.Skip("regtest.host flag missing.")
|
||||
}
|
||||
substringTests := []struct {
|
||||
Message string
|
||||
Path string
|
||||
Substring string
|
||||
Regexp string
|
||||
NoAnalytics bool // expect the response to not contain GA.
|
||||
PostBody string
|
||||
StatusCode int // if 0, expect 2xx status code.
|
||||
}{
|
||||
{
|
||||
Path: "/doc/faq",
|
||||
Substring: "What is the purpose of the project",
|
||||
},
|
||||
{
|
||||
Path: "/pkg/",
|
||||
Substring: "Package tar",
|
||||
},
|
||||
{
|
||||
Path: "/pkg/os/",
|
||||
Substring: "func Open",
|
||||
},
|
||||
{
|
||||
Path: "/pkg/net/http/",
|
||||
Substring: `title="Added in Go 1.11"`,
|
||||
Message: "version information not present - failed InitVersionInfo?",
|
||||
},
|
||||
{
|
||||
Path: "/robots.txt",
|
||||
Substring: "Disallow: /search",
|
||||
Message: "robots not present - not deployed from Dockerfile?",
|
||||
NoAnalytics: true,
|
||||
},
|
||||
{
|
||||
Path: "/change/75944e2e3a63",
|
||||
Substring: "bdb10cf",
|
||||
Message: "no change redirect - hg to git mapping not registered?",
|
||||
NoAnalytics: true,
|
||||
StatusCode: 302,
|
||||
},
|
||||
{
|
||||
Path: "/dl/",
|
||||
Substring: "go1.11.windows-amd64.msi",
|
||||
Message: "missing data on dl page - misconfiguration of datastore?",
|
||||
},
|
||||
{
|
||||
Path: "/dl/?mode=json",
|
||||
Substring: ".windows-amd64.msi",
|
||||
NoAnalytics: true,
|
||||
},
|
||||
{
|
||||
Message: "broken shortlinks - misconfiguration of datastore or memcache?",
|
||||
Path: "/s/go2design",
|
||||
Regexp: "proposal.*Found",
|
||||
NoAnalytics: true,
|
||||
StatusCode: 302,
|
||||
},
|
||||
{
|
||||
Message: "incorrect search result - broken index?",
|
||||
Path: "/search?q=IsDir",
|
||||
Substring: "src/os/types.go",
|
||||
},
|
||||
{
|
||||
Path: "/compile",
|
||||
PostBody: "body=" + url.QueryEscape("package main; func main() { print(6*7); }"),
|
||||
Regexp: `^{"compile_errors":"","output":"42"}$`,
|
||||
NoAnalytics: true,
|
||||
},
|
||||
{
|
||||
Path: "/compile",
|
||||
PostBody: "body=" + url.QueryEscape("//empty"),
|
||||
Substring: "expected 'package', found 'EOF'",
|
||||
NoAnalytics: true,
|
||||
},
|
||||
{
|
||||
Path: "/compile",
|
||||
PostBody: "version=2&body=package+main%3Bimport+(%22fmt%22%3B%22time%22)%3Bfunc+main()%7Bfmt.Print(%22A%22)%3Btime.Sleep(time.Second)%3Bfmt.Print(%22B%22)%7D",
|
||||
Regexp: `^{"Errors":"","Events":\[{"Message":"A","Kind":"stdout","Delay":0},{"Message":"B","Kind":"stdout","Delay":1000000000}\]}$`,
|
||||
NoAnalytics: true,
|
||||
},
|
||||
{
|
||||
Path: "/share",
|
||||
PostBody: "package main",
|
||||
Substring: "", // just check it is a 2xx.
|
||||
NoAnalytics: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range substringTests {
|
||||
t.Run(tc.Path, func(t *testing.T) {
|
||||
method := "GET"
|
||||
var reqBody io.Reader
|
||||
if tc.PostBody != "" {
|
||||
method = "POST"
|
||||
reqBody = strings.NewReader(tc.PostBody)
|
||||
}
|
||||
req, err := http.NewRequest(method, *host+tc.Path, reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
if reqBody != nil {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
resp, err := http.DefaultTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatalf("RoundTrip: %v", err)
|
||||
}
|
||||
if tc.StatusCode == 0 {
|
||||
if resp.StatusCode > 299 {
|
||||
t.Errorf("Non-OK status code: %v", resp.StatusCode)
|
||||
}
|
||||
} else if tc.StatusCode != resp.StatusCode {
|
||||
t.Errorf("StatusCode; got %v, want %v", resp.StatusCode, tc.StatusCode)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll: %v", err)
|
||||
}
|
||||
|
||||
const googleAnalyticsID = "UA-11222381-2" // golang.org analytics ID
|
||||
if !tc.NoAnalytics && !bytes.Contains(body, []byte(googleAnalyticsID)) {
|
||||
t.Errorf("want response to contain analytics tracking ID")
|
||||
}
|
||||
|
||||
if tc.Substring != "" {
|
||||
tc.Regexp = regexp.QuoteMeta(tc.Substring)
|
||||
}
|
||||
re := regexp.MustCompile(tc.Regexp)
|
||||
|
||||
if !re.Match(body) {
|
||||
t.Log("------ actual output -------")
|
||||
t.Log(string(body))
|
||||
t.Log("----------------------------")
|
||||
if tc.Message != "" {
|
||||
t.Log(tc.Message)
|
||||
}
|
||||
t.Fatalf("wanted response to match %s", tc.Regexp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// 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.
|
||||
|
||||
// This file contains the handlers that serve go-import redirects for Go
|
||||
// sub-repositories. It specifies the mapping from import paths like
|
||||
// "golang.org/x/tools" to the actual repository locations.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const xPrefix = "/x/"
|
||||
|
||||
type xRepo struct {
|
||||
URL, VCS string
|
||||
}
|
||||
|
||||
var xMap = map[string]xRepo{
|
||||
"codereview": {"https://code.google.com/p/go.codereview", "hg"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
|
||||
"arch": {"https://go.googlesource.com/arch", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"benchmarks": {"https://go.googlesource.com/benchmarks", "git"},
|
||||
"blog": {"https://go.googlesource.com/blog", "git"},
|
||||
"build": {"https://go.googlesource.com/build", "git"},
|
||||
"crypto": {"https://go.googlesource.com/crypto", "git"},
|
||||
"debug": {"https://go.googlesource.com/debug", "git"},
|
||||
"exp": {"https://go.googlesource.com/exp", "git"},
|
||||
"image": {"https://go.googlesource.com/image", "git"},
|
||||
"lint": {"https://go.googlesource.com/lint", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"mobile": {"https://go.googlesource.com/mobile", "git"},
|
||||
"net": {"https://go.googlesource.com/net", "git"},
|
||||
"oauth2": {"https://go.googlesource.com/oauth2", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"perf": {"https://go.googlesource.com/perf", "git"},
|
||||
"playground": {"https://go.googlesource.com/playground", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"review": {"https://go.googlesource.com/review", "git"},
|
||||
"sync": {"https://go.googlesource.com/sync", "git"},
|
||||
"sys": {"https://go.googlesource.com/sys", "git"},
|
||||
"talks": {"https://go.googlesource.com/talks", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"term": {"https://go.googlesource.com/term", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"text": {"https://go.googlesource.com/text", "git"},
|
||||
"time": {"https://go.googlesource.com/time", "git"},
|
||||
"tools": {"https://go.googlesource.com/tools", "git"},
|
||||
"tour": {"https://go.googlesource.com/tour", "git"},
|
||||
"vgo": {"https://go.googlesource.com/vgo", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
"website": {"https://go.googlesource.com/website", "git"}, // Not included at https://golang.org/pkg/#subrepo.
|
||||
}
|
||||
|
||||
func init() {
|
||||
http.HandleFunc(xPrefix, xHandler)
|
||||
}
|
||||
|
||||
func xHandler(w http.ResponseWriter, r *http.Request) {
|
||||
head, tail := strings.TrimPrefix(r.URL.Path, xPrefix), ""
|
||||
if i := strings.Index(head, "/"); i != -1 {
|
||||
head, tail = head[:i], head[i:]
|
||||
}
|
||||
if head == "" {
|
||||
http.Redirect(w, r, "https://godoc.org/-/subrepo", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
repo, ok := xMap[head]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data := struct {
|
||||
Prefix, Head, Tail string
|
||||
Repo xRepo
|
||||
}{xPrefix, head, tail, repo}
|
||||
if err := xTemplate.Execute(w, data); err != nil {
|
||||
log.Println("xHandler:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var xTemplate = template.Must(template.New("x").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="go-import" content="golang.org{{.Prefix}}{{.Head}} {{.Repo.VCS}} {{.Repo.URL}}">
|
||||
<meta name="go-source" content="golang.org{{.Prefix}}{{.Head}} https://github.com/golang/{{.Head}}/ https://github.com/golang/{{.Head}}/tree/master{/dir} https://github.com/golang/{{.Head}}/blob/master{/dir}/{file}#L{line}">
|
||||
<meta http-equiv="refresh" content="0; url=https://godoc.org/golang.org{{.Prefix}}{{.Head}}{{.Tail}}">
|
||||
</head>
|
||||
<body>
|
||||
Nothing to see here; <a href="https://godoc.org/golang.org{{.Prefix}}{{.Head}}{{.Tail}}">move along</a>.
|
||||
</body>
|
||||
</html>
|
||||
`))
|
|
@ -0,0 +1 @@
|
|||
issuerepo: golang/go
|
|
@ -0,0 +1,30 @@
|
|||
# content
|
||||
|
||||
This directory contains doc/, static/, favicon.ico, and robots.txt. The
|
||||
executable lives at golang.org/x/website/cmd/golangorg.
|
||||
|
||||
## Development mode
|
||||
|
||||
In production, CSS/JS/template assets need to be compiled into the golangorg
|
||||
binary. It can be tedious to recompile assets every time, but you can pass a
|
||||
flag to load CSS/JS/templates from disk every time a page loads:
|
||||
|
||||
```
|
||||
golangorg -templates=$GOPATH/src/golang.org/x/website/content/static -http=:6060
|
||||
```
|
||||
|
||||
## Recompiling static assets
|
||||
|
||||
The files that live at `static/style.css`, `static/jquery.js` and so on are not
|
||||
present in the final binary. They are placed into `static/static.go` by running
|
||||
`go generate`. So to compile a change and test it in your browser:
|
||||
|
||||
1) Make changes to e.g. `static/style.css`.
|
||||
|
||||
2) Run `go generate golang.org/x/website/content/static` so `static/static.go` picks
|
||||
up the change.
|
||||
|
||||
3) Run `go install golang.org/x/website/cmd/golangorg` so the compiled `golangorg` binary picks up the change.
|
||||
|
||||
4) Run `golangorg -http=:6060` and view your changes in the browser. You may need
|
||||
to disable your browser's cache to avoid reloading a stale file.
|
|
@ -0,0 +1,254 @@
|
|||
<!--{
|
||||
"title": "About the go command"
|
||||
}-->
|
||||
|
||||
<p>The Go distribution includes a command, named
|
||||
"<code><a href="/cmd/go/">go</a></code>", that
|
||||
automates the downloading, building, installation, and testing of Go packages
|
||||
and commands. This document talks about why we wrote a new command, what it
|
||||
is, what it's not, and how to use it.</p>
|
||||
|
||||
<h2>Motivation</h2>
|
||||
|
||||
<p>You might have seen early Go talks in which Rob Pike jokes that the idea
|
||||
for Go arose while waiting for a large Google server to compile. That
|
||||
really was the motivation for Go: to build a language that worked well
|
||||
for building the large software that Google writes and runs. It was
|
||||
clear from the start that such a language must provide a way to
|
||||
express dependencies between code libraries clearly, hence the package
|
||||
grouping and the explicit import blocks. It was also clear from the
|
||||
start that you might want arbitrary syntax for describing the code
|
||||
being imported; this is why import paths are string literals.</p>
|
||||
|
||||
<p>An explicit goal for Go from the beginning was to be able to build Go
|
||||
code using only the information found in the source itself, not
|
||||
needing to write a makefile or one of the many modern replacements for
|
||||
makefiles. If Go needed a configuration file to explain how to build
|
||||
your program, then Go would have failed.</p>
|
||||
|
||||
<p>At first, there was no Go compiler, and the initial development
|
||||
focused on building one and then building libraries for it. For
|
||||
expedience, we postponed the automation of building Go code by using
|
||||
make and writing makefiles. When compiling a single package involved
|
||||
multiple invocations of the Go compiler, we even used a program to
|
||||
write the makefiles for us. You can find it if you dig through the
|
||||
repository history.</p>
|
||||
|
||||
<p>The purpose of the new go command is our return to this ideal, that Go
|
||||
programs should compile without configuration or additional effort on
|
||||
the part of the developer beyond writing the necessary import
|
||||
statements.</p>
|
||||
|
||||
<h2>Configuration versus convention</h2>
|
||||
|
||||
<p>The way to achieve the simplicity of a configuration-free system is to
|
||||
establish conventions. The system works only to the extent that those conventions
|
||||
are followed. When we first launched Go, many people published packages that
|
||||
had to be installed in certain places, under certain names, using certain build
|
||||
tools, in order to be used. That's understandable: that's the way it works in
|
||||
most other languages. Over the last few years we consistently reminded people
|
||||
about the <code>goinstall</code> command
|
||||
(now replaced by <a href="/cmd/go/#hdr-Download_and_install_packages_and_dependencies"><code>go get</code></a>)
|
||||
and its conventions: first, that the import path is derived in a known way from
|
||||
the URL of the source code; second, that the place to store the sources in
|
||||
the local file system is derived in a known way from the import path; third,
|
||||
that each directory in a source tree corresponds to a single package; and
|
||||
fourth, that the package is built using only information in the source code.
|
||||
Today, the vast majority of packages follow these conventions.
|
||||
The Go ecosystem is simpler and more powerful as a result.</p>
|
||||
|
||||
<p>We received many requests to allow a makefile in a package directory to
|
||||
provide just a little extra configuration beyond what's in the source code.
|
||||
But that would have introduced new rules. Because we did not accede to such
|
||||
requests, we were able to write the go command and eliminate our use of make
|
||||
or any other build system.</p>
|
||||
|
||||
<p>It is important to understand that the go command is not a general
|
||||
build tool. It cannot be configured and it does not attempt to build
|
||||
anything but Go packages. These are important simplifying
|
||||
assumptions: they simplify not only the implementation but also, more
|
||||
important, the use of the tool itself.</p>
|
||||
|
||||
<h2>Go's conventions</h2>
|
||||
|
||||
<p>The <code>go</code> command requires that code adheres to a few key,
|
||||
well-established conventions.</p>
|
||||
|
||||
<p>First, the import path is derived in an known way from the URL of the
|
||||
source code. For Bitbucket, GitHub, Google Code, and Launchpad, the
|
||||
root directory of the repository is identified by the repository's
|
||||
main URL, without the <code>http://</code> prefix. Subdirectories are named by
|
||||
adding to that path.
|
||||
For example, the Go example programs are obtained by running</p>
|
||||
|
||||
<pre>
|
||||
git clone https://github.com/golang/example
|
||||
</pre>
|
||||
|
||||
<p>and thus the import path for the root directory of that repository is
|
||||
"<code>github.com/golang/example</code>".
|
||||
The <a href="https://godoc.org/github.com/golang/example/stringutil">stringutil</a>
|
||||
package is stored in a subdirectory, so its import path is
|
||||
"<code>github.com/golang/example/stringutil</code>".</p>
|
||||
|
||||
<p>These paths are on the long side, but in exchange we get an
|
||||
automatically managed name space for import paths and the ability for
|
||||
a tool like the go command to look at an unfamiliar import path and
|
||||
deduce where to obtain the source code.</p>
|
||||
|
||||
<p>Second, the place to store sources in the local file system is derived
|
||||
in a known way from the import path, specifically
|
||||
<code>$GOPATH/src/<import-path></code>.
|
||||
If unset, <code>$GOPATH</code> defaults to a subdirectory
|
||||
named <code>go</code> in the user's home directory.
|
||||
If <code>$GOPATH</code> is set to a list of paths, the go command tries
|
||||
<code><dir>/src/<import-path></code> for each of the directories in
|
||||
that list.
|
||||
</p>
|
||||
|
||||
<p>Each of those trees contains, by convention, a top-level directory named
|
||||
"<code>bin</code>", for holding compiled executables, and a top-level directory
|
||||
named "<code>pkg</code>", for holding compiled packages that can be imported,
|
||||
and the "<code>src</code>" directory, for holding package source files.
|
||||
Imposing this structure lets us keep each of these directory trees
|
||||
self-contained: the compiled form and the sources are always near each
|
||||
other.</p>
|
||||
|
||||
<p>These naming conventions also let us work in the reverse direction,
|
||||
from a directory name to its import path. This mapping is important
|
||||
for many of the go command's subcommands, as we'll see below.</p>
|
||||
|
||||
<p>Third, each directory in a source tree corresponds to a single
|
||||
package. By restricting a directory to a single package, we don't have
|
||||
to create hybrid import paths that specify first the directory and
|
||||
then the package within that directory. Also, most file management
|
||||
tools and UIs work on directories as fundamental units. Tying the
|
||||
fundamental Go unit—the package—to file system structure means
|
||||
that file system tools become Go package tools. Copying, moving, or
|
||||
deleting a package corresponds to copying, moving, or deleting a
|
||||
directory.</p>
|
||||
|
||||
<p>Fourth, each package is built using only the information present in
|
||||
the source files. This makes it much more likely that the tool will
|
||||
be able to adapt to changing build environments and conditions. For
|
||||
example, if we allowed extra configuration such as compiler flags or
|
||||
command line recipes, then that configuration would need to be updated
|
||||
each time the build tools changed; it would also be inherently tied
|
||||
to the use of a specific toolchain.</p>
|
||||
|
||||
<h2>Getting started with the go command</h2>
|
||||
|
||||
<p>Finally, a quick tour of how to use the go command.
|
||||
As mentioned above, the default <code>$GOPATH</code> on Unix is <code>$HOME/go</code>.
|
||||
We'll store our programs there.
|
||||
To use a different location, you can set <code>$GOPATH</code>;
|
||||
see <a href="/doc/code.html">How to Write Go Code</a> for details.
|
||||
|
||||
<p>We first add some source code. Suppose we want to use
|
||||
the indexing library from the codesearch project along with a left-leaning
|
||||
red-black tree. We can install both with the "<code>go get</code>"
|
||||
subcommand:</p>
|
||||
|
||||
<pre>
|
||||
$ go get github.com/google/codesearch/index
|
||||
$ go get github.com/petar/GoLLRB/llrb
|
||||
$
|
||||
</pre>
|
||||
|
||||
<p>Both of these projects are now downloaded and installed into <code>$HOME/go</code>,
|
||||
which contains the two directories
|
||||
<code>src/github.com/google/codesearch/index/</code> and
|
||||
<code>src/github.com/petar/GoLLRB/llrb/</code>, along with the compiled
|
||||
packages (in <code>pkg/</code>) for those libraries and their dependencies.</p>
|
||||
|
||||
<p>Because we used version control systems (Mercurial and Git) to check
|
||||
out the sources, the source tree also contains the other files in the
|
||||
corresponding repositories, such as related packages. The "<code>go list</code>"
|
||||
subcommand lists the import paths corresponding to its arguments, and
|
||||
the pattern "<code>./...</code>" means start in the current directory
|
||||
("<code>./</code>") and find all packages below that directory
|
||||
("<code>...</code>"):</p>
|
||||
|
||||
<pre>
|
||||
$ cd $HOME/go/src
|
||||
$ go list ./...
|
||||
github.com/google/codesearch/cmd/cgrep
|
||||
github.com/google/codesearch/cmd/cindex
|
||||
github.com/google/codesearch/cmd/csearch
|
||||
github.com/google/codesearch/index
|
||||
github.com/google/codesearch/regexp
|
||||
github.com/google/codesearch/sparse
|
||||
github.com/petar/GoLLRB/example
|
||||
github.com/petar/GoLLRB/llrb
|
||||
$
|
||||
</pre>
|
||||
|
||||
<p>We can also test those packages:</p>
|
||||
|
||||
<pre>
|
||||
$ go test ./...
|
||||
? github.com/google/codesearch/cmd/cgrep [no test files]
|
||||
? github.com/google/codesearch/cmd/cindex [no test files]
|
||||
? github.com/google/codesearch/cmd/csearch [no test files]
|
||||
ok github.com/google/codesearch/index 0.203s
|
||||
ok github.com/google/codesearch/regexp 0.017s
|
||||
? github.com/google/codesearch/sparse [no test files]
|
||||
? github.com/petar/GoLLRB/example [no test files]
|
||||
ok github.com/petar/GoLLRB/llrb 0.231s
|
||||
$
|
||||
</pre>
|
||||
|
||||
<p>If a go subcommand is invoked with no paths listed, it operates on the
|
||||
current directory:</p>
|
||||
|
||||
<pre>
|
||||
$ cd github.com/google/codesearch/regexp
|
||||
$ go list
|
||||
github.com/google/codesearch/regexp
|
||||
$ go test -v
|
||||
=== RUN TestNstateEnc
|
||||
--- PASS: TestNstateEnc (0.00s)
|
||||
=== RUN TestMatch
|
||||
--- PASS: TestMatch (0.00s)
|
||||
=== RUN TestGrep
|
||||
--- PASS: TestGrep (0.00s)
|
||||
PASS
|
||||
ok github.com/google/codesearch/regexp 0.018s
|
||||
$ go install
|
||||
$
|
||||
</pre>
|
||||
|
||||
<p>That "<code>go install</code>" subcommand installs the latest copy of the
|
||||
package into the pkg directory. Because the go command can analyze the
|
||||
dependency graph, "<code>go install</code>" also installs any packages that
|
||||
this package imports but that are out of date, recursively.</p>
|
||||
|
||||
<p>Notice that "<code>go install</code>" was able to determine the name of the
|
||||
import path for the package in the current directory, because of the convention
|
||||
for directory naming. It would be a little more convenient if we could pick
|
||||
the name of the directory where we kept source code, and we probably wouldn't
|
||||
pick such a long name, but that ability would require additional configuration
|
||||
and complexity in the tool. Typing an extra directory name or two is a small
|
||||
price to pay for the increased simplicity and power.</p>
|
||||
|
||||
<h2>Limitations</h2>
|
||||
|
||||
<p>As mentioned above, the go command is not a general-purpose build
|
||||
tool.
|
||||
In particular, it does not have any facility for generating Go
|
||||
source files <em>during</em> a build, although it does provide
|
||||
<a href="/cmd/go/#hdr-Generate_Go_files_by_processing_source"><code>go</code>
|
||||
<code>generate</code></a>,
|
||||
which can automate the creation of Go files <em>before</em> the build.
|
||||
For more advanced build setups, you may need to write a
|
||||
makefile (or a configuration file for the build tool of your choice)
|
||||
to run whatever tool creates the Go files and then check those generated source files
|
||||
into your repository. This is more work for you, the package author,
|
||||
but it is significantly less work for your users, who can use
|
||||
"<code>go get</code>" without needing to obtain and build
|
||||
any additional tools.</p>
|
||||
|
||||
<h2>More information</h2>
|
||||
|
||||
<p>For more information, read <a href="/doc/code.html">How to Write Go Code</a>
|
||||
and see the <a href="/cmd/go/">go command documentation</a>.</p>
|
|
@ -0,0 +1,8 @@
|
|||
<!--{
|
||||
"Title": "/doc/articles/"
|
||||
}-->
|
||||
|
||||
<p>
|
||||
See the <a href="/doc/#articles">Documents page</a> and the
|
||||
<a href="/blog/index">Blog index</a> for a complete list of Go articles.
|
||||
</p>
|
|
@ -0,0 +1,389 @@
|
|||
<!--{
|
||||
"Title": "Data Race Detector",
|
||||
"Template": true
|
||||
}-->
|
||||
|
||||
<h2 id="Introduction">Introduction</h2>
|
||||
|
||||
<p>
|
||||
Data races are among the most common and hardest to debug types of bugs in concurrent systems.
|
||||
A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write.
|
||||
See the <a href="/ref/mem/">The Go Memory Model</a> for details.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Here is an example of a data race that can lead to crashes and memory corruption:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
func main() {
|
||||
c := make(chan bool)
|
||||
m := make(map[string]string)
|
||||
go func() {
|
||||
m["1"] = "a" // First conflicting access.
|
||||
c <- true
|
||||
}()
|
||||
m["2"] = "b" // Second conflicting access.
|
||||
<-c
|
||||
for k, v := range m {
|
||||
fmt.Println(k, v)
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h2 id="Usage">Usage</h2>
|
||||
|
||||
<p>
|
||||
To help diagnose such bugs, Go includes a built-in data race detector.
|
||||
To use it, add the <code>-race</code> flag to the go command:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
$ go test -race mypkg // to test the package
|
||||
$ go run -race mysrc.go // to run the source file
|
||||
$ go build -race mycmd // to build the command
|
||||
$ go install -race mypkg // to install the package
|
||||
</pre>
|
||||
|
||||
<h2 id="Report_Format">Report Format</h2>
|
||||
|
||||
<p>
|
||||
When the race detector finds a data race in the program, it prints a report.
|
||||
The report contains stack traces for conflicting accesses, as well as stacks where the involved goroutines were created.
|
||||
Here is an example:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
WARNING: DATA RACE
|
||||
Read by goroutine 185:
|
||||
net.(*pollServer).AddFD()
|
||||
src/net/fd_unix.go:89 +0x398
|
||||
net.(*pollServer).WaitWrite()
|
||||
src/net/fd_unix.go:247 +0x45
|
||||
net.(*netFD).Write()
|
||||
src/net/fd_unix.go:540 +0x4d4
|
||||
net.(*conn).Write()
|
||||
src/net/net.go:129 +0x101
|
||||
net.func·060()
|
||||
src/net/timeout_test.go:603 +0xaf
|
||||
|
||||
Previous write by goroutine 184:
|
||||
net.setWriteDeadline()
|
||||
src/net/sockopt_posix.go:135 +0xdf
|
||||
net.setDeadline()
|
||||
src/net/sockopt_posix.go:144 +0x9c
|
||||
net.(*conn).SetDeadline()
|
||||
src/net/net.go:161 +0xe3
|
||||
net.func·061()
|
||||
src/net/timeout_test.go:616 +0x3ed
|
||||
|
||||
Goroutine 185 (running) created at:
|
||||
net.func·061()
|
||||
src/net/timeout_test.go:609 +0x288
|
||||
|
||||
Goroutine 184 (running) created at:
|
||||
net.TestProlongTimeout()
|
||||
src/net/timeout_test.go:618 +0x298
|
||||
testing.tRunner()
|
||||
src/testing/testing.go:301 +0xe8
|
||||
</pre>
|
||||
|
||||
<h2 id="Options">Options</h2>
|
||||
|
||||
<p>
|
||||
The <code>GORACE</code> environment variable sets race detector options.
|
||||
The format is:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
GORACE="option1=val1 option2=val2"
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
The options are:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<code>log_path</code> (default <code>stderr</code>): The race detector writes
|
||||
its report to a file named <code>log_path.<em>pid</em></code>.
|
||||
The special names <code>stdout</code>
|
||||
and <code>stderr</code> cause reports to be written to standard output and
|
||||
standard error, respectively.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>exitcode</code> (default <code>66</code>): The exit status to use when
|
||||
exiting after a detected race.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>strip_path_prefix</code> (default <code>""</code>): Strip this prefix
|
||||
from all reported file paths, to make reports more concise.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>history_size</code> (default <code>1</code>): The per-goroutine memory
|
||||
access history is <code>32K * 2**history_size elements</code>.
|
||||
Increasing this value can avoid a "failed to restore the stack" error in reports, at the
|
||||
cost of increased memory usage.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>halt_on_error</code> (default <code>0</code>): Controls whether the program
|
||||
exits after reporting first data race.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Example:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race
|
||||
</pre>
|
||||
|
||||
<h2 id="Excluding_Tests">Excluding Tests</h2>
|
||||
|
||||
<p>
|
||||
When you build with <code>-race</code> flag, the <code>go</code> command defines additional
|
||||
<a href="/pkg/go/build/#hdr-Build_Constraints">build tag</a> <code>race</code>.
|
||||
You can use the tag to exclude some code and tests when running the race detector.
|
||||
Some examples:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
// +build !race
|
||||
|
||||
package foo
|
||||
|
||||
// The test contains a data race. See issue 123.
|
||||
func TestFoo(t *testing.T) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// The test fails under the race detector due to timeouts.
|
||||
func TestBar(t *testing.T) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// The test takes too long under the race detector.
|
||||
func TestBaz(t *testing.T) {
|
||||
// ...
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h2 id="How_To_Use">How To Use</h2>
|
||||
|
||||
<p>
|
||||
To start, run your tests using the race detector (<code>go test -race</code>).
|
||||
The race detector only finds races that happen at runtime, so it can't find
|
||||
races in code paths that are not executed.
|
||||
If your tests have incomplete coverage,
|
||||
you may find more races by running a binary built with <code>-race</code> under a realistic
|
||||
workload.
|
||||
</p>
|
||||
|
||||
<h2 id="Typical_Data_Races">Typical Data Races</h2>
|
||||
|
||||
<p>
|
||||
Here are some typical data races. All of them can be detected with the race detector.
|
||||
</p>
|
||||
|
||||
<h3 id="Race_on_loop_counter">Race on loop counter</h3>
|
||||
|
||||
<pre>
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
fmt.Println(i) // Not the 'i' you are looking for.
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
The variable <code>i</code> in the function literal is the same variable used by the loop, so
|
||||
the read in the goroutine races with the loop increment.
|
||||
(This program typically prints 55555, not 01234.)
|
||||
The program can be fixed by making a copy of the variable:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func(j int) {
|
||||
fmt.Println(j) // Good. Read local copy of the loop counter.
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h3 id="Accidentally_shared_variable">Accidentally shared variable</h3>
|
||||
|
||||
<pre>
|
||||
// ParallelWrite writes data to file1 and file2, returns the errors.
|
||||
func ParallelWrite(data []byte) chan error {
|
||||
res := make(chan error, 2)
|
||||
f1, err := os.Create("file1")
|
||||
if err != nil {
|
||||
res <- err
|
||||
} else {
|
||||
go func() {
|
||||
// This err is shared with the main goroutine,
|
||||
// so the write races with the write below.
|
||||
_, err = f1.Write(data)
|
||||
res <- err
|
||||
f1.Close()
|
||||
}()
|
||||
}
|
||||
f2, err := os.Create("file2") // The second conflicting write to err.
|
||||
if err != nil {
|
||||
res <- err
|
||||
} else {
|
||||
go func() {
|
||||
_, err = f2.Write(data)
|
||||
res <- err
|
||||
f2.Close()
|
||||
}()
|
||||
}
|
||||
return res
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
The fix is to introduce new variables in the goroutines (note the use of <code>:=</code>):
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
...
|
||||
_, err := f1.Write(data)
|
||||
...
|
||||
_, err := f2.Write(data)
|
||||
...
|
||||
</pre>
|
||||
|
||||
<h3 id="Unprotected_global_variable">Unprotected global variable</h3>
|
||||
|
||||
<p>
|
||||
If the following code is called from several goroutines, it leads to races on the <code>service</code> map.
|
||||
Concurrent reads and writes of the same map are not safe:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
var service map[string]net.Addr
|
||||
|
||||
func RegisterService(name string, addr net.Addr) {
|
||||
service[name] = addr
|
||||
}
|
||||
|
||||
func LookupService(name string) net.Addr {
|
||||
return service[name]
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
To make the code safe, protect the accesses with a mutex:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
var (
|
||||
service map[string]net.Addr
|
||||
serviceMu sync.Mutex
|
||||
)
|
||||
|
||||
func RegisterService(name string, addr net.Addr) {
|
||||
serviceMu.Lock()
|
||||
defer serviceMu.Unlock()
|
||||
service[name] = addr
|
||||
}
|
||||
|
||||
func LookupService(name string) net.Addr {
|
||||
serviceMu.Lock()
|
||||
defer serviceMu.Unlock()
|
||||
return service[name]
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h3 id="Primitive_unprotected_variable">Primitive unprotected variable</h3>
|
||||
|
||||
<p>
|
||||
Data races can happen on variables of primitive types as well (<code>bool</code>, <code>int</code>, <code>int64</code>, etc.),
|
||||
as in this example:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
type Watchdog struct{ last int64 }
|
||||
|
||||
func (w *Watchdog) KeepAlive() {
|
||||
w.last = time.Now().UnixNano() // First conflicting access.
|
||||
}
|
||||
|
||||
func (w *Watchdog) Start() {
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
// Second conflicting access.
|
||||
if w.last < time.Now().Add(-10*time.Second).UnixNano() {
|
||||
fmt.Println("No keepalives for 10 seconds. Dying.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Even such "innocent" data races can lead to hard-to-debug problems caused by
|
||||
non-atomicity of the memory accesses,
|
||||
interference with compiler optimizations,
|
||||
or reordering issues accessing processor memory .
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A typical fix for this race is to use a channel or a mutex.
|
||||
To preserve the lock-free behavior, one can also use the
|
||||
<a href="/pkg/sync/atomic/"><code>sync/atomic</code></a> package.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
type Watchdog struct{ last int64 }
|
||||
|
||||
func (w *Watchdog) KeepAlive() {
|
||||
atomic.StoreInt64(&w.last, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (w *Watchdog) Start() {
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {
|
||||
fmt.Println("No keepalives for 10 seconds. Dying.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h2 id="Supported_Systems">Supported Systems</h2>
|
||||
|
||||
<p>
|
||||
The race detector runs on <code>darwin/amd64</code>, <code>freebsd/amd64</code>,
|
||||
<code>linux/amd64</code>, and <code>windows/amd64</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="Runtime_Overheads">Runtime Overhead</h2>
|
||||
|
||||
<p>
|
||||
The cost of race detection varies by program, but for a typical program, memory
|
||||
usage may increase by 5-10x and execution time by 2-20x.
|
||||
</p>
|
|
@ -0,0 +1,6 @@
|
|||
<h1>Editing {{.Title}}</h1>
|
||||
|
||||
<form action="/save/{{.Title}}" method="POST">
|
||||
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
|
||||
<div><input type="submit" value="Save"></div>
|
||||
</form>
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title, err := getTitle(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
|
||||
return
|
||||
}
|
||||
renderTemplate(w, "view", p)
|
||||
}
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title, err := getTitle(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
p = &Page{Title: title}
|
||||
}
|
||||
renderTemplate(w, "edit", p)
|
||||
}
|
||||
|
||||
func saveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title, err := getTitle(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
body := r.FormValue("body")
|
||||
p := &Page{Title: title, Body: []byte(body)}
|
||||
err = p.save()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+title, http.StatusFound)
|
||||
}
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
|
||||
t, err := template.ParseFiles(tmpl + ".html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = t.Execute(w, p)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
|
||||
|
||||
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
m := validPath.FindStringSubmatch(r.URL.Path)
|
||||
if m == nil {
|
||||
http.NotFound(w, r)
|
||||
return "", errors.New("Invalid Page Title")
|
||||
}
|
||||
return m[2], nil // The title is the second subexpression.
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", viewHandler)
|
||||
http.HandleFunc("/edit/", editHandler)
|
||||
http.HandleFunc("/save/", saveHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/edit/"):]
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
p = &Page{Title: title}
|
||||
}
|
||||
t, _ := template.ParseFiles("edit.html")
|
||||
t.Execute(w, p)
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/view/"):]
|
||||
p, _ := loadPage(title)
|
||||
t, _ := template.ParseFiles("view.html")
|
||||
t.Execute(w, p)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", viewHandler)
|
||||
http.HandleFunc("/edit/", editHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
|
||||
return
|
||||
}
|
||||
renderTemplate(w, "view", p)
|
||||
}
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
p = &Page{Title: title}
|
||||
}
|
||||
renderTemplate(w, "edit", p)
|
||||
}
|
||||
|
||||
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
|
||||
body := r.FormValue("body")
|
||||
p := &Page{Title: title, Body: []byte(body)}
|
||||
err := p.save()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+title, http.StatusFound)
|
||||
}
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
|
||||
t, err := template.ParseFiles(tmpl + ".html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = t.Execute(w, p)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
|
||||
|
||||
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m := validPath.FindStringSubmatch(r.URL.Path)
|
||||
if m == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fn(w, r, m[2])
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", makeHandler(viewHandler))
|
||||
http.HandleFunc("/edit/", makeHandler(editHandler))
|
||||
http.HandleFunc("/save/", makeHandler(saveHandler))
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/edit/"):]
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
p = &Page{Title: title}
|
||||
}
|
||||
renderTemplate(w, "edit", p)
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/view/"):]
|
||||
p, _ := loadPage(title)
|
||||
renderTemplate(w, "view", p)
|
||||
}
|
||||
|
||||
func saveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/save/"):]
|
||||
body := r.FormValue("body")
|
||||
p := &Page{Title: title, Body: []byte(body)}
|
||||
p.save()
|
||||
http.Redirect(w, r, "/view/"+title, http.StatusFound)
|
||||
}
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
|
||||
t, _ := template.ParseFiles(tmpl + ".html")
|
||||
t.Execute(w, p)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", viewHandler)
|
||||
http.HandleFunc("/edit/", editHandler)
|
||||
http.HandleFunc("/save/", saveHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
--- final.go 2017-08-31 13:19:00.422925489 -0700
|
||||
+++ final-test.go 2017-08-31 13:23:43.381391659 -0700
|
||||
@@ -8,6 +8,7 @@
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
+ "net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
@@ -86,5 +87,15 @@
|
||||
http.HandleFunc("/edit/", makeHandler(editHandler))
|
||||
http.HandleFunc("/save/", makeHandler(saveHandler))
|
||||
|
||||
- log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
+ l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
+ if err != nil {
|
||||
+ log.Fatal(err)
|
||||
+ }
|
||||
+ err = ioutil.WriteFile("final-test-port.txt", []byte(l.Addr().String()), 0644)
|
||||
+ if err != nil {
|
||||
+ log.Fatal(err)
|
||||
+ }
|
||||
+ s := &http.Server{}
|
||||
+ s.Serve(l)
|
||||
+ return
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
|
||||
return
|
||||
}
|
||||
renderTemplate(w, "view", p)
|
||||
}
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
p = &Page{Title: title}
|
||||
}
|
||||
renderTemplate(w, "edit", p)
|
||||
}
|
||||
|
||||
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
|
||||
body := r.FormValue("body")
|
||||
p := &Page{Title: title, Body: []byte(body)}
|
||||
err := p.save()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+title, http.StatusFound)
|
||||
}
|
||||
|
||||
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
|
||||
err := templates.ExecuteTemplate(w, tmpl+".html", p)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
|
||||
|
||||
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m := validPath.FindStringSubmatch(r.URL.Path)
|
||||
if m == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fn(w, r, m[2])
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", makeHandler(viewHandler))
|
||||
http.HandleFunc("/edit/", makeHandler(editHandler))
|
||||
http.HandleFunc("/save/", makeHandler(saveHandler))
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2011 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
post = flag.String("post", "", "urlencoded form data to POST")
|
||||
addr = flag.Bool("addr", false, "find open address and print to stdout")
|
||||
wait = flag.Duration("wait_for_port", 0, "if non-zero, the amount of time to wait for the address to become available")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *addr {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
fmt.Print(l.Addr())
|
||||
return
|
||||
}
|
||||
url := flag.Arg(0)
|
||||
if url == "" {
|
||||
log.Fatal("no url supplied")
|
||||
}
|
||||
var r *http.Response
|
||||
var err error
|
||||
loopUntil := time.Now().Add(*wait)
|
||||
for {
|
||||
if *post != "" {
|
||||
b := strings.NewReader(*post)
|
||||
r, err = http.Post(url, "application/x-www-form-urlencoded", b)
|
||||
} else {
|
||||
r, err = http.Get(url)
|
||||
}
|
||||
if err == nil || *wait == 0 || time.Now().After(loopUntil) {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
_, err = io.Copy(os.Stdout, r.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", handler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,740 @@
|
|||
<!--{
|
||||
"Title": "Writing Web Applications",
|
||||
"Template": true
|
||||
}-->
|
||||
|
||||
<h2>Introduction</h2>
|
||||
|
||||
<p>
|
||||
Covered in this tutorial:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Creating a data structure with load and save methods</li>
|
||||
<li>Using the <code>net/http</code> package to build web applications
|
||||
<li>Using the <code>html/template</code> package to process HTML templates</li>
|
||||
<li>Using the <code>regexp</code> package to validate user input</li>
|
||||
<li>Using closures</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Assumed knowledge:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Programming experience</li>
|
||||
<li>Understanding of basic web technologies (HTTP, HTML)</li>
|
||||
<li>Some UNIX/DOS command-line knowledge</li>
|
||||
</ul>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
|
||||
<p>
|
||||
At present, you need to have a FreeBSD, Linux, OS X, or Windows machine to run Go.
|
||||
We will use <code>$</code> to represent the command prompt.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Install Go (see the <a href="/doc/install">Installation Instructions</a>).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Make a new directory for this tutorial inside your <code>GOPATH</code> and cd to it:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
$ mkdir gowiki
|
||||
$ cd gowiki
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Create a file named <code>wiki.go</code>, open it in your favorite editor, and
|
||||
add the following lines:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
We import the <code>fmt</code> and <code>ioutil</code> packages from the Go
|
||||
standard library. Later, as we implement additional functionality, we will
|
||||
add more packages to this <code>import</code> declaration.
|
||||
</p>
|
||||
|
||||
<h2>Data Structures</h2>
|
||||
|
||||
<p>
|
||||
Let's start by defining the data structures. A wiki consists of a series of
|
||||
interconnected pages, each of which has a title and a body (the page content).
|
||||
Here, we define <code>Page</code> as a struct with two fields representing
|
||||
the title and body.
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part1.go" `/^type Page/` `/}/`}}
|
||||
|
||||
<p>
|
||||
The type <code>[]byte</code> means "a <code>byte</code> slice".
|
||||
(See <a href="/doc/articles/slices_usage_and_internals.html">Slices: usage and
|
||||
internals</a> for more on slices.)
|
||||
The <code>Body</code> element is a <code>[]byte</code> rather than
|
||||
<code>string</code> because that is the type expected by the <code>io</code>
|
||||
libraries we will use, as you'll see below.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <code>Page</code> struct describes how page data will be stored in memory.
|
||||
But what about persistent storage? We can address that by creating a
|
||||
<code>save</code> method on <code>Page</code>:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part1.go" `/^func.*Page.*save/` `/}/`}}
|
||||
|
||||
<p>
|
||||
This method's signature reads: "This is a method named <code>save</code> that
|
||||
takes as its receiver <code>p</code>, a pointer to <code>Page</code> . It takes
|
||||
no parameters, and returns a value of type <code>error</code>."
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This method will save the <code>Page</code>'s <code>Body</code> to a text
|
||||
file. For simplicity, we will use the <code>Title</code> as the file name.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <code>save</code> method returns an <code>error</code> value because
|
||||
that is the return type of <code>WriteFile</code> (a standard library function
|
||||
that writes a byte slice to a file). The <code>save</code> method returns the
|
||||
error value, to let the application handle it should anything go wrong while
|
||||
writing the file. If all goes well, <code>Page.save()</code> will return
|
||||
<code>nil</code> (the zero-value for pointers, interfaces, and some other
|
||||
types).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The octal integer literal <code>0600</code>, passed as the third parameter to
|
||||
<code>WriteFile</code>, indicates that the file should be created with
|
||||
read-write permissions for the current user only. (See the Unix man page
|
||||
<code>open(2)</code> for details.)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In addition to saving pages, we will want to load pages, too:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part1-noerror.go" `/^func loadPage/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The function <code>loadPage</code> constructs the file name from the title
|
||||
parameter, reads the file's contents into a new variable <code>body</code>, and
|
||||
returns a pointer to a <code>Page</code> literal constructed with the proper
|
||||
title and body values.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Functions can return multiple values. The standard library function
|
||||
<code>io.ReadFile</code> returns <code>[]byte</code> and <code>error</code>.
|
||||
In <code>loadPage</code>, error isn't being handled yet; the "blank identifier"
|
||||
represented by the underscore (<code>_</code>) symbol is used to throw away the
|
||||
error return value (in essence, assigning the value to nothing).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
But what happens if <code>ReadFile</code> encounters an error? For example,
|
||||
the file might not exist. We should not ignore such errors. Let's modify the
|
||||
function to return <code>*Page</code> and <code>error</code>.
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part1.go" `/^func loadPage/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
Callers of this function can now check the second parameter; if it is
|
||||
<code>nil</code> then it has successfully loaded a Page. If not, it will be an
|
||||
<code>error</code> that can be handled by the caller (see the
|
||||
<a href="/ref/spec#Errors">language specification</a> for details).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
At this point we have a simple data structure and the ability to save to and
|
||||
load from a file. Let's write a <code>main</code> function to test what we've
|
||||
written:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part1.go" `/^func main/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
After compiling and executing this code, a file named <code>TestPage.txt</code>
|
||||
would be created, containing the contents of <code>p1</code>. The file would
|
||||
then be read into the struct <code>p2</code>, and its <code>Body</code> element
|
||||
printed to the screen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can compile and run the program like this:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
$ go build wiki.go
|
||||
$ ./wiki
|
||||
This is a sample Page.
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
(If you're using Windows you must type "<code>wiki</code>" without the
|
||||
"<code>./</code>" to run the program.)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="part1.go">Click here to view the code we've written so far.</a>
|
||||
</p>
|
||||
|
||||
<h2>Introducing the <code>net/http</code> package (an interlude)</h2>
|
||||
|
||||
<p>
|
||||
Here's a full working example of a simple web server:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/http-sample.go"}}
|
||||
|
||||
<p>
|
||||
The <code>main</code> function begins with a call to
|
||||
<code>http.HandleFunc</code>, which tells the <code>http</code> package to
|
||||
handle all requests to the web root (<code>"/"</code>) with
|
||||
<code>handler</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It then calls <code>http.ListenAndServe</code>, specifying that it should
|
||||
listen on port 8080 on any interface (<code>":8080"</code>). (Don't
|
||||
worry about its second parameter, <code>nil</code>, for now.)
|
||||
This function will block until the program is terminated.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<code>ListenAndServe</code> always returns an error, since it only returns when an
|
||||
unexpected error occurs.
|
||||
In order to log that error we wrap the function call with <code>log.Fatal</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The function <code>handler</code> is of the type <code>http.HandlerFunc</code>.
|
||||
It takes an <code>http.ResponseWriter</code> and an <code>http.Request</code> as
|
||||
its arguments.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An <code>http.ResponseWriter</code> value assembles the HTTP server's response; by writing
|
||||
to it, we send data to the HTTP client.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An <code>http.Request</code> is a data structure that represents the client
|
||||
HTTP request. <code>r.URL.Path</code> is the path component
|
||||
of the request URL. The trailing <code>[1:]</code> means
|
||||
"create a sub-slice of <code>Path</code> from the 1st character to the end."
|
||||
This drops the leading "/" from the path name.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you run this program and access the URL:
|
||||
</p>
|
||||
<pre>http://localhost:8080/monkeys</pre>
|
||||
<p>
|
||||
the program would present a page containing:
|
||||
</p>
|
||||
<pre>Hi there, I love monkeys!</pre>
|
||||
|
||||
<h2>Using <code>net/http</code> to serve wiki pages</h2>
|
||||
|
||||
<p>
|
||||
To use the <code>net/http</code> package, it must be imported:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
<b>"net/http"</b>
|
||||
)
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Let's create a handler, <code>viewHandler</code> that will allow users to
|
||||
view a wiki page. It will handle URLs prefixed with "/view/".
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part2.go" `/^func viewHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
Again, note the use of <code>_</code> to ignore the <code>error</code>
|
||||
return value from <code>loadPage</code>. This is done here for simplicity
|
||||
and generally considered bad practice. We will attend to this later.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, this function extracts the page title from <code>r.URL.Path</code>,
|
||||
the path component of the request URL.
|
||||
The <code>Path</code> is re-sliced with <code>[len("/view/"):]</code> to drop
|
||||
the leading <code>"/view/"</code> component of the request path.
|
||||
This is because the path will invariably begin with <code>"/view/"</code>,
|
||||
which is not part of the page's title.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The function then loads the page data, formats the page with a string of simple
|
||||
HTML, and writes it to <code>w</code>, the <code>http.ResponseWriter</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To use this handler, we rewrite our <code>main</code> function to
|
||||
initialize <code>http</code> using the <code>viewHandler</code> to handle
|
||||
any requests under the path <code>/view/</code>.
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part2.go" `/^func main/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
<a href="part2.go">Click here to view the code we've written so far.</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Let's create some page data (as <code>test.txt</code>), compile our code, and
|
||||
try serving a wiki page.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Open <code>test.txt</code> file in your editor, and save the string "Hello world" (without quotes)
|
||||
in it.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
$ go build wiki.go
|
||||
$ ./wiki
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
(If you're using Windows you must type "<code>wiki</code>" without the
|
||||
"<code>./</code>" to run the program.)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
With this web server running, a visit to <code><a
|
||||
href="http://localhost:8080/view/test">http://localhost:8080/view/test</a></code>
|
||||
should show a page titled "test" containing the words "Hello world".
|
||||
</p>
|
||||
|
||||
<h2>Editing Pages</h2>
|
||||
|
||||
<p>
|
||||
A wiki is not a wiki without the ability to edit pages. Let's create two new
|
||||
handlers: one named <code>editHandler</code> to display an 'edit page' form,
|
||||
and the other named <code>saveHandler</code> to save the data entered via the
|
||||
form.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, we add them to <code>main()</code>:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-noclosure.go" `/^func main/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The function <code>editHandler</code> loads the page
|
||||
(or, if it doesn't exist, create an empty <code>Page</code> struct),
|
||||
and displays an HTML form.
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/notemplate.go" `/^func editHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
This function will work fine, but all that hard-coded HTML is ugly.
|
||||
Of course, there is a better way.
|
||||
</p>
|
||||
|
||||
<h2>The <code>html/template</code> package</h2>
|
||||
|
||||
<p>
|
||||
The <code>html/template</code> package is part of the Go standard library.
|
||||
We can use <code>html/template</code> to keep the HTML in a separate file,
|
||||
allowing us to change the layout of our edit page without modifying the
|
||||
underlying Go code.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, we must add <code>html/template</code> to the list of imports. We
|
||||
also won't be using <code>fmt</code> anymore, so we have to remove that.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
import (
|
||||
<b>"html/template"</b>
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Let's create a template file containing the HTML form.
|
||||
Open a new file named <code>edit.html</code>, and add the following lines:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/edit.html"}}
|
||||
|
||||
<p>
|
||||
Modify <code>editHandler</code> to use the template, instead of the hard-coded
|
||||
HTML:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-noerror.go" `/^func editHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The function <code>template.ParseFiles</code> will read the contents of
|
||||
<code>edit.html</code> and return a <code>*template.Template</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The method <code>t.Execute</code> executes the template, writing the
|
||||
generated HTML to the <code>http.ResponseWriter</code>.
|
||||
The <code>.Title</code> and <code>.Body</code> dotted identifiers refer to
|
||||
<code>p.Title</code> and <code>p.Body</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Template directives are enclosed in double curly braces.
|
||||
The <code>printf "%s" .Body</code> instruction is a function call
|
||||
that outputs <code>.Body</code> as a string instead of a stream of bytes,
|
||||
the same as a call to <code>fmt.Printf</code>.
|
||||
The <code>html/template</code> package helps guarantee that only safe and
|
||||
correct-looking HTML is generated by template actions. For instance, it
|
||||
automatically escapes any greater than sign (<code>></code>), replacing it
|
||||
with <code>&gt;</code>, to make sure user data does not corrupt the form
|
||||
HTML.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Since we're working with templates now, let's create a template for our
|
||||
<code>viewHandler</code> called <code>view.html</code>:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/view.html"}}
|
||||
|
||||
<p>
|
||||
Modify <code>viewHandler</code> accordingly:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-noerror.go" `/^func viewHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
Notice that we've used almost exactly the same templating code in both
|
||||
handlers. Let's remove this duplication by moving the templating code
|
||||
to its own function:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-template.go" `/^func renderTemplate/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
And modify the handlers to use that function:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-template.go" `/^func viewHandler/` `/^}/`}}
|
||||
{{code "doc/articles/wiki/final-template.go" `/^func editHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
If we comment out the registration of our unimplemented save handler in
|
||||
<code>main</code>, we can once again build and test our program.
|
||||
<a href="part3.go">Click here to view the code we've written so far.</a>
|
||||
</p>
|
||||
|
||||
<h2>Handling non-existent pages</h2>
|
||||
|
||||
<p>
|
||||
What if you visit <a href="http://localhost:8080/view/APageThatDoesntExist">
|
||||
<code>/view/APageThatDoesntExist</code></a>? You'll see a page containing
|
||||
HTML. This is because it ignores the error return value from
|
||||
<code>loadPage</code> and continues to try and fill out the template
|
||||
with no data. Instead, if the requested Page doesn't exist, it should
|
||||
redirect the client to the edit Page so the content may be created:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part3-errorhandling.go" `/^func viewHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The <code>http.Redirect</code> function adds an HTTP status code of
|
||||
<code>http.StatusFound</code> (302) and a <code>Location</code>
|
||||
header to the HTTP response.
|
||||
</p>
|
||||
|
||||
<h2>Saving Pages</h2>
|
||||
|
||||
<p>
|
||||
The function <code>saveHandler</code> will handle the submission of forms
|
||||
located on the edit pages. After uncommenting the related line in
|
||||
<code>main</code>, let's implement the handler:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-template.go" `/^func saveHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The page title (provided in the URL) and the form's only field,
|
||||
<code>Body</code>, are stored in a new <code>Page</code>.
|
||||
The <code>save()</code> method is then called to write the data to a file,
|
||||
and the client is redirected to the <code>/view/</code> page.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The value returned by <code>FormValue</code> is of type <code>string</code>.
|
||||
We must convert that value to <code>[]byte</code> before it will fit into
|
||||
the <code>Page</code> struct. We use <code>[]byte(body)</code> to perform
|
||||
the conversion.
|
||||
</p>
|
||||
|
||||
<h2>Error handling</h2>
|
||||
|
||||
<p>
|
||||
There are several places in our program where errors are being ignored. This
|
||||
is bad practice, not least because when an error does occur the program will
|
||||
have unintended behavior. A better solution is to handle the errors and return
|
||||
an error message to the user. That way if something does go wrong, the server
|
||||
will function exactly how we want and the user can be notified.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, let's handle the errors in <code>renderTemplate</code>:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-parsetemplate.go" `/^func renderTemplate/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The <code>http.Error</code> function sends a specified HTTP response code
|
||||
(in this case "Internal Server Error") and error message.
|
||||
Already the decision to put this in a separate function is paying off.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Now let's fix up <code>saveHandler</code>:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/part3-errorhandling.go" `/^func saveHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
Any errors that occur during <code>p.save()</code> will be reported
|
||||
to the user.
|
||||
</p>
|
||||
|
||||
<h2>Template caching</h2>
|
||||
|
||||
<p>
|
||||
There is an inefficiency in this code: <code>renderTemplate</code> calls
|
||||
<code>ParseFiles</code> every time a page is rendered.
|
||||
A better approach would be to call <code>ParseFiles</code> once at program
|
||||
initialization, parsing all templates into a single <code>*Template</code>.
|
||||
Then we can use the
|
||||
<a href="/pkg/html/template/#Template.ExecuteTemplate"><code>ExecuteTemplate</code></a>
|
||||
method to render a specific template.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First we create a global variable named <code>templates</code>, and initialize
|
||||
it with <code>ParseFiles</code>.
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final.go" `/var templates/`}}
|
||||
|
||||
<p>
|
||||
The function <code>template.Must</code> is a convenience wrapper that panics
|
||||
when passed a non-nil <code>error</code> value, and otherwise returns the
|
||||
<code>*Template</code> unaltered. A panic is appropriate here; if the templates
|
||||
can't be loaded the only sensible thing to do is exit the program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <code>ParseFiles</code> function takes any number of string arguments that
|
||||
identify our template files, and parses those files into templates that are
|
||||
named after the base file name. If we were to add more templates to our
|
||||
program, we would add their names to the <code>ParseFiles</code> call's
|
||||
arguments.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We then modify the <code>renderTemplate</code> function to call the
|
||||
<code>templates.ExecuteTemplate</code> method with the name of the appropriate
|
||||
template:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final.go" `/func renderTemplate/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
Note that the template name is the template file name, so we must
|
||||
append <code>".html"</code> to the <code>tmpl</code> argument.
|
||||
</p>
|
||||
|
||||
<h2>Validation</h2>
|
||||
|
||||
<p>
|
||||
As you may have observed, this program has a serious security flaw: a user
|
||||
can supply an arbitrary path to be read/written on the server. To mitigate
|
||||
this, we can write a function to validate the title with a regular expression.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, add <code>"regexp"</code> to the <code>import</code> list.
|
||||
Then we can create a global variable to store our validation
|
||||
expression:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-noclosure.go" `/^var validPath/`}}
|
||||
|
||||
<p>
|
||||
The function <code>regexp.MustCompile</code> will parse and compile the
|
||||
regular expression, and return a <code>regexp.Regexp</code>.
|
||||
<code>MustCompile</code> is distinct from <code>Compile</code> in that it will
|
||||
panic if the expression compilation fails, while <code>Compile</code> returns
|
||||
an <code>error</code> as a second parameter.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Now, let's write a function that uses the <code>validPath</code>
|
||||
expression to validate path and extract the page title:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-noclosure.go" `/func getTitle/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
If the title is valid, it will be returned along with a <code>nil</code>
|
||||
error value. If the title is invalid, the function will write a
|
||||
"404 Not Found" error to the HTTP connection, and return an error to the
|
||||
handler. To create a new error, we have to import the <code>errors</code>
|
||||
package.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Let's put a call to <code>getTitle</code> in each of the handlers:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final-noclosure.go" `/^func viewHandler/` `/^}/`}}
|
||||
{{code "doc/articles/wiki/final-noclosure.go" `/^func editHandler/` `/^}/`}}
|
||||
{{code "doc/articles/wiki/final-noclosure.go" `/^func saveHandler/` `/^}/`}}
|
||||
|
||||
<h2>Introducing Function Literals and Closures</h2>
|
||||
|
||||
<p>
|
||||
Catching the error condition in each handler introduces a lot of repeated code.
|
||||
What if we could wrap each of the handlers in a function that does this
|
||||
validation and error checking? Go's
|
||||
<a href="/ref/spec#Function_literals">function
|
||||
literals</a> provide a powerful means of abstracting functionality
|
||||
that can help us here.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
First, we re-write the function definition of each of the handlers to accept
|
||||
a title string:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request, title string)
|
||||
func editHandler(w http.ResponseWriter, r *http.Request, title string)
|
||||
func saveHandler(w http.ResponseWriter, r *http.Request, title string)
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Now let's define a wrapper function that <i>takes a function of the above
|
||||
type</i>, and returns a function of type <code>http.HandlerFunc</code>
|
||||
(suitable to be passed to the function <code>http.HandleFunc</code>):
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Here we will extract the page title from the Request,
|
||||
// and call the provided handler 'fn'
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
The returned function is called a closure because it encloses values defined
|
||||
outside of it. In this case, the variable <code>fn</code> (the single argument
|
||||
to <code>makeHandler</code>) is enclosed by the closure. The variable
|
||||
<code>fn</code> will be one of our save, edit, or view handlers.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Now we can take the code from <code>getTitle</code> and use it here
|
||||
(with some minor modifications):
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final.go" `/func makeHandler/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
The closure returned by <code>makeHandler</code> is a function that takes
|
||||
an <code>http.ResponseWriter</code> and <code>http.Request</code> (in other
|
||||
words, an <code>http.HandlerFunc</code>).
|
||||
The closure extracts the <code>title</code> from the request path, and
|
||||
validates it with the <code>TitleValidator</code> regexp. If the
|
||||
<code>title</code> is invalid, an error will be written to the
|
||||
<code>ResponseWriter</code> using the <code>http.NotFound</code> function.
|
||||
If the <code>title</code> is valid, the enclosed handler function
|
||||
<code>fn</code> will be called with the <code>ResponseWriter</code>,
|
||||
<code>Request</code>, and <code>title</code> as arguments.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Now we can wrap the handler functions with <code>makeHandler</code> in
|
||||
<code>main</code>, before they are registered with the <code>http</code>
|
||||
package:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final.go" `/func main/` `/^}/`}}
|
||||
|
||||
<p>
|
||||
Finally we remove the calls to <code>getTitle</code> from the handler functions,
|
||||
making them much simpler:
|
||||
</p>
|
||||
|
||||
{{code "doc/articles/wiki/final.go" `/^func viewHandler/` `/^}/`}}
|
||||
{{code "doc/articles/wiki/final.go" `/^func editHandler/` `/^}/`}}
|
||||
{{code "doc/articles/wiki/final.go" `/^func saveHandler/` `/^}/`}}
|
||||
|
||||
<h2>Try it out!</h2>
|
||||
|
||||
<p>
|
||||
<a href="final.go">Click here to view the final code listing.</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Recompile the code, and run the app:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
$ go build wiki.go
|
||||
$ ./wiki
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Visiting <a href="http://localhost:8080/view/ANewPage">http://localhost:8080/view/ANewPage</a>
|
||||
should present you with the page edit form. You should then be able to
|
||||
enter some text, click 'Save', and be redirected to the newly created page.
|
||||
</p>
|
||||
|
||||
<h2>Other tasks</h2>
|
||||
|
||||
<p>
|
||||
Here are some simple tasks you might want to tackle on your own:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Store templates in <code>tmpl/</code> and page data in <code>data/</code>.
|
||||
<li>Add a handler to make the web root redirect to
|
||||
<code>/view/FrontPage</code>.</li>
|
||||
<li>Spruce up the page templates by making them valid HTML and adding some
|
||||
CSS rules.</li>
|
||||
<li>Implement inter-page linking by converting instances of
|
||||
<code>[PageName]</code> to <br>
|
||||
<code><a href="/view/PageName">PageName</a></code>.
|
||||
(hint: you could use <code>regexp.ReplaceAllFunc</code> to do this)
|
||||
</li>
|
||||
</ul>
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/view/"):]
|
||||
p, _ := loadPage(title)
|
||||
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
|
||||
}
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/edit/"):]
|
||||
p, err := loadPage(title)
|
||||
if err != nil {
|
||||
p = &Page{Title: title}
|
||||
}
|
||||
fmt.Fprintf(w, "<h1>Editing %s</h1>"+
|
||||
"<form action=\"/save/%s\" method=\"POST\">"+
|
||||
"<textarea name=\"body\">%s</textarea><br>"+
|
||||
"<input type=\"submit\" value=\"Save\">"+
|
||||
"</form>",
|
||||
p.Title, p.Title, p.Body)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", viewHandler)
|
||||
http.HandleFunc("/edit/", editHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) *Page {
|
||||
filename := title + ".txt"
|
||||
body, _ := ioutil.ReadFile(filename)
|
||||
return &Page{Title: title, Body: body}
|
||||
}
|
||||
|
||||
func main() {
|
||||
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample page.")}
|
||||
p1.save()
|
||||
p2 := loadPage("TestPage")
|
||||
fmt.Println(string(p2.Body))
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
|
||||
p1.save()
|
||||
p2, _ := loadPage("TestPage")
|
||||
fmt.Println(string(p2.Body))
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2010 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (p *Page) save() error {
|
||||
filename := p.Title + ".txt"
|
||||
return ioutil.WriteFile(filename, p.Body, 0600)
|
||||
}
|
||||
|
||||
func loadPage(title string) (*Page, error) {
|
||||
filename := title + ".txt"
|
||||
body, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Page{Title: title, Body: body}, nil
|
||||
}
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[len("/view/"):]
|
||||
p, _ := loadPage(title)
|
||||
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", viewHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче