diff --git a/_content/css/styles.css b/_content/css/styles.css index 921c8b0418..eea617d8b1 100644 --- a/_content/css/styles.css +++ b/_content/css/styles.css @@ -5370,3 +5370,9 @@ a.downloadBox:hover .filename span { display: none; } } + +hr { + border: none; + border-top: 1px solid var(--color-border); + height: 1px; +} diff --git a/_content/site.tmpl b/_content/site.tmpl index a663bf6123..2057dc0ce3 100644 --- a/_content/site.tmpl +++ b/_content/site.tmpl @@ -71,7 +71,7 @@ {{.name}} {{if .children}}{{end}} - {{end}} + +{{if strings.HasPrefix .URL "/wiki/"}} +
+

+This content is part of the Go Wiki. +

+{{end}} {{end}} diff --git a/_content/wiki/Comments.md b/_content/wiki/Comments.md new file mode 100644 index 0000000000..98ce74dea4 --- /dev/null +++ b/_content/wiki/Comments.md @@ -0,0 +1,50 @@ +--- +title: Comments +--- + + + +Every package should have a package comment. It should immediately precede the ` package ` statement in one of the files in the package. (It only needs to appear in one file.) It should begin with a single sentence that begins "Package _packagename_" and give a concise summary of the package functionality. This introductory sentence will be used in godoc's list of all packages. + +Subsequent sentences and/or paragraphs can give more details. Sentences should be properly punctuated. + +```go +// Package superman implements methods for saving the world. +// +// Experience has shown that a small number of procedures can prove +// helpful when attempting to save the world. +package superman +``` + +Nearly every top-level type, const, var and func should have a comment. A comment for bar should be in the form "_bar_ floats on high o'er vales and hills.". The first letter of _bar_ should not be capitalized unless it's capitalized in the code. + +```go +// enterOrbit causes Superman to fly into low Earth orbit, a position +// that presents several possibilities for planet salvation. +func enterOrbit() os.Error { + ... +} +``` + +All text that you indent inside a comment, godoc will render as a pre-formatted block. This facilitates code samples. + +```go +// fight can be used on any enemy and returns whether Superman won. +// +// Examples: +// +// fight("a random potato") +// fight(LexLuthor{}) +// +func fight(enemy interface{}) bool { + // This is testing proper escaping in the wiki. + for i := 0; i < 10; i++ { + println("fight!") + } +} +``` + + diff --git a/_content/wiki/default.tmpl b/_content/wiki/default.tmpl new file mode 100644 index 0000000000..d57fc0b83a --- /dev/null +++ b/_content/wiki/default.tmpl @@ -0,0 +1,3 @@ +{{define "layout"}} +{{doclayout .}} +{{end}} diff --git a/_content/wiki/index.md b/_content/wiki/index.md new file mode 100644 index 0000000000..828c4f510c --- /dev/null +++ b/_content/wiki/index.md @@ -0,0 +1,5 @@ +--- +title: Index +--- + +The Wiki is still loading... diff --git a/cmd/golangorg/cloudbuild.yaml b/cmd/golangorg/cloudbuild.yaml index c6edaa6e9b..48b5296cbf 100644 --- a/cmd/golangorg/cloudbuild.yaml +++ b/cmd/golangorg/cloudbuild.yaml @@ -3,6 +3,7 @@ # Do not run directly. steps: + # Clone go repo to _goroot.zip for use by uploaded app. - name: gcr.io/cloud-builders/git args: ["clone", "--branch=release-branch.go1.21", "--depth=1", "https://go.googlesource.com/go", "_gotmp"] - name: gcr.io/cloud-builders/git @@ -10,14 +11,28 @@ steps: dir: _gotmp - name: golang args: ["rm", "-rf", "_gotmp"] + # Clone wiki repo into _content/wiki as initial wiki content. + # The server will replace this with the wiki repo when it can, + # but this provides a good fallback. + - name: gcr.io/cloud-builders/git + args: ["clone", "--depth=1", "https://go.googlesource.com/wiki", "_wikitmp"] + - name: golang + args: ["rm", "-rf", "_wikitmp/.git"] + - name: golang + args: ["cp", "-a", "_wikitmp/*", "_content/wiki"] + # Run tests. - name: golang args: ["go", "test", "./..."] + # Coordinate with other Cloud Build jobs to deploy only newest commit. + # May abort job here. - name: golang args: ["go", "run", "./cmd/locktrigger", "--project=$PROJECT_ID", "--build=$BUILD_ID", "--repo=https://go.googlesource.com/website"] + # Deploy site and redirect traffic (maybe; tests again in prod first). - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: ["./go-app-deploy.sh", "cmd/golangorg/app.yaml"] + # Clean up stale versions. - name: golang args: ["go", "run", "./cmd/versionprune", "--dry_run=false", "--project=$PROJECT_ID", "--service=default"] diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go index 7665efa159..4f87330702 100644 --- a/cmd/golangorg/server.go +++ b/cmd/golangorg/server.go @@ -59,7 +59,8 @@ var ( runningOnAppEngine = os.Getenv("PORT") != "" - tipFlag = flag.Bool("tip", runningOnAppEngine, "load git content for tip.golang.org") + tipFlag = flag.Bool("tip", runningOnAppEngine, "load git content for tip.golang.org") + wikiFlag = flag.Bool("wiki", runningOnAppEngine, "load git content for go.dev/wiki") googleAnalytics string ) @@ -157,6 +158,19 @@ func NewHandler(contentDir, goroot string) http.Handler { gorootFS = os.DirFS(goroot) } + // go.dev/wiki serves content from the very latest Git commit of the wiki repo. + // Start with the _content/wiki directory as placeholder until Git loads. + var wikiFS atomicFS + wikiDefault, err := fs.Sub(contentFS, "wiki") + if err != nil { + log.Fatalf("loading default wiki content: %v", err) + } + wikiFS.Set(wikiDefault) + if *wikiFlag { + go watchGit(&wikiFS, "https://go.googlesource.com/wiki") + } + contentFS = &mountFS{contentFS, "wiki", &wikiFS} + // tip.golang.org serves content from the very latest Git commit // of the main Go repo, instead of the one the app is bundled with. var tipGoroot atomicFS @@ -164,7 +178,7 @@ func NewHandler(contentDir, goroot string) http.Handler { log.Fatalf("loading tip site: %v", err) } if *tipFlag { - go watchTip(&tipGoroot) + go watchGit(&tipGoroot, "https://go.googlesource.com/go") } // beta.golang.org is an old name for tip. @@ -280,32 +294,32 @@ func parseRFC3339(s string) (time.Time, error) { return time.Parse(time.RFC3339, s) } -// watchTip is a background goroutine that watches the main Go repo for updates. -// When a new commit is available, watchTip downloads the new tree and calls -// tipGoroot.Set to install the new file system. -func watchTip(tipGoroot *atomicFS) { +// watchGit is a background goroutine that watches a Git repo for updates. +// When a new commit is available, watchGit downloads the new tree and calls +// fsys.Set to install the new file system. +func watchGit(fsys *atomicFS, repo string) { for { - // watchTip1 runs until it panics (hopefully never). + // watchGit1 runs until it panics (hopefully never). // If that happens, sleep 5 minutes and try again. - watchTip1(tipGoroot) + watchGit1(fsys, repo) time.Sleep(5 * time.Minute) } } -// watchTip1 does the actual work of watchTip and recovers from panics. -func watchTip1(tipGoroot *atomicFS) { +// watchGit1 does the actual work of watchGit and recovers from panics. +func watchGit1(afs *atomicFS, repo string) { defer func() { if e := recover(); e != nil { - log.Printf("watchTip panic: %v\n%s", e, debug.Stack()) + log.Printf("watchGit %s panic: %v\n%s", repo, e, debug.Stack()) } }() var r *gitfs.Repo for { var err error - r, err = gitfs.NewRepo("https://go.googlesource.com/go") + r, err = gitfs.NewRepo(repo) if err != nil { - log.Printf("tip: %v", err) + log.Printf("watchGit %s: %v", repo, err) time.Sleep(1 * time.Minute) continue } @@ -318,11 +332,11 @@ func watchTip1(tipGoroot *atomicFS) { var err error h, fsys, err = r.Clone("HEAD") if err != nil { - log.Printf("tip: %v", err) + log.Printf("watchGit %s: %v", repo, err) time.Sleep(1 * time.Minute) continue } - tipGoroot.Set(fsys) + afs.Set(fsys) break } @@ -330,17 +344,17 @@ func watchTip1(tipGoroot *atomicFS) { time.Sleep(5 * time.Minute) h2, err := r.Resolve("HEAD") if err != nil { - log.Printf("tip: %v", err) + log.Printf("watchGit %s: %v", repo, err) continue } if h2 != h { fsys, err := r.CloneHash(h2) if err != nil { - log.Printf("tip: %v", err) + log.Printf("watchGit %s: %v", repo, err) time.Sleep(1 * time.Minute) continue } - tipGoroot.Set(fsys) + afs.Set(fsys) h = h2 } } @@ -715,6 +729,7 @@ func (fsys fixSpecsFS) Open(name string) (fs.File, error) { // README.md, and SECURITY.md. The last is particularly problematic // when running locally on a Mac, because it can be opened as // security.md, which takes priority over _content/security.html. +// Same for wiki. type hideRootMDFS struct { fs fs.FS } @@ -723,6 +738,10 @@ func (fsys hideRootMDFS) Open(name string) (fs.File, error) { if !strings.Contains(name, "/") && strings.HasSuffix(name, ".md") { return nil, errors.New(".md file not available") } + switch name { + case "wiki/README.md", "wiki/CONTRIBUTING.md", "wiki/LICENSE", "wiki/PATENTS", "wiki/codereview.cfg": + return nil, errors.New("wiki meta file not available") + } return fsys.fs.Open(name) } @@ -795,6 +814,23 @@ func (a *atomicFS) Set(fsys fs.FS) { a.v.Store(&fsys) } +// A mountFS is a root FS with a second FS mounted at a specific location. +type mountFS struct { + old fs.FS // root file system + dir string // mount point + new fs.FS // fs mounted on dir +} + +func (m *mountFS) Open(name string) (fs.File, error) { + if name == m.dir { + return m.new.Open(".") + } + if strings.HasPrefix(name, m.dir) && len(name) > len(m.dir) && name[len(m.dir)] == '/' { + return m.new.Open(name[len(m.dir)+1:]) + } + return m.old.Open(name) +} + // Open returns fsys.Open(name) where fsys is the file system passed to the most recent call to Set. // If there has been no call to Set, Open returns an error with text “no file system”. func (a *atomicFS) Open(name string) (fs.File, error) { diff --git a/cmd/golangorg/server_test.go b/cmd/golangorg/server_test.go index fd5c88924a..a31688a8ae 100644 --- a/cmd/golangorg/server_test.go +++ b/cmd/golangorg/server_test.go @@ -114,6 +114,9 @@ func TestAll(t *testing.T) { // Do not process these paths or path prefixes. ignores := []string{ + // Wiki is in a different repo; errors there should not block production push. + "/wiki/", + // Support files not meant to be served directly. "/doc/articles/wiki/", "/talks/2013/highperf/", diff --git a/cmd/golangorg/testdata/godev.txt b/cmd/golangorg/testdata/godev.txt index c071130a03..f33b057d79 100644 --- a/cmd/golangorg/testdata/godev.txt +++ b/cmd/golangorg/testdata/godev.txt @@ -109,3 +109,11 @@ redirect == /doc/security/ GET https://go.dev/doc/security/ body contains Security + +GET https://go.dev/wiki/ +body contains Go Wiki: Index +body contains This content is part of the Go Wiki. + +GET https://go.dev/wiki/Comments +body contains Go Wiki: Comments +body contains This content is part of the Go Wiki. diff --git a/cmd/golangorg/testdata/live.txt b/cmd/golangorg/testdata/live.txt index 28b4ae9ef3..20470beebd 100644 --- a/cmd/golangorg/testdata/live.txt +++ b/cmd/golangorg/testdata/live.txt @@ -62,3 +62,11 @@ header Content-Type == text/plain; charset=utf-8 body !contains The Go Playground body !contains About the Playground body contains Hello, 世界 + +GET https://go.dev/wiki/Comments +body contains Go Wiki: Comments +body contains This content is part of the Go Wiki. + +GET https://go.dev/wiki/CommonMistakes +body contains Go Wiki: Common Mistakes +body contains This content is part of the Go Wiki. diff --git a/internal/redirect/redirect.go b/internal/redirect/redirect.go index f71f225a1b..e12455b89f 100644 --- a/internal/redirect/redirect.go +++ b/internal/redirect/redirect.go @@ -111,8 +111,6 @@ var redirects = map[string]string{ "/doc/mem": "/ref/mem", "/doc/spec": "/ref/spec", - "/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", @@ -163,7 +161,6 @@ var newIssueRedirects = [...]string{ var prefixHelpers = map[string]string{ "issue": "https://github.com/golang/go/issues/", "issues": "https://github.com/golang/go/issues/", - "wiki": "https://github.com/golang/go/wiki/", } func Handler(target string) http.Handler { diff --git a/internal/redirect/redirect_test.go b/internal/redirect/redirect_test.go index 2e788c684f..10d4de05ee 100644 --- a/internal/redirect/redirect_test.go +++ b/internal/redirect/redirect_test.go @@ -85,9 +85,6 @@ func TestRedirects(t *testing.T) { "/issues/new/choose": errorResult(404), "/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"}, diff --git a/internal/web/render.go b/internal/web/render.go index bb3ef1b9be..a21cd049f5 100644 --- a/internal/web/render.go +++ b/internal/web/render.go @@ -127,6 +127,12 @@ func (site *Site) renderHTML(p Page, tmpl string, r *http.Request) ([]byte, erro // Either the page explicitly requested templating, or it is markdown, // which is treated as a template by default. isTemplate, explicit := p["template"].(bool) + + // The wiki is not templated by default. + if !explicit && strings.HasPrefix(file, "wiki/") { + isTemplate, explicit = false, true + } + tdata := data if !explicit || isTemplate { // Load content as a template. @@ -193,6 +199,7 @@ func markdownToHTML(markdown string) (template.HTML, error) { extension.WithLinkifyEmailRegexp(regexp.MustCompile(`[^\x00-\x{10FFFF}]`)), // impossible ), extension.DefinitionList, + extension.NewTable(), ), ) var buf bytes.Buffer