1
0
mirror of https://github.com/golang/go synced 2024-11-25 06:07:58 -07:00

godoc support for directories outside $GOROOT

Example use: godoc -path=/home/user1:/home/build/foo -http=:6666
will start a local godoc that maps urls starting with /pkg/user1 or
/pkg/foo to the respective roots specified in the path.

Missing: Handling of overlapping package directories, multiple
packages per directory.

R=rsc
CC=golang-dev
https://golang.org/cl/206078
This commit is contained in:
Robert Griesemer 2010-02-16 11:20:55 -08:00
parent 85498cbcdd
commit 5883c6ef1f
8 changed files with 327 additions and 271 deletions

View File

@ -4,10 +4,6 @@
license that can be found in the LICENSE file. license that can be found in the LICENSE file.
--> -->
{.section Error} <p>
<p> <span class="alert" style="font-size:120%">{@|html-esc}</span>
<span class="alert" style="font-size:120%">{@|html}</span> </p>
</p>
{.or}
<pre>{Source|html}</pre>
{.end}

View File

@ -94,6 +94,9 @@
<li><a href="/doc/code.html">How to write code</a></li> <li><a href="/doc/code.html">How to write code</a></li>
<li><a href="/cmd">Command documentation</a></li> <li><a href="/cmd">Command documentation</a></li>
<li><a href="/pkg">Package documentation</a></li> <li><a href="/pkg">Package documentation</a></li>
{.repeated section PkgRoots}
<li><a href="/pkg/{@|html-esc}">Package documentation for {@|html-esc}</a></li>
{.end}
<li><a href="/src">Source files</a></li> <li><a href="/src">Source files</a></li>
<li><a href="/doc/devel/">The Go project</a></li> <li><a href="/doc/devel/">The Go project</a></li>

View File

@ -16,7 +16,7 @@
<h4>Package files</h4> <h4>Package files</h4>
<span style="font-size:90%"> <span style="font-size:90%">
{.repeated section @} {.repeated section @}
<a href="/{FilePath|html-esc}/{@|html-esc}">{@|html}</a> <a href="{@|url-src}">{@|localname}</a>
{.end} {.end}
</span> </span>
</p> </p>
@ -38,14 +38,14 @@
{.end} {.end}
{.section Funcs} {.section Funcs}
{.repeated section @} {.repeated section @}
<h2 id="{Name|html-esc}">func <a href="{Decl|link}">{Name|html}</a></h2> <h2 id="{Name|html-esc}">func <a href="{Decl|url-pos}">{Name|html}</a></h2>
<p><code>{Decl|html}</code></p> <p><code>{Decl|html}</code></p>
{Doc|html-comment} {Doc|html-comment}
{.end} {.end}
{.end} {.end}
{.section Types} {.section Types}
{.repeated section @} {.repeated section @}
<h2 id="{Type.Name|html-esc}">type <a href="{Decl|link}">{Type.Name|html}</a></h2> <h2 id="{Type.Name|html-esc}">type <a href="{Decl|url-pos}">{Type.Name|html}</a></h2>
{Doc|html-comment} {Doc|html-comment}
<p><pre>{Decl|html}</pre></p> <p><pre>{Decl|html}</pre></p>
{.repeated section Consts} {.repeated section Consts}
@ -57,12 +57,12 @@
<pre>{Decl|html}</pre> <pre>{Decl|html}</pre>
{.end} {.end}
{.repeated section Factories} {.repeated section Factories}
<h3 id="{Type.Name|html-esc}.{Name|html-esc}">func <a href="{Decl|link}">{Name|html}</a></h3> <h3 id="{Type.Name|html-esc}.{Name|html-esc}">func <a href="{Decl|url-pos}">{Name|html}</a></h3>
<p><code>{Decl|html}</code></p> <p><code>{Decl|html}</code></p>
{Doc|html-comment} {Doc|html-comment}
{.end} {.end}
{.repeated section Methods} {.repeated section Methods}
<h3 id="{Type.Name|html-esc}.{Name|html-esc}">func ({Recv|html}) <a href="{Decl|link}">{Name|html}</a></h3> <h3 id="{Type.Name|html-esc}.{Name|html-esc}">func ({Recv|html}) <a href="{Decl|url-pos}">{Name|html}</a></h3>
<p><code>{Decl|html}</code></p> <p><code>{Decl|html}</code></p>
{Doc|html-comment} {Doc|html-comment}
{.end} {.end}

View File

@ -22,11 +22,11 @@
{.section Decls} {.section Decls}
<h2 id="Global">Package-level declarations</h2> <h2 id="Global">Package-level declarations</h2>
{.repeated section @} {.repeated section @}
<h3 id="Global_{Pak.Path|path}">package <a href="{Pak.Path|path}">{Pak.Name|html}</a></h3> <h3 id="Global_{Pak.Path|url-pkg}">package <a href="{Pak.Path|url-pkg}">{Pak.Name|html}</a></h3>
{.repeated section Files} {.repeated section Files}
{.repeated section Groups} {.repeated section Groups}
{.repeated section Infos} {.repeated section Infos}
<a href="{File.Path|html-esc}?h={Query|html-esc}#L{@|infoLine}">{File.Path|html}:{@|infoLine}</a> <a href="{File.Path|url-src}?h={Query|html-esc}#L{@|infoLine}">{File.Path|html}:{@|infoLine}</a>
<pre>{@|infoSnippet}</pre> <pre>{@|infoSnippet}</pre>
{.end} {.end}
{.end} {.end}
@ -36,9 +36,9 @@
{.section Others} {.section Others}
<h2 id="Local">Local declarations and uses</h2> <h2 id="Local">Local declarations and uses</h2>
{.repeated section @} {.repeated section @}
<h3 id="Local_{Pak.Path|path}">package <a href="{Pak.Path|path}">{Pak.Name|html}</a></h3> <h3 id="Local_{Pak.Path|url-pkg}">package <a href="{Pak.Path|url-pkg}">{Pak.Name|html}</a></h3>
{.repeated section Files} {.repeated section Files}
<a href="{File.Path|html-esc}?h={Query|html-esc}">{File.Path|html}</a> <a href="{File.Path|url-src}?h={Query|html-esc}">{File.Path|html}</a>
<table class="layout"> <table class="layout">
{.repeated section Groups} {.repeated section Groups}
<tr> <tr>
@ -47,7 +47,7 @@
<td align="left" width="4"></td> <td align="left" width="4"></td>
<td> <td>
{.repeated section Infos} {.repeated section Infos}
<a href="{File.Path|html-esc}?h={Query|html-esc}#L{@|infoLine}">{@|infoLine}</a> <a href="{File.Path|url-src}?h={Query|html-esc}#L{@|infoLine}">{@|infoLine}</a>
{.end} {.end}
</td> </td>
</tr> </tr>

View File

@ -9,6 +9,7 @@ GOFILES=\
godoc.go\ godoc.go\
index.go\ index.go\
main.go\ main.go\
mapping.go\
snippet.go\ snippet.go\
spec.go\ spec.go\

View File

@ -77,26 +77,54 @@ func (dt *delayTime) backoff(max int) {
var ( var (
verbose = flag.Bool("v", false, "verbose mode") verbose = flag.Bool("v", false, "verbose mode")
// file system roots // "fixed" file system roots
goroot string goroot string
cmdroot = flag.String("cmdroot", "src/cmd", "root command source directory (if unrooted, relative to goroot)") cmdroot string
pkgroot = flag.String("pkgroot", "src/pkg", "root package source directory (if unrooted, relative to goroot)") pkgroot string
tmplroot = flag.String("tmplroot", "lib/godoc", "root template directory (if unrooted, relative to goroot)") tmplroot string
// additional file system roots to consider
path = flag.String("path", "", "additional pkg directories")
// layout control // layout control
tabwidth = flag.Int("tabwidth", 4, "tab width") tabwidth = flag.Int("tabwidth", 4, "tab width")
// file system mapping
fsMap Mapping // user-defined mapping
fsTree RWValue // *Directory tree of packages, updated with each sync
// http handlers
fileServer http.Handler // default file server
cmdHandler httpHandler
pkgHandler httpHandler
) )
var fsTree RWValue // *Directory tree of packages, updated with each sync func initRoots() {
func init() {
goroot = os.Getenv("GOROOT") goroot = os.Getenv("GOROOT")
if goroot == "" { if goroot == "" {
goroot = pathutil.Join(os.Getenv("HOME"), "go") goroot = pathutil.Join(os.Getenv("HOME"), "go")
} }
flag.StringVar(&goroot, "goroot", goroot, "Go root directory") flag.StringVar(&goroot, "goroot", goroot, "Go root directory")
// other flags/variables that depend on goroot
flag.StringVar(&cmdroot, "cmdroot", pathutil.Join(goroot, "src/cmd"), "command source directory")
flag.StringVar(&pkgroot, "pkgroot", pathutil.Join(goroot, "src/pkg"), "package source directory")
flag.StringVar(&tmplroot, "tmplroot", pathutil.Join(goroot, "lib/godoc"), "template directory")
fsMap.Init(*path)
fileServer = http.FileServer(goroot, "")
cmdHandler = httpHandler{"/cmd/", cmdroot, false}
pkgHandler = httpHandler{"/pkg/", pkgroot, true}
}
func registerPublicHandlers(mux *http.ServeMux) {
mux.Handle(cmdHandler.pattern, &cmdHandler)
mux.Handle(pkgHandler.pattern, &pkgHandler)
mux.Handle("/search", http.HandlerFunc(search))
mux.Handle("/", http.HandlerFunc(serveFile))
} }
@ -173,6 +201,31 @@ func firstSentence(s string) string {
} }
func absolutePath(path, defaultRoot string) string {
abspath := fsMap.ToAbsolute(path)
if abspath == "" {
// no user-defined mapping found; use default mapping
abspath = pathutil.Join(defaultRoot, path)
}
return abspath
}
func relativePath(path string) string {
relpath := fsMap.ToRelative(path)
if relpath == "" && strings.HasPrefix(path, goroot+"/") {
// no user-defined mapping found; use default mapping
relpath = path[len(goroot)+1:]
}
// Only if path is an invalid absolute path is relpath == ""
// at this point. This should never happen since absolute paths
// are only created via godoc for files that do exist. However,
// it is ok to return ""; it will simply provide a link to the
// top of the pkg or src directories.
return relpath
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Package directories // Package directories
@ -287,26 +340,32 @@ func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
} }
func (dir *Directory) lookupLocal(name string) *Directory {
for _, d := range dir.Dirs {
if d.Name == name {
return d
}
}
return nil
}
// lookup looks for the *Directory for a given path, relative to dir. // lookup looks for the *Directory for a given path, relative to dir.
func (dir *Directory) lookup(path string) *Directory { func (dir *Directory) lookup(path string) *Directory {
path = pathutil.Clean(path) // no trailing '/' d := strings.Split(dir.Path, "/", 0)
p := strings.Split(path, "/", 0)
if dir == nil || path == "" || path == "." { i := 0
return dir for i < len(d) {
} if i >= len(p) || d[i] != p[i] {
return nil
dpath, dname := pathutil.Split(path)
if dpath == "" {
// directory-local name
for _, d := range dir.Dirs {
if dname == d.Name {
return d
}
} }
return nil i++
} }
for dir != nil && i < len(p) {
return dir.lookup(dpath).lookup(dname) dir = dir.lookupLocal(p[i])
i++
}
return dir
} }
@ -383,20 +442,6 @@ func (root *Directory) listing(skipRoot bool) *DirList {
} }
func listing(dirs []*os.Dir) *DirList {
list := make([]DirEntry, len(dirs)+1)
list[0] = DirEntry{0, 1, "..", "..", ""}
for i, d := range dirs {
p := &list[i+1]
p.Depth = 0
p.Height = 1
p.Path = d.Name
p.Name = d.Name
}
return &DirList{1, list}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// HTML formatting support // HTML formatting support
@ -552,12 +597,6 @@ func writeText(w io.Writer, text []byte, html bool) {
} }
type StyledNode struct {
node interface{}
styler printer.Styler
}
// Write anything to w; optionally html-escaped. // Write anything to w; optionally html-escaped.
func writeAny(w io.Writer, x interface{}, html bool) { func writeAny(w io.Writer, x interface{}, html bool) {
switch v := x.(type) { switch v := x.(type) {
@ -567,8 +606,6 @@ func writeAny(w io.Writer, x interface{}, html bool) {
writeText(w, strings.Bytes(v), html) writeText(w, strings.Bytes(v), html)
case ast.Decl, ast.Expr, ast.Stmt, *ast.File: case ast.Decl, ast.Expr, ast.Stmt, *ast.File:
writeNode(w, x, html, &defaultStyler) writeNode(w, x, html, &defaultStyler)
case StyledNode:
writeNode(w, v.node, html, v.styler)
default: default:
if html { if html {
var buf bytes.Buffer var buf bytes.Buffer
@ -609,36 +646,51 @@ func textFmt(w io.Writer, x interface{}, format string) {
} }
func removePrefix(s, prefix string) string { // Template formatter for the various "url-xxx" formats.
if strings.HasPrefix(s, prefix) { func urlFmt(w io.Writer, x interface{}, format string) {
return s[len(prefix):] var path string
} var line int
return s
}
// determine path and position info, if any
// Template formatter for "path" format. type positioner interface {
func pathFmt(w io.Writer, x interface{}, format string) {
// TODO(gri): Need to find a better solution for this.
// This will not work correctly if *cmdroot
// or *pkgroot change.
writeAny(w, removePrefix(x.(string), "src"), true)
}
// Template formatter for "link" format.
func linkFmt(w io.Writer, x interface{}, format string) {
type Positioner interface {
Pos() token.Position Pos() token.Position
} }
if node, ok := x.(Positioner); ok { switch t := x.(type) {
pos := node.Pos() case string:
path = t
case positioner:
pos := t.Pos()
if pos.IsValid() { if pos.IsValid() {
// line id's in html-printed source are of the path = pos.Filename
// form "L%d" where %d stands for the line number line = pos.Line
fmt.Fprintf(w, "/%s#L%d", htmlEscape(pos.Filename), pos.Line)
} }
} }
// map path
relpath := relativePath(path)
// convert to URL
switch format {
default:
// we should never reach here, but be resilient
// and assume the url-pkg format instead
log.Stderrf("INTERNAL ERROR: urlFmt(%s)", format)
fallthrough
case "url-pkg":
// because of the irregular mapping under goroot
// we need to correct certain relative paths
if strings.HasPrefix(relpath, "src/pkg/") {
relpath = relpath[len("src/pkg/"):]
}
template.HTMLEscape(w, strings.Bytes(pkgHandler.pattern+relpath))
case "url-src":
template.HTMLEscape(w, strings.Bytes("/"+relpath))
case "url-pos":
// line id's in html-printed source are of the
// form "L%d" where %d stands for the line number
template.HTMLEscape(w, strings.Bytes("/"+relpath))
fmt.Fprintf(w, "#L%d", line)
}
} }
@ -710,24 +762,33 @@ func dirslashFmt(w io.Writer, x interface{}, format string) {
} }
// Template formatter for "localname" format.
func localnameFmt(w io.Writer, x interface{}, format string) {
_, localname := pathutil.Split(x.(string))
template.HTMLEscape(w, strings.Bytes(localname))
}
var fmap = template.FormatterMap{ var fmap = template.FormatterMap{
"": textFmt, "": textFmt,
"html": htmlFmt, "html": htmlFmt,
"html-esc": htmlEscFmt, "html-esc": htmlEscFmt,
"html-comment": htmlCommentFmt, "html-comment": htmlCommentFmt,
"path": pathFmt, "url-pkg": urlFmt,
"link": linkFmt, "url-src": urlFmt,
"url-pos": urlFmt,
"infoKind": infoKindFmt, "infoKind": infoKindFmt,
"infoLine": infoLineFmt, "infoLine": infoLineFmt,
"infoSnippet": infoSnippetFmt, "infoSnippet": infoSnippetFmt,
"padding": paddingFmt, "padding": paddingFmt,
"time": timeFmt, "time": timeFmt,
"dir/": dirslashFmt, "dir/": dirslashFmt,
"localname": localnameFmt,
} }
func readTemplate(name string) *template.Template { func readTemplate(name string) *template.Template {
path := pathutil.Join(*tmplroot, name) path := pathutil.Join(tmplroot, name)
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
log.Exitf("ReadFile %s: %v", path, err) log.Exitf("ReadFile %s: %v", path, err)
@ -742,22 +803,21 @@ func readTemplate(name string) *template.Template {
var ( var (
dirlistHTML, dirlistHTML,
errorHTML,
godocHTML, godocHTML,
packageHTML, packageHTML,
packageText, packageText,
searchHTML, searchHTML *template.Template
sourceHTML *template.Template
) )
func readTemplates() { func readTemplates() {
// have to delay until after flags processing, // have to delay until after flags processing, so that tmplroot is known
// so that main has chdir'ed to goroot.
dirlistHTML = readTemplate("dirlist.html") dirlistHTML = readTemplate("dirlist.html")
errorHTML = readTemplate("error.html")
godocHTML = readTemplate("godoc.html") godocHTML = readTemplate("godoc.html")
packageHTML = readTemplate("package.html") packageHTML = readTemplate("package.html")
packageText = readTemplate("package.txt") packageText = readTemplate("package.txt")
searchHTML = readTemplate("search.html") searchHTML = readTemplate("search.html")
sourceHTML = readTemplate("source.html")
} }
@ -767,6 +827,7 @@ func readTemplates() {
func servePage(c *http.Conn, title, query string, content []byte) { func servePage(c *http.Conn, title, query string, content []byte) {
type Data struct { type Data struct {
Title string Title string
PkgRoots []string
Timestamp uint64 // int64 to be compatible with os.Dir.Mtime_ns Timestamp uint64 // int64 to be compatible with os.Dir.Mtime_ns
Query string Query string
Content []byte Content []byte
@ -775,6 +836,7 @@ func servePage(c *http.Conn, title, query string, content []byte) {
_, ts := fsTree.get() _, ts := fsTree.get()
d := Data{ d := Data{
Title: title, Title: title,
PkgRoots: fsMap.PrefixList(),
Timestamp: uint64(ts) * 1e9, // timestamp in ns Timestamp: uint64(ts) * 1e9, // timestamp in ns
Query: query, Query: query,
Content: content, Content: content,
@ -787,7 +849,7 @@ func servePage(c *http.Conn, title, query string, content []byte) {
func serveText(c *http.Conn, text []byte) { func serveText(c *http.Conn, text []byte) {
c.SetHeader("content-type", "text/plain; charset=utf-8") c.SetHeader("Content-Type", "text/plain; charset=utf-8")
c.Write(text) c.Write(text)
} }
@ -811,12 +873,18 @@ func commentText(src []byte) (text string) {
} }
func serveHTMLDoc(c *http.Conn, r *http.Request, path string) { func serveError(c *http.Conn, r *http.Request, relpath string, err os.Error) {
contents := applyTemplate(errorHTML, "errorHTML", err)
servePage(c, "File "+relpath, "", contents)
}
func serveHTMLDoc(c *http.Conn, r *http.Request, abspath, relpath string) {
// get HTML body contents // get HTML body contents
src, err := ioutil.ReadFile(path) src, err := ioutil.ReadFile(abspath)
if err != nil { if err != nil {
log.Stderrf("%v", err) log.Stderrf("ioutil.ReadFile: %s", err)
http.NotFound(c, r) serveError(c, r, relpath, err)
return return
} }
@ -828,7 +896,7 @@ func serveHTMLDoc(c *http.Conn, r *http.Request, path string) {
} }
// if it's the language spec, add tags to EBNF productions // if it's the language spec, add tags to EBNF productions
if strings.HasSuffix(path, "go_spec.html") { if strings.HasSuffix(abspath, "go_spec.html") {
var buf bytes.Buffer var buf bytes.Buffer
linkify(&buf, src) linkify(&buf, src)
src = buf.Bytes() src = buf.Bytes()
@ -839,24 +907,29 @@ func serveHTMLDoc(c *http.Conn, r *http.Request, path string) {
} }
func serveGoSource(c *http.Conn, r *http.Request, path string) { func applyTemplate(t *template.Template, name string, data interface{}) []byte {
var info struct { var buf bytes.Buffer
Source StyledNode if err := t.Execute(data, &buf); err != nil {
Error string log.Stderrf("%s.Execute: %s", name, err)
} }
return buf.Bytes()
}
file, err := parser.ParseFile(path, nil, nil, parser.ParseComments)
info.Source = StyledNode{file, &Styler{linetags: true, highlight: r.FormValue("h")}} func serveGoSource(c *http.Conn, r *http.Request, abspath, relpath string) {
file, err := parser.ParseFile(abspath, nil, nil, parser.ParseComments)
if err != nil { if err != nil {
info.Error = err.String() log.Stderrf("parser.ParseFile: %s", err)
serveError(c, r, relpath, err)
return
} }
var buf bytes.Buffer var buf bytes.Buffer
if err := sourceHTML.Execute(info, &buf); err != nil { fmt.Fprintln(&buf, "<pre>")
log.Stderrf("sourceHTML.Execute: %s", err) writeNode(&buf, file, true, &Styler{linetags: true, highlight: r.FormValue("h")})
} fmt.Fprintln(&buf, "</pre>")
servePage(c, "Source file "+path, "", buf.Bytes()) servePage(c, "Source file "+relpath, "", buf.Bytes())
} }
@ -916,10 +989,12 @@ func isTextFile(path string) bool {
} }
func serveTextFile(c *http.Conn, r *http.Request, path string) { func serveTextFile(c *http.Conn, r *http.Request, abspath, relpath string) {
src, err := ioutil.ReadFile(path) src, err := ioutil.ReadFile(abspath)
if err != nil { if err != nil {
log.Stderrf("serveTextFile: %s", err) log.Stderrf("ioutil.ReadFile: %s", err)
serveError(c, r, relpath, err)
return
} }
var buf bytes.Buffer var buf bytes.Buffer
@ -927,18 +1002,19 @@ func serveTextFile(c *http.Conn, r *http.Request, path string) {
template.HTMLEscape(&buf, src) template.HTMLEscape(&buf, src)
fmt.Fprintln(&buf, "</pre>") fmt.Fprintln(&buf, "</pre>")
servePage(c, "Text file "+path, "", buf.Bytes()) servePage(c, "Text file "+relpath, "", buf.Bytes())
} }
func serveDirectory(c *http.Conn, r *http.Request, path string) { func serveDirectory(c *http.Conn, r *http.Request, abspath, relpath string) {
if redirect(c, r) { if redirect(c, r) {
return return
} }
list, err := ioutil.ReadDir(path) list, err := ioutil.ReadDir(abspath)
if err != nil { if err != nil {
http.NotFound(c, r) log.Stderrf("ioutil.ReadDir: %s", err)
serveError(c, r, relpath, err)
return return
} }
@ -948,49 +1024,47 @@ func serveDirectory(c *http.Conn, r *http.Request, path string) {
} }
} }
var buf bytes.Buffer contents := applyTemplate(dirlistHTML, "dirlistHTML", list)
if err := dirlistHTML.Execute(list, &buf); err != nil { servePage(c, "Directory "+relpath, "", contents)
log.Stderrf("dirlistHTML.Execute: %s", err)
}
servePage(c, "Directory "+path, "", buf.Bytes())
} }
var fileServer = http.FileServer(".", "")
func serveFile(c *http.Conn, r *http.Request) { func serveFile(c *http.Conn, r *http.Request) {
path := pathutil.Join(".", r.URL.Path) relpath := r.URL.Path[1:] // serveFile URL paths start with '/'
abspath := absolutePath(relpath, goroot)
// pick off special cases and hand the rest to the standard file server // pick off special cases and hand the rest to the standard file server
switch ext := pathutil.Ext(path); { switch r.URL.Path {
case r.URL.Path == "/": case "/":
serveHTMLDoc(c, r, "doc/root.html") serveHTMLDoc(c, r, pathutil.Join(goroot, "doc/root.html"), "doc/root.html")
return return
case r.URL.Path == "/doc/root.html": case "/doc/root.html":
// hide landing page from its real name // hide landing page from its real name
http.NotFound(c, r) http.Redirect(c, "/", http.StatusMovedPermanently)
return return
}
case ext == ".html": switch pathutil.Ext(abspath) {
if strings.HasSuffix(path, "/index.html") { case ".html":
if strings.HasSuffix(abspath, "/index.html") {
// We'll show index.html for the directory. // We'll show index.html for the directory.
// Use the dir/ version as canonical instead of dir/index.html. // Use the dir/ version as canonical instead of dir/index.html.
http.Redirect(c, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) http.Redirect(c, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
return return
} }
serveHTMLDoc(c, r, path) serveHTMLDoc(c, r, abspath, relpath)
return return
case ext == ".go": case ".go":
serveGoSource(c, r, path) serveGoSource(c, r, abspath, relpath)
return return
} }
dir, err := os.Lstat(path) dir, err := os.Lstat(abspath)
if err != nil { if err != nil {
http.NotFound(c, r) log.Stderr(err)
serveError(c, r, abspath, err)
return return
} }
@ -998,16 +1072,16 @@ func serveFile(c *http.Conn, r *http.Request) {
if redirect(c, r) { if redirect(c, r) {
return return
} }
if index := path + "/index.html"; isTextFile(index) { if index := abspath + "/index.html"; isTextFile(index) {
serveHTMLDoc(c, r, index) serveHTMLDoc(c, r, index, relativePath(index))
return return
} }
serveDirectory(c, r, path) serveDirectory(c, r, abspath, relpath)
return return
} }
if isTextFile(path) { if isTextFile(abspath) {
serveTextFile(c, r, path) serveTextFile(c, r, abspath, relpath)
return return
} }
@ -1018,14 +1092,16 @@ func serveFile(c *http.Conn, r *http.Request) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Packages // Packages
// Package name used for commands that have non-identifier names. // Fake package file and name for commands. Contains the command documentation.
const fakePkgFile = "doc.go"
const fakePkgName = "documentation" const fakePkgName = "documentation"
type PageInfo struct { type PageInfo struct {
PDoc *doc.PackageDoc // nil if no package found Dirname string // directory containing the package
Dirs *DirList // nil if no directory information found PDoc *doc.PackageDoc // nil if no package found
IsPkg bool // false if this is not documenting a real package Dirs *DirList // nil if no directory information found
IsPkg bool // false if this is not documenting a real package
} }
@ -1036,62 +1112,22 @@ type httpHandler struct {
} }
// getPageInfo returns the PageInfo for a package directory path. If the // getPageInfo returns the PageInfo for a package directory path. If
// parameter try is true, no errors are logged if getPageInfo fails. // the parameter try is true, no errors are logged if getPageInfo fails.
// If there is no corresponding package in the directory, // If there is no corresponding package in the directory, PageInfo.PDoc
// PageInfo.PDoc is nil. If there are no subdirectories, // is nil. If there are no subdirectories, PageInfo.Dirs is nil.
// PageInfo.Dirs is nil.
// //
func (h *httpHandler) getPageInfo(path string, try bool) PageInfo { func (h *httpHandler) getPageInfo(relpath string, try bool) PageInfo {
var dirname string dirname := absolutePath(relpath, h.fsRoot)
// If the path starts with a slash or ., ignore $GOROOT.
// It would be nice to handle "./dir" too, but godoc chdirs to $GOROOT. TODO: fix.
if len(path) > 0 && path[0] == '/' {
dirname = path
// --- Start of hack
} else if len(path) > 0 && path[0] == '.' && workingDir != "" {
path = pathutil.Join(workingDir, path)
dirname = path
// --- End of hack
} else {
// the path is relative to h.fsroot
dirname = pathutil.Join(h.fsRoot, path)
}
// the package name is the directory name within its parent
// (use dirname instead of path because dirname is clean; i.e. has no trailing '/')
_, pkgname := pathutil.Split(dirname)
// filter function to select the desired .go files // filter function to select the desired .go files
filter := func(d *os.Dir) bool { filter := func(d *os.Dir) bool {
if isPkgFile(d) { // If we are looking at cmd documentation, only accept
// the special fakePkgFile containing the documentation.
// --- Start of hack. return isPkgFile(d) && (h.isPkg || d.Name == fakePkgFile)
// An ugly special case: If the path is rooted, just say
// yes in the hope we'll get some output from a directory
// outside $GOROOT. Ugly but effective for command-line
// output but may not find everything if there are multiple
// packages in the directory, since godoc assumes one
// package per directory.
// TODO: Do this better.
if len(path) > 0 && path[0] == '/' {
return true
}
// --- End of hack.
// Some directories contain main packages: Only accept
// files that belong to the expected package so that
// parser.ParsePackage doesn't return "multiple packages
// found" errors.
// Additionally, accept the special package name
// fakePkgName if we are looking at cmd documentation.
name := pkgName(dirname + "/" + d.Name)
return name == pkgname || h.fsRoot == *cmdroot && name == fakePkgName
}
return false
} }
// get package AST // get package ASTs
pkgs, err := parser.ParseDir(dirname, filter, parser.ParseComments) pkgs, err := parser.ParseDir(dirname, filter, parser.ParseComments)
if err != nil && !try { if err != nil && !try {
// TODO: errors should be shown instead of an empty directory // TODO: errors should be shown instead of an empty directory
@ -1101,16 +1137,29 @@ func (h *httpHandler) getPageInfo(path string, try bool) PageInfo {
// TODO: should handle multiple packages // TODO: should handle multiple packages
log.Stderrf("parser.parseDir: found %d packages", len(pkgs)) log.Stderrf("parser.parseDir: found %d packages", len(pkgs))
} }
// Get the best matching package: either the first one, or the
// first one whose package name matches the directory name.
// The package name is the directory name within its parent
// (use dirname instead of path because dirname is clean; i.e.
// has no trailing '/').
_, pkgname := pathutil.Split(dirname)
var pkg *ast.Package var pkg *ast.Package
for _, pkg = range pkgs { for _, p := range pkgs {
break // take the first package found switch {
case pkg == nil:
pkg = p
case p.Name == pkgname:
pkg = p
break
}
} }
// compute package documentation // compute package documentation
var pdoc *doc.PackageDoc var pdoc *doc.PackageDoc
if pkg != nil { if pkg != nil {
ast.PackageExports(pkg) ast.PackageExports(pkg)
pdoc = doc.NewPackageDoc(pkg, pathutil.Clean(path)) // no trailing '/' in importpath pdoc = doc.NewPackageDoc(pkg, pathutil.Clean(relpath)) // no trailing '/' in importpath
} }
// get directory information // get directory information
@ -1118,15 +1167,18 @@ func (h *httpHandler) getPageInfo(path string, try bool) PageInfo {
if tree, _ := fsTree.get(); tree != nil { if tree, _ := fsTree.get(); tree != nil {
// directory tree is present; lookup respective directory // directory tree is present; lookup respective directory
// (may still fail if the file system was updated and the // (may still fail if the file system was updated and the
// new directory tree has not yet beet computed) // new directory tree has not yet been computed)
// TODO(gri) Need to build directory tree for fsMap entries
dir = tree.(*Directory).lookup(dirname) dir = tree.(*Directory).lookup(dirname)
} else { }
if dir == nil {
// no directory tree present (either early after startup // no directory tree present (either early after startup
// or command-line mode); compute one level for this page // or command-line mode, or we don't build a tree for the
// directory; e.g. google3); compute one level for this page
dir = newDirectory(dirname, 1) dir = newDirectory(dirname, 1)
} }
return PageInfo{pdoc, dir.listing(true), h.isPkg} return PageInfo{dirname, pdoc, dir.listing(true), h.isPkg}
} }
@ -1135,41 +1187,33 @@ func (h *httpHandler) ServeHTTP(c *http.Conn, r *http.Request) {
return return
} }
path := r.URL.Path relpath := r.URL.Path[len(h.pattern):]
path = path[len(h.pattern):] info := h.getPageInfo(relpath, false)
info := h.getPageInfo(path, false)
var buf bytes.Buffer
if r.FormValue("f") == "text" { if r.FormValue("f") == "text" {
if err := packageText.Execute(info, &buf); err != nil { contents := applyTemplate(packageText, "packageText", info)
log.Stderrf("packageText.Execute: %s", err) serveText(c, contents)
}
serveText(c, buf.Bytes())
return return
} }
if err := packageHTML.Execute(info, &buf); err != nil { var title string
log.Stderrf("packageHTML.Execute: %s", err)
}
if path == "" {
path = "." // don't display an empty path
}
title := "Directory " + path
if info.PDoc != nil { if info.PDoc != nil {
switch { switch {
case h.isPkg: case h.isPkg:
title = "Package " + info.PDoc.PackageName title = "Package " + info.PDoc.PackageName
case info.PDoc.PackageName == fakePkgName: case info.PDoc.PackageName == fakePkgName:
// assume that the directory name is the command name // assume that the directory name is the command name
_, pkgname := pathutil.Split(pathutil.Clean(path)) _, pkgname := pathutil.Split(pathutil.Clean(relpath))
title = "Command " + pkgname title = "Command " + pkgname
default: default:
title = "Command " + info.PDoc.PackageName title = "Command " + info.PDoc.PackageName
} }
} else {
title = "Directory " + relativePath(info.Dirname)
} }
servePage(c, title, "", buf.Bytes()) contents := applyTemplate(packageHTML, "packageHTML", info)
servePage(c, title, "", contents)
} }
@ -1197,11 +1241,6 @@ func search(c *http.Conn, r *http.Request) {
result.Accurate = timestamp >= ts result.Accurate = timestamp >= ts
} }
var buf bytes.Buffer
if err := searchHTML.Execute(result, &buf); err != nil {
log.Stderrf("searchHTML.Execute: %s", err)
}
var title string var title string
if result.Hit != nil { if result.Hit != nil {
title = fmt.Sprintf(`Results for query %q`, query) title = fmt.Sprintf(`Results for query %q`, query)
@ -1209,28 +1248,14 @@ func search(c *http.Conn, r *http.Request) {
title = fmt.Sprintf(`No results found for query %q`, query) title = fmt.Sprintf(`No results found for query %q`, query)
} }
servePage(c, title, query, buf.Bytes()) contents := applyTemplate(searchHTML, "searchHTML", result)
servePage(c, title, query, contents)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Server // Indexer
var (
cmdHandler = httpHandler{"/cmd/", *cmdroot, false}
pkgHandler = httpHandler{"/pkg/", *pkgroot, true}
)
func registerPublicHandlers(mux *http.ServeMux) {
mux.Handle(cmdHandler.pattern, &cmdHandler)
mux.Handle(pkgHandler.pattern, &pkgHandler)
mux.Handle("/search", http.HandlerFunc(search))
mux.Handle("/", http.HandlerFunc(serveFile))
}
// Indexing goroutine.
func indexer() { func indexer() {
for { for {
_, ts := fsTree.get() _, ts := fsTree.get()
@ -1240,7 +1265,7 @@ func indexer() {
// from the sync goroutine, but this solution is // from the sync goroutine, but this solution is
// more decoupled, trivial, and works well enough) // more decoupled, trivial, and works well enough)
start := time.Nanoseconds() start := time.Nanoseconds()
index := NewIndex(".") index := NewIndex(goroot)
stop := time.Nanoseconds() stop := time.Nanoseconds()
searchIndex.set(index) searchIndex.set(index)
if *verbose { if *verbose {

View File

@ -47,9 +47,6 @@ var (
// layout control // layout control
html = flag.Bool("html", false, "print HTML in command-line mode") html = flag.Bool("html", false, "print HTML in command-line mode")
// --- Hack to remember current directory
workingDir string
) )
@ -112,7 +109,7 @@ func dosync(c *http.Conn, r *http.Request) {
// TODO(gri): The directory tree may be temporarily out-of-sync. // TODO(gri): The directory tree may be temporarily out-of-sync.
// Consider keeping separate time stamps so the web- // Consider keeping separate time stamps so the web-
// page can indicate this discrepancy. // page can indicate this discrepancy.
fsTree.set(newDirectory(".", maxDirDepth)) fsTree.set(newDirectory(goroot, maxDirDepth))
fallthrough fallthrough
case 1: case 1:
// sync failed because no files changed; // sync failed because no files changed;
@ -155,17 +152,7 @@ func main() {
log.Exitf("negative tabwidth %d", *tabwidth) log.Exitf("negative tabwidth %d", *tabwidth)
} }
// --- Start of hack. initRoots()
// Remember where we were, so "." works as a directory name.
// Error's not worth worrying about; we just check for empty string
// when we need it.
workingDir, _ = os.Getwd()
// --- End of hack.
if err := os.Chdir(goroot); err != nil {
log.Exitf("chdir %s: %v", goroot, err)
}
readTemplates() readTemplates()
if *httpaddr != "" { if *httpaddr != "" {
@ -175,10 +162,14 @@ func main() {
log.Stderrf("Go Documentation Server\n") log.Stderrf("Go Documentation Server\n")
log.Stderrf("address = %s\n", *httpaddr) log.Stderrf("address = %s\n", *httpaddr)
log.Stderrf("goroot = %s\n", goroot) log.Stderrf("goroot = %s\n", goroot)
log.Stderrf("cmdroot = %s\n", *cmdroot) log.Stderrf("cmdroot = %s\n", cmdroot)
log.Stderrf("pkgroot = %s\n", *pkgroot) log.Stderrf("pkgroot = %s\n", pkgroot)
log.Stderrf("tmplroot = %s\n", *tmplroot) log.Stderrf("tmplroot = %s\n", tmplroot)
log.Stderrf("tabwidth = %d\n", *tabwidth) log.Stderrf("tabwidth = %d\n", *tabwidth)
if !fsMap.IsEmpty() {
log.Stderr("user-defined mapping:")
fsMap.Fprint(os.Stderr)
}
handler = loggingHandler(handler) handler = loggingHandler(handler)
} }
@ -192,7 +183,7 @@ func main() {
// 1) set timestamp right away so that the indexer is kicked on // 1) set timestamp right away so that the indexer is kicked on
fsTree.set(nil) fsTree.set(nil)
// 2) compute initial directory tree in a goroutine so that launch is quick // 2) compute initial directory tree in a goroutine so that launch is quick
go func() { fsTree.set(newDirectory(".", maxDirDepth)) }() go func() { fsTree.set(newDirectory(goroot, maxDirDepth)) }()
// Start sync goroutine, if enabled. // Start sync goroutine, if enabled.
if *syncCmd != "" && *syncMin > 0 { if *syncCmd != "" && *syncMin > 0 {

View File

@ -27,13 +27,13 @@ import (
// until a valid mapping is found. For instance, for the mapping: // until a valid mapping is found. For instance, for the mapping:
// //
// user -> /home/user // user -> /home/user
// public -> /home/user/public // public -> /home/user/public
// public -> /home/build/public // public -> /home/build/public
// //
// the relative paths below are mapped to absolute paths as follows: // the relative paths below are mapped to absolute paths as follows:
// //
// user/foo -> /home/user/foo // user/foo -> /home/user/foo
// public/net/rpc/file1.go -> /home/user/public/net/rpc/file1.go // public/net/rpc/file1.go -> /home/user/public/net/rpc/file1.go
// //
// If there is no /home/user/public/net/rpc/file2.go, the next public // If there is no /home/user/public/net/rpc/file2.go, the next public
// mapping entry is used to map the relative path to: // mapping entry is used to map the relative path to:
@ -43,7 +43,8 @@ import (
// (assuming that file exists). // (assuming that file exists).
// //
type Mapping struct { type Mapping struct {
list []mapping list []mapping
prefixes []string
} }
@ -71,7 +72,7 @@ type mapping struct {
// leads to the following mapping: // leads to the following mapping:
// //
// user -> /home/user // user -> /home/user
// public -> /home/build/public // public -> /home/build/public
// //
func (m *Mapping) Init(paths string) { func (m *Mapping) Init(paths string) {
cwd, _ := os.Getwd() // ignore errors cwd, _ := os.Getwd() // ignore errors
@ -105,7 +106,7 @@ func (m *Mapping) Init(paths string) {
// add mapping if it is new // add mapping if it is new
if i >= n { if i >= n {
_, prefix := pathutil.Split(path) _, prefix := pathutil.Split(path)
list[i] = mapping{prefix, path} list[n] = mapping{prefix, path}
n++ n++
} }
} }
@ -118,6 +119,45 @@ func (m *Mapping) Init(paths string) {
func (m *Mapping) IsEmpty() bool { return len(m.list) == 0 } func (m *Mapping) IsEmpty() bool { return len(m.list) == 0 }
// PrefixList returns a list of all prefixes, with duplicates removed.
// For instance, for the mapping:
//
// user -> /home/user
// public -> /home/user/public
// public -> /home/build/public
//
// the prefix list is:
//
// user, public
//
func (m *Mapping) PrefixList() []string {
// compute the list lazily
if m.prefixes == nil {
list := make([]string, len(m.list))
n := 0 // nuber of prefixes
for _, e := range m.list {
// check if prefix exists already
var i int
for i = 0; i < n; i++ {
if e.prefix == list[i] {
break
}
}
// add prefix if it is new
if i >= n {
list[n] = e.prefix
n++
}
}
m.prefixes = list[0:n]
}
return m.prefixes
}
// Fprint prints the mapping. // Fprint prints the mapping.
func (m *Mapping) Fprint(w io.Writer) { func (m *Mapping) Fprint(w io.Writer) {
for _, e := range m.list { for _, e := range m.list {