diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index 7e070aa..c3541cd 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -152,18 +152,18 @@
TaskIDChangeInterval: config.TaskIDChangeIntervalFrontend,
}
server, err := frontend.NewServer(frontend.ServerConfig{
- Config: cfg,
- FetchServer: fetchServer,
- DataSourceGetter: dsg,
- Queue: fetchQueue,
- TemplateFS: template.TrustedFSFromTrustedSource(staticSource),
- StaticFS: os.DirFS(*staticFlag),
- ThirdPartyFS: os.DirFS(*thirdPartyPath),
- DevMode: *devMode,
- LocalMode: *localMode,
- Reporter: reporter,
- VulndbClient: vc,
- DepsDevHTTPClient: &http.Client{Transport: new(ochttp.Transport)},
+ Config: cfg,
+ FetchServer: fetchServer,
+ DataSourceGetter: dsg,
+ Queue: fetchQueue,
+ TemplateFS: template.TrustedFSFromTrustedSource(staticSource),
+ StaticFS: os.DirFS(*staticFlag),
+ ThirdPartyFS: os.DirFS(*thirdPartyPath),
+ DevMode: *devMode,
+ LocalMode: *localMode,
+ Reporter: reporter,
+ VulndbClient: vc,
+ HTTPClient: &http.Client{Transport: new(ochttp.Transport)},
})
if err != nil {
log.Fatalf(ctx, "frontend.NewServer: %v", err)
diff --git a/internal/frontend/codewiki.go b/internal/frontend/codewiki.go
new file mode 100644
index 0000000..8588b3a
--- /dev/null
+++ b/internal/frontend/codewiki.go
@@ -0,0 +1,77 @@
+// Copyright 2025 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 frontend
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/log"
+)
+
+var (
+ codeWikiURLBase = "https://codewiki.google/"
+ codeWikiExistsURL = "https://codewiki.google/_/exists/"
+ timeout = 1 * time.Second
+)
+
+// codeWikiURLGenerator returns a function that will return a URL for the given
+// module version on codewiki. If the URL can't be generated within
+// timeout then the empty string is returned instead.
+func codeWikiURLGenerator(ctx context.Context, client *http.Client, um *internal.UnitMeta) func() string {
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ url := make(chan string, 1)
+ go func() {
+ u, err := fetchCodeWikiURL(ctx, client, um.ModulePath)
+ switch {
+ case errors.Is(err, context.Canceled):
+ log.Warningf(ctx, "fetching url from codewiki.google: %v", err)
+ case errors.Is(err, context.DeadlineExceeded):
+ log.Warningf(ctx, "fetching url from codewiki.google: %v", err)
+ case err != nil:
+ log.Errorf(ctx, "fetching url from codewiki.google: %v", err)
+ }
+ url <- u
+ }()
+ return func() string {
+ defer cancel()
+ return <-url
+ }
+}
+
+// fetchCodeWikiURL makes a request to codewiki to check whether the given
+// path is known there, and if so it returns the link to that page.
+func fetchCodeWikiURL(ctx context.Context, client *http.Client, path string) (string, error) {
+ repoPath := path
+ if strings.HasPrefix(path, "golang.org/x/") {
+ repoPath = strings.Replace(path, "golang.org/x/", "github.com/golang/", 1)
+ }
+ if !strings.HasPrefix(repoPath, "github.com/") {
+ return "", nil
+ }
+ ghRepoPath := repoPath
+ parts := strings.Split(ghRepoPath, "/")
+ if len(parts) > 3 {
+ ghRepoPath = strings.Join(parts[:3], "/")
+ }
+ req, err := http.NewRequestWithContext(ctx, "GET", codeWikiExistsURL+ghRepoPath, nil)
+ if err != nil {
+ return "", err
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ return fmt.Sprintf("%s%s", codeWikiURLBase, repoPath), nil
+ }
+ return "", nil
+}
diff --git a/internal/frontend/codewiki_test.go b/internal/frontend/codewiki_test.go
new file mode 100644
index 0000000..ded75b7
--- /dev/null
+++ b/internal/frontend/codewiki_test.go
@@ -0,0 +1,75 @@
+// Copyright 2021 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 frontend
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "golang.org/x/pkgsite/internal"
+)
+
+func TestCodeWikiURLGenerator(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/_/exists/github.com/owner/repo", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+ mux.HandleFunc("/_/exists/github.com/golang/glog", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+ server := httptest.NewServer(mux)
+ t.Cleanup(server.Close)
+
+ oldCodeWikiURLBase := codeWikiURLBase
+ oldCodeWikiExistsURL := codeWikiExistsURL
+ codeWikiURLBase = server.URL + "/"
+ codeWikiExistsURL = server.URL + "/_/exists/"
+ t.Cleanup(func() {
+ codeWikiURLBase = oldCodeWikiURLBase
+ codeWikiExistsURL = oldCodeWikiExistsURL
+ })
+
+ testCases := []struct {
+ name, modulePath, path string
+ want string
+ }{
+ {
+ name: "github repo",
+ modulePath: "github.com/owner/repo",
+ want: server.URL + "/github.com/owner/repo",
+ },
+ {
+ name: "github repo subpackage",
+ modulePath: "github.com/owner/repo",
+ want: server.URL + "/github.com/owner/repo",
+ },
+ {
+ name: "github repo not found",
+ modulePath: "github.com/owner/repo-not-found",
+ want: "",
+ },
+ {
+ name: "non-github repo",
+ modulePath: "example.com/owner/repo",
+ want: "",
+ },
+ {
+ name: "golang.org/x/ repo",
+ modulePath: "golang.org/x/glog",
+ want: server.URL + "/github.com/golang/glog",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ um := &internal.UnitMeta{ModuleInfo: internal.ModuleInfo{ModulePath: tc.modulePath}}
+ url := codeWikiURLGenerator(context.Background(), server.Client(), um)()
+ if url != tc.want {
+ t.Errorf("codeWikiURLGenerator(ctx, client, %q) = %q, want %q, got %q", tc.path, url, tc.want, url)
+ }
+ })
+ }
+}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 5f621fd..e15f929 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -60,7 +60,7 @@
vulnClient *vuln.Client
versionID string
instanceID string
- depsDevHTTPClient *http.Client
+ HTTPClient *http.Client
mu sync.Mutex // Protects all fields below
templates map[string]*template.Template
@@ -84,18 +84,18 @@
FetchServer FetchServerInterface
// DataSourceGetter should return a DataSource on each call.
// It should be goroutine-safe.
- DataSourceGetter func(context.Context) internal.DataSource
- Queue queue.Queue
- TemplateFS template.TrustedFS // for loading templates safely
- StaticFS fs.FS // for static/ directory
- ThirdPartyFS fs.FS // for third_party/ directory
- DevMode bool
- LocalMode bool
- GoDocMode bool
- LocalModules []LocalModule
- Reporter derrors.Reporter
- VulndbClient *vuln.Client
- DepsDevHTTPClient *http.Client
+ DataSourceGetter func(context.Context) internal.DataSource
+ Queue queue.Queue
+ TemplateFS template.TrustedFS // for loading templates safely
+ StaticFS fs.FS // for static/ directory
+ ThirdPartyFS fs.FS // for third_party/ directory
+ DevMode bool
+ LocalMode bool
+ GoDocMode bool
+ LocalModules []LocalModule
+ Reporter derrors.Reporter
+ VulndbClient *vuln.Client
+ HTTPClient *http.Client
}
// NewServer creates a new Server for the given database and template directory.
@@ -107,24 +107,24 @@
}
dochtml.LoadTemplates(scfg.TemplateFS)
s := &Server{
- fetchServer: scfg.FetchServer,
- getDataSource: scfg.DataSourceGetter,
- queue: scfg.Queue,
- templateFS: scfg.TemplateFS,
- staticFS: scfg.StaticFS,
- thirdPartyFS: scfg.ThirdPartyFS,
- devMode: scfg.DevMode,
- localMode: scfg.LocalMode,
- goDocMode: scfg.GoDocMode,
- localModules: scfg.LocalModules,
- templates: ts,
- reporter: scfg.Reporter,
- fileMux: http.NewServeMux(),
- vulnClient: scfg.VulndbClient,
- depsDevHTTPClient: scfg.DepsDevHTTPClient,
+ fetchServer: scfg.FetchServer,
+ getDataSource: scfg.DataSourceGetter,
+ queue: scfg.Queue,
+ templateFS: scfg.TemplateFS,
+ staticFS: scfg.StaticFS,
+ thirdPartyFS: scfg.ThirdPartyFS,
+ devMode: scfg.DevMode,
+ localMode: scfg.LocalMode,
+ goDocMode: scfg.GoDocMode,
+ localModules: scfg.LocalModules,
+ templates: ts,
+ reporter: scfg.Reporter,
+ fileMux: http.NewServeMux(),
+ vulnClient: scfg.VulndbClient,
+ HTTPClient: scfg.HTTPClient,
}
- if s.depsDevHTTPClient == nil {
- s.depsDevHTTPClient = http.DefaultClient
+ if s.HTTPClient == nil {
+ s.HTTPClient = http.DefaultClient
}
if scfg.Config != nil {
s.appVersionLabel = scfg.Config.AppVersionLabel()
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index 96aafa9..5547179 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -97,7 +97,7 @@
Details any
// GoDocMode indicates whether to suppress the unit header and right hand unit metadata.
- // If set to true, the page will also not have Vulns or a DepsDevURL.
+ // If set to true, the page will also not have Vulns, DepsDevURL or a CodeWikiURL.
GoDocMode bool
// Vulns holds vulnerability information.
@@ -106,6 +106,9 @@
// DepsDevURL holds the full URL to this module version on deps.dev.
DepsDevURL string
+ // CodeWikiURL holds the full URL to this module's repo on codewiki.google.
+ CodeWikiURL string
+
// IsGoProject is true if the package is from the standard library or a
// golang.org sub-repository.
IsGoProject bool
@@ -141,8 +144,10 @@
}
makeDepsDevURL := func() string { return "" }
+ makeCodeWikiURL := func() string { return "" }
if !s.goDocMode {
- makeDepsDevURL = depsDevURLGenerator(ctx, s.depsDevHTTPClient, um)
+ makeDepsDevURL = depsDevURLGenerator(ctx, s.HTTPClient, um)
+ makeCodeWikiURL = codeWikiURLGenerator(ctx, s.HTTPClient, um)
}
// Use GOOS and GOARCH query parameters to create a build context, which
@@ -239,6 +244,7 @@
if !s.goDocMode {
page.DepsDevURL = makeDepsDevURL()
+ page.CodeWikiURL = makeCodeWikiURL()
}
// Show the banner if there was no error getting the latest major version,
diff --git a/static/frontend/unit/main/_meta.tmpl b/static/frontend/unit/main/_meta.tmpl
index 649d0d8..8e32eb2 100644
--- a/static/frontend/unit/main/_meta.tmpl
+++ b/static/frontend/unit/main/_meta.tmpl
@@ -40,6 +40,16 @@
</a>
</li>
{{end}}
+ {{with .CodeWikiURL}}
+ <li>
+ <a href="{{.}}" title="View this repo on Code Wiki"
+ target="_blank" rel="noopener" data-test-id="meta-link-codewiki">
+ <img class="go-Icon" src="/static/shared/icon/codewiki-logo.svg"
+ alt="Code Wiki Logo" />
+ Code Wiki
+ </a>
+ </li>
+ {{end}}
{{template "unit-meta-links" .Details.ReadmeLinks}}
{{template "unit-meta-links" .Details.DocLinks}}
{{template "unit-meta-links" .Details.ModuleReadmeLinks}}
diff --git a/static/shared/icon/codewiki-logo.svg b/static/shared/icon/codewiki-logo.svg
new file mode 100644
index 0000000..28bd44a
--- /dev/null
+++ b/static/shared/icon/codewiki-logo.svg
@@ -0,0 +1 @@
+<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_737_3632)"><path d="M13.619.458a6.41 6.41 0 014.762 0l12.947 5.18c.896.358.896 1.625 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.626 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.625 0 1.984L18.381 31.54a6.412 6.412 0 01-4.762 0L.672 26.36c-.896-.358-.896-1.625 0-1.983l7.746-3.1a.64.64 0 000-1.188L.672 16.992c-.896-.358-.896-1.626 0-1.984l7.746-3.1a.64.64 0 000-1.188L.672 7.622c-.896-.359-.896-1.626 0-1.985L13.619.458z" fill="#3186FF"/><path d="M13.619.458a6.41 6.41 0 014.762 0l12.947 5.18c.896.358.896 1.625 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.626 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.625 0 1.984L18.381 31.54a6.412 6.412 0 01-4.762 0L.672 26.36c-.896-.358-.896-1.625 0-1.983l7.746-3.1a.64.64 0 000-1.188L.672 16.992c-.896-.358-.896-1.626 0-1.984l7.746-3.1a.64.64 0 000-1.188L.672 7.622c-.896-.359-.896-1.626 0-1.985L13.619.458z" fill="url(#paint0_linear_737_3632)"/><path d="M13.619.458a6.41 6.41 0 014.762 0l12.947 5.18c.896.358.896 1.625 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.626 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.625 0 1.984L18.381 31.54a6.412 6.412 0 01-4.762 0L.672 26.36c-.896-.358-.896-1.625 0-1.983l7.746-3.1a.64.64 0 000-1.188L.672 16.992c-.896-.358-.896-1.626 0-1.984l7.746-3.1a.64.64 0 000-1.188L.672 7.622c-.896-.359-.896-1.626 0-1.985L13.619.458z" fill="url(#paint1_linear_737_3632)"/><path d="M13.62.459a6.41 6.41 0 014.761 0l12.947 5.179c.896.358.896 1.626 0 1.984l-7.746 3.098a.64.64 0 000 1.189l7.746 3.099c.896.358.896 1.626 0 1.984l-7.746 3.099a.64.64 0 000 1.188l7.746 3.099c.896.358.896 1.626 0 1.984l-12.947 5.179a6.412 6.412 0 01-4.762 0L.672 26.362c-.896-.358-.896-1.626 0-1.984l7.746-3.099a.64.64 0 000-1.188L.672 16.992c-.896-.358-.896-1.626 0-1.984l7.746-3.099a.64.64 0 000-1.189L.672 7.622c-.896-.358-.896-1.626 0-1.984L13.619.458z" fill="url(#paint2_radial_737_3632)"/></g><defs><linearGradient id="paint0_linear_737_3632" x1="9.423" y1="29.688" x2="14.578" y2="18.488" gradientUnits="userSpaceOnUse"><stop offset=".206" stop-color="#0EBC5F"/><stop offset=".987" stop-color="#0EBC5F" stop-opacity="0"/></linearGradient><linearGradient id="paint1_linear_737_3632" x1="11.378" y1="4.977" x2="17.245" y2="15.999" gradientUnits="userSpaceOnUse"><stop stop-color="#FF4641"/><stop offset="1" stop-color="#FF4641" stop-opacity="0"/></linearGradient><radialGradient id="paint2_radial_737_3632" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.36 15.997) scale(50.0701)"><stop stop-color="#FC0"/><stop offset=".423" stop-color="#FC0" stop-opacity="0"/></radialGradient><clipPath id="clip0_737_3632"><path fill="#fff" d="M0 0h32v32H0z"/></clipPath></defs></svg>
\ No newline at end of file