internal/web: redirect golang.org/pkg/... to pkg.go.dev/...

This reduces the number of documentation sites we have to one.
Except in China, where we have to keep serving on the one domain
golang.google.cn - there is no pkg.go.dev in China.
And unless people opt out with ?m=old.

For golang/go#44356.

Change-Id: I2a5b788ac861ce37f356287413468497d184fc09
Reviewed-on: https://go-review.googlesource.com/c/website/+/327849
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/golangorg/testdata/web.txt b/cmd/golangorg/testdata/web.txt
index 676e9d7..addbf91 100644
--- a/cmd/golangorg/testdata/web.txt
+++ b/cmd/golangorg/testdata/web.txt
@@ -18,6 +18,9 @@
 body !contains UA-
 
 GET https://golang.org/cmd/compile/internal/amd64/
+redirect == https://pkg.go.dev/cmd/compile/internal/amd64
+
+GET https://golang.org/cmd/compile/internal/amd64/?m=old
 body contains href="/src/cmd/compile/internal/amd64/ssa.go"
 
 GET https://golang.org/conduct
@@ -74,57 +77,94 @@
 GET https://golang.org/help
 body contains Get help
 
-GET https://golang.org/pkg/fmt/
-body contains Package fmt implements formatted I/O
-
 GET https://golang.org/src/fmt/
 body contains scan_test.go
 
 GET https://golang.org/src/fmt/print.go
 body contains // Println formats using
 
+GET https://golang.org/pkg/fmt/
+redirect == https://pkg.go.dev/fmt
+
+GET https://golang.org/pkg/fmt/?m=old
+body contains Package fmt implements formatted I/O
+
+GET https://golang.google.cn/pkg/fmt/
+body contains Package fmt implements formatted I/O
+
 GET https://golang.org/pkg
 redirect == /pkg/
 
 GET https://golang.org/pkg/
+redirect == https://pkg.go.dev/std
+
+GET https://tip.golang.org/pkg/
+redirect == https://pkg.go.dev/std@master
+
+GET https://golang.org/pkg?m=old
+redirect == /pkg/?m=old
+
+GET https://golang.org/pkg/?m=old
 body contains Standard library
 body contains Package fmt implements formatted I/O
 body !contains internal/syscall
 body !contains cmd/gc
 
-GET https://golang.org/pkg/?m=all
+GET https://golang.org/pkg/?m=old,all
 body contains Standard library
 body contains Package fmt implements formatted I/O
 body contains internal/syscall/?m=all
 body !contains cmd/gc
 
 GET https://golang.org/pkg/bufio/
-body contains href="/pkg/io/#Writer
+redirect == https://pkg.go.dev/bufio
+
+GET https://golang.org/pkg/bufio/?GOOS=windows&GOARCH=amd64
+redirect == https://pkg.go.dev/bufio?GOOS=windows&GOARCH=amd64
+
+GET https://tip.golang.org/pkg/bufio/
+redirect == https://pkg.go.dev/bufio@master
+
+GET https://golang.org/pkg/bufio/?m=old
+body contains href="/pkg/io/?m=old#Writer
+body !contains href="/pkg/io/#Writer
 
 GET https://golang.org/pkg/database/sql/
+redirect == https://pkg.go.dev/database/sql
+
+GET https://golang.org/pkg/database/sql/?m=old
 body contains The number of connections currently in use; added in Go 1.11
 body contains The number of idle connections; added in Go 1.11
 
-GET https://golang.org/cmd/compile/internal/amd64/
-body contains href="/src/cmd/compile/internal/amd64/ssa.go"
-
 GET https://golang.org/pkg/math/bits/
+redirect == https://pkg.go.dev/math/bits
+
+GET https://golang.org/pkg/math/bits/?m=old
 body contains Added in Go 1.9
 
 GET https://golang.org/pkg/net/
+redirect == https://pkg.go.dev/net
+
+GET https://golang.org/pkg/net/?m=old
 body contains // IPv6 scoped addressing zone; added in Go 1.1
 
-GET https://golang.org/pkg/net/http/
+GET https://golang.org/pkg/net/http/?m=old
 body contains title="Added in Go 1.11"
 
 GET https://golang.org/pkg/net/http/httptrace/
+redirect == https://pkg.go.dev/net/http/httptrace
+
+GET https://golang.org/pkg/net/http/httptrace/?m=old
 body ~ Got1xxResponse.*// Go 1\.11
 body ~ GotFirstResponseByte func\(\)\s*$
 
-GET https://golang.org/pkg/os/
+GET https://golang.org/pkg/os/?m=old
 body contains func Open
 
 GET https://golang.org/pkg/strings/
+redirect == https://pkg.go.dev/strings
+
+GET https://golang.org/pkg/strings/?m=old
 body contains href="/src/strings/strings.go"
 
 GET https://golang.org/project
diff --git a/internal/pkgdoc/doc.go b/internal/pkgdoc/doc.go
index edb5a9f..6dd86e8 100644
--- a/internal/pkgdoc/doc.go
+++ b/internal/pkgdoc/doc.go
@@ -70,6 +70,7 @@
 	ModeAll     Mode = 1 << iota // do not filter exports
 	ModeFlat                     // show directory in a flat (non-indented) manner
 	ModeMethods                  // show all embedded methods
+	ModeOld                      // do not redirect to pkg.go.dev
 	ModeBuiltin                  // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
 )
 
@@ -79,6 +80,7 @@
 	"all",
 	"flat",
 	"methods",
+	"old",
 }
 
 // generate a query string for persisting PageInfoMode between pages.
diff --git a/internal/texthtml/ast.go b/internal/texthtml/ast.go
index 76bd4bb..c7e0560 100644
--- a/internal/texthtml/ast.go
+++ b/internal/texthtml/ast.go
@@ -20,16 +20,17 @@
 type goLink struct {
 	path, name string // package path, identifier name
 	isVal      bool   // identifier is defined in a const or var declaration
+	oldDocs    bool   // link to ?m=old docs
 }
 
 func (l *goLink) tags() (start, end string) {
 	switch {
 	case l.path != "" && l.name == "":
 		// package path
-		return `<a href="/pkg/` + l.path + `/">`, `</a>`
+		return `<a href="/pkg/` + l.path + `/` + l.docSuffix() + `">`, `</a>`
 	case l.path != "" && l.name != "":
 		// qualified identifier
-		return `<a href="/pkg/` + l.path + `/#` + l.name + `">`, `</a>`
+		return `<a href="/pkg/` + l.path + `/` + l.docSuffix() + `#` + l.name + `">`, `</a>`
 	case l.path == "" && l.name != "":
 		// local identifier
 		if l.isVal {
@@ -42,6 +43,13 @@
 	return "", ""
 }
 
+func (l *goLink) docSuffix() string {
+	if l.oldDocs {
+		return "?m=old"
+	}
+	return ""
+}
+
 // goLinksFor returns the list of links for the identifiers used
 // by node in the same order as they appear in the source.
 func goLinksFor(node ast.Node) (links []goLink) {
diff --git a/internal/texthtml/texthtml.go b/internal/texthtml/texthtml.go
index 1175fe1..334085a 100644
--- a/internal/texthtml/texthtml.go
+++ b/internal/texthtml/texthtml.go
@@ -37,6 +37,7 @@
 	Highlight  string    // highlight matches for this regexp with <span class="highlight">
 	Selection  Selection // mark selected spans with <span class="selection">
 	AST        ast.Node  // link uses to declarations, assuming text is formatting of AST
+	OldDocs    bool      // emit links to ?m=old docs
 }
 
 // Format formats text to HTML according to the configuration cfg.
@@ -55,6 +56,11 @@
 	if cfg.AST != nil {
 		idents = tokenSelection(text, token.IDENT)
 		goLinks = goLinksFor(cfg.AST)
+		if cfg.OldDocs {
+			for i := range goLinks {
+				goLinks[i].oldDocs = true
+			}
+		}
 	}
 
 	formatSelections(&buf, text, goLinks, comments, highlights, cfg.Selection, idents)
diff --git a/internal/web/astfuncs.go b/internal/web/astfuncs.go
index 79d193d..4fbe5f7 100644
--- a/internal/web/astfuncs.go
+++ b/internal/web/astfuncs.go
@@ -36,6 +36,7 @@
 	buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
 		AST:        n,
 		GoComments: true,
+		OldDocs:    p.OldDocs,
 	}))
 	return template.HTML(buf2.String())
 }
@@ -50,6 +51,7 @@
 	var buf2 bytes.Buffer
 	buf2.Write(texthtml.Format(buf1.Bytes(), texthtml.Config{
 		GoComments: true,
+		OldDocs:    p.OldDocs,
 	}))
 
 	return sanitize(template.HTML(buf2.String()))
diff --git a/internal/web/pkgdoc.go b/internal/web/pkgdoc.go
index 212526d..21dc1cd 100644
--- a/internal/web/pkgdoc.go
+++ b/internal/web/pkgdoc.go
@@ -7,6 +7,7 @@
 import (
 	"log"
 	"net/http"
+	"net/url"
 	"path"
 	"strings"
 
@@ -29,6 +30,36 @@
 	relpath = strings.TrimPrefix(relpath, "/")
 
 	mode := pkgdoc.ParseMode(r.FormValue("m"))
+
+	// Redirect to pkg.go.dev.
+	// We provide two overrides for the redirect.
+	// First, the request can set ?m=old to get the old pages.
+	// Second, the request can come from China:
+	// since pkg.go.dev is not available in China, we serve the docs directly.
+	if mode&pkgdoc.ModeOld == 0 && !GoogleCN(r) {
+		if relpath == "" {
+			relpath = "std"
+		}
+		suffix := ""
+		if r.Host == "tip.golang.org" {
+			suffix = "@master"
+		}
+		if goos, goarch := r.FormValue("GOOS"), r.FormValue("GOARCH"); goos != "" || goarch != "" {
+			suffix += "?"
+			if goos != "" {
+				suffix += "GOOS=" + url.QueryEscape(goos)
+			}
+			if goarch != "" {
+				if goos != "" {
+					suffix += "&"
+				}
+				suffix += "GOARCH=" + url.QueryEscape(goarch)
+			}
+		}
+		http.Redirect(w, r, "https://pkg.go.dev/"+relpath+suffix, http.StatusTemporaryRedirect)
+		return
+	}
+
 	if relpath == "builtin" {
 		// The fake built-in package contains unexported identifiers,
 		// but we want to show them. Also, disable type association,
@@ -81,6 +112,7 @@
 		Subtitle: subtitle,
 		Template: name,
 		Data:     info,
+		OldDocs:  mode&pkgdoc.ModeOld != 0,
 	})
 }
 
diff --git a/internal/web/site.go b/internal/web/site.go
index 245c99e..96da21c 100644
--- a/internal/web/site.go
+++ b/internal/web/site.go
@@ -117,6 +117,9 @@
 	Template string      // template to apply to data (empty string when Data is raw template.HTML)
 	Data     interface{} // data to be rendered into page frame
 
+	// Filled in for document rendering
+	OldDocs bool // use ?m=old in doc links
+
 	// Filled in automatically by ServePage
 	GoogleCN        bool   // served on golang.google.cn
 	GoogleAnalytics string // Google Analytics tag