diff --git a/godoc/redirect/redirect.go b/godoc/redirect/redirect.go index 8208518c18..b4599f6a0f 100644 --- a/godoc/redirect/redirect.go +++ b/godoc/redirect/redirect.go @@ -8,12 +8,18 @@ package redirect // import "golang.org/x/tools/godoc/redirect" import ( + "context" "fmt" + "html/template" "net/http" "os" "regexp" "strconv" "strings" + "sync" + "time" + + "golang.org/x/net/context/ctxhttp" ) // Register registers HTTP handlers that redirect old godoc paths to their new @@ -193,11 +199,17 @@ func clHandler(w http.ResponseWriter, r *http.Request) { target := "" if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) { - // TODO: Issue 28836: if this Rietveld CL happens to + // 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. + // page with two links instead. We need to make a + // Gerrit API call to figure that out, but we cache + // known Gerrit CLs so it's done at most once per CL. + if ok, err := isGerritCL(r.Context(), n); err == nil && ok { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + clDisambiguationHTML.Execute(w, n) + return + } + target = "https://codereview.appspot.com/" + id } else { target = "https://go-review.googlesource.com/" + id @@ -205,6 +217,64 @@ func clHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target, http.StatusFound) } +var clDisambiguationHTML = template.Must(template.New("").Parse(` + + + Go CL {{.}} Disambiguation + + + + CL number {{.}} exists in both Gerrit (the current code review system) + and Rietveld (the previous code review system). Please make a choice: + + + +`)) + +// isGerritCL reports whether a Gerrit CL with the specified numeric change ID (e.g., "4247") +// is known to exist by querying the Gerrit API at https://go-review.googlesource.com. +// isGerritCL uses gerritCLCache as a cache of Gerrit CL IDs that exist. +func isGerritCL(ctx context.Context, id int) (bool, error) { + // Check cache first. + gerritCLCache.Lock() + ok := gerritCLCache.exist[id] + gerritCLCache.Unlock() + if ok { + return true, nil + } + + // Query the Gerrit API Get Change endpoint, as documented at + // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change. + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + resp, err := ctxhttp.Get(ctx, nil, fmt.Sprintf("https://go-review.googlesource.com/changes/%d", id)) + if err != nil { + return false, err + } + resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + // A Gerrit CL with this ID exists. Add it to cache. + gerritCLCache.Lock() + gerritCLCache.exist[id] = true + gerritCLCache.Unlock() + return true, nil + case http.StatusNotFound: + // A Gerrit CL with this ID doesn't exist. It may get created in the future. + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %v", resp.Status) + } +} + +var gerritCLCache = struct { + sync.Mutex + exist map[int]bool // exist is a set of Gerrit CL IDs that are known to exist. +}{exist: make(map[int]bool)} + var changeMap *hashMap // LoadChangeMap loads the specified map of Mercurial to Git revisions,