internal/frontend: add fetchMainDetails

Logic for rendering the main unit page (which is not shared with other
tabs) is factored out into a fetchMainPage function and MainDetails
struct. This means that GetUnit and GetImportedBy are no longer
called unnecessarily when fetching data for non-main page tabs.

Data for MainDetails is now stored on UnitPage.Details, as is the case
with other tabs. A couple of fields are added/changed:

* LastCommitTime is renamed to CommitTime, since they mean the same thing
  and the latter is less verbose.

* IsPackage is added, which is used to determine whether a documentation
  section should be shown, even if the documentation is empty.

* NumImports is added, which is used to display the number of imports
  for given package.

UnitPage.Unit is changed to a UnitMeta type, since other fields on Unit
are no longer needed and UnitMeta is more explicit.

Change-Id: I7d5f4de867678c60d697fe9559416f3171f2d15c
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/261721
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/content/static/html/helpers/_unit_fixed_header.tmpl b/content/static/html/helpers/_unit_fixed_header.tmpl
index 7b5cac9..8d19a54 100644
--- a/content/static/html/helpers/_unit_fixed_header.tmpl
+++ b/content/static/html/helpers/_unit_fixed_header.tmpl
@@ -45,11 +45,11 @@
             </span>
             <span class="UnitHeaderFixed-detailItem UnitHeaderFixed-detailItem--md">
               <img height="16px" width="16px" src="/static/img/pkg-icon-circularArrows_16x16.svg" alt="">
-              {{.LastCommitTime}}
+              {{.Details.CommitTime}}
             </span>
             <span class="UnitHeaderFixed-detailItem UnitHeaderFixed-detailItem--md">
               <img height="16px" width="16px" src="/static/img/pkg-icon-scale_16x16.svg" alt="">
-              {{range $i, $e := .Licenses}}
+              {{range $i, $e := .Details.Licenses}}
                 {{if $i}}, {{end}}
                 <a href="{{$.URLPath}}?tab=licenses#{{.Anchor}}" tabindex="-1">{{$e.Type}}</a>
               {{else}}
@@ -63,13 +63,13 @@
               <span class="UnitHeaderFixed-detailItem UnitHeaderFixed-detailItem--lg">
                 <img height="16px" width="16px" src="/static/img/pkg-icon-boxClosed_16x16.svg" alt="">
                 <a href="{{$.URLPath}}?tab=imports" tabindex="-1">
-                  {{len .Unit.Imports}} <span>Imports</span>
+                  {{.Details.NumImports}} <span>Imports</span>
                 </a>
               </span>
               <span class="UnitHeaderFixed-detailItem UnitHeaderFixed-detailItem--lg">
                 <img height="16px" width="16px" src="/static/img/pkg-icon-boxClosed_16x16.svg" alt="">
                 <a href="{{$.URLPath}}?tab=importedby" tabindex="-1">
-                  {{.ImportedByCount}} <span>Imported by</span>
+                  {{.Details.ImportedByCount}} <span>Imported by</span>
                 </a>
               </span>
             {{end}}
diff --git a/content/static/html/helpers/_unit_header.tmpl b/content/static/html/helpers/_unit_header.tmpl
index 64f006b..cd09a81 100644
--- a/content/static/html/helpers/_unit_header.tmpl
+++ b/content/static/html/helpers/_unit_header.tmpl
@@ -57,13 +57,13 @@
           </span>
           <span class="UnitHeader-detailItem">
             <img height="16px" width="16px" src="/static/img/pkg-icon-circularArrows_16x16.svg" alt="">
-            {{.LastCommitTime}}
+            {{.Details.CommitTime}}
           </span>
           <span class="UnitHeader-detailItem">
             <img height="16px" width="16px" src="/static/img/pkg-icon-scale_16x16.svg" alt="">
-            {{if .Licenses}}
+            {{if .Details.Licenses}}
               <a href="{{$.URLPath}}?tab=licenses">
-                {{range $i, $e := .Licenses}}
+                {{range $i, $e := .Details.Licenses}}
                 {{if $i}}, {{end}} {{$e.Type}}
                 {{end}}
               </a>
@@ -76,13 +76,13 @@
             <span class="UnitHeader-detailItem">
               <img height="16px" width="16px" src="/static/img/pkg-icon-boxClosed_16x16.svg" alt="">
               <a href="{{$.URLPath}}?tab=imports">
-                {{len .Unit.Imports}} <span>Imports</span>
+                {{.Details.NumImports}} <span>Imports</span>
               </a>
             </span>
             <span class="UnitHeader-detailItem">
               <img height="16px" width="16px" src="/static/img/pkg-icon-boxClosed_16x16.svg" alt="">
               <a href="{{$.URLPath}}?tab=importedby">
-                {{.ImportedByCount}} <span>Imported by</span>
+                {{.Details.ImportedByCount}} <span>Imported by</span>
               </a>
             </span>
           {{end}}
diff --git a/content/static/html/helpers/_unit_outline.tmpl b/content/static/html/helpers/_unit_outline.tmpl
index 655ef11..aea5f85 100644
--- a/content/static/html/helpers/_unit_outline.tmpl
+++ b/content/static/html/helpers/_unit_outline.tmpl
@@ -19,7 +19,7 @@
       <div class="UnitOutline-panel js-accordionPanel"
           id="readme-panel" role="region" aria-labelledby="readme-accordion" aria-hidden="true"></div>
     {{end}}
-    {{if (or .DocOutline.String .Unit.IsPackage)}}
+    {{if .IsPackage}}
       <a class="UnitOutline-accordion  js-accordionTrigger" href="#section-documentation"
           role="button" aria-expanded="false" aria-controls="outline-panel" id="outline-accordion">
         Documentation
diff --git a/content/static/html/pages/unit_details.tmpl b/content/static/html/pages/unit_details.tmpl
index 10474cc..c38e607 100644
--- a/content/static/html/pages/unit_details.tmpl
+++ b/content/static/html/pages/unit_details.tmpl
@@ -10,25 +10,25 @@
 
 {{define "unit_content"}}
   <div class="UnitDetails">
-    {{.MobileOutline}}
+    {{.Details.MobileOutline}}
     <div class="UnitDetails-outline" role="navigation"
         aria-label="{{if eq .PageType "std"}}module
         {{else}}{{.PageType}}{{end}}details navigation">
-      {{block "unit_outline" .}}{{end}}
+      {{block "unit_outline" .Details}}{{end}}
     </div>
     <div class="UnitDetails-content" role="main">
       {{if .CanShowDetails}}
-        {{if .Readme.String}}
-          {{block "unit_readme" .}}{{end}}
+        {{if .Details.Readme.String}}
+          {{block "unit_readme" .Details}}{{end}}
         {{end}}
-        {{if (or .DocBody.String .Unit.IsPackage)}}
-          {{block "unit_doc" .}}{{end}}
+        {{if .Details.IsPackage}}
+          {{block "unit_doc" .Details}}{{end}}
         {{end}}
-        {{if .SourceFiles}}
-          {{block "unit_files" .}}{{end}}
+        {{if .Details.SourceFiles}}
+          {{block "unit_files" .Details}}{{end}}
         {{end}}
-        {{if (or .Subdirectories .NestedModules)}}
-          {{block "unit_directories" .}}{{end}}
+        {{if (or .Details.Subdirectories .Details.NestedModules)}}
+          {{block "unit_directories" .Details}}{{end}}
         {{end}}
       {{else}}
         <h2>“{{.UnitContentName}}” not displayed due to license restrictions.</h2>
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index bdc3aba..f776108 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -9,17 +9,14 @@
 	"net/http"
 	"path"
 	"sort"
-	"strconv"
 	"strings"
 
 	"github.com/google/safehtml"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/experiment"
-	"golang.org/x/pkgsite/internal/godoc"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/middleware"
-	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/stdlib"
 )
 
@@ -27,14 +24,7 @@
 type UnitPage struct {
 	basePage
 	// Unit is the unit for this page.
-	Unit *internal.Unit
-
-	// NestedModules are nested modules relative to the path for the unit.
-	NestedModules []*NestedModule
-
-	// Subdirectories are packages in subdirectories relative to the path for
-	// the unit.
-	Subdirectories []*Subdirectory
+	Unit *internal.UnitMeta
 
 	// Breadcrumb contains data used to render breadcrumb UI elements.
 	Breadcrumb breadcrumb
@@ -51,12 +41,6 @@
 	// the canonical url for that path would be /my.module@v1.5.2/pkg
 	CanonicalURLPath string
 
-	// Licenses contains license metadata used in the header.
-	Licenses []LicenseMetadata
-
-	// Elapsed time since this version was committed.
-	LastCommitTime string
-
 	// The version string formatted for display.
 	DisplayVersion string
 
@@ -81,12 +65,6 @@
 	// UnitContentName is the display name of the selected unit content template".
 	UnitContentName string
 
-	// Readme is the rendered readme HTML.
-	Readme safehtml.HTML
-
-	// ExpandReadme is holds the expandable readme state.
-	ExpandReadme bool
-
 	// Tabs contains data to render the varioius tabs on each details page.
 	Tabs []TabSettings
 
@@ -95,39 +73,6 @@
 
 	// Details contains data specific to the type of page being rendered.
 	Details interface{}
-
-	// ImportedByCount is the number of packages that import this path.
-	// When the count is > limit it will read as 'limit+'. This field
-	// is not supported when using a datasource proxy.
-	ImportedByCount string
-
-	DocBody       safehtml.HTML
-	DocOutline    safehtml.HTML
-	MobileOutline safehtml.HTML
-
-	// SourceFiles contains .go files for the package.
-	SourceFiles []*File
-}
-
-// File is a source file for a package.
-type File struct {
-	Name string
-	URL  string
-}
-
-// NestedModule is a nested module relative to the path of a given unit.
-// This content is used in the Directories section of the unit page.
-type NestedModule struct {
-	Suffix string // suffix after the unit path
-	URL    string
-}
-
-// Subdirectory is a package in a subdirectory relative to the path of a given
-// unit. This content is used in the Directories section of the unit page.
-type Subdirectory struct {
-	Suffix   string
-	URL      string
-	Synopsis string
 }
 
 var (
@@ -175,71 +120,6 @@
 func (s *Server) serveUnitPage(ctx context.Context, w http.ResponseWriter, r *http.Request,
 	ds internal.DataSource, um *internal.UnitMeta, requestedVersion string) (err error) {
 	defer derrors.Wrap(&err, "serveUnitPage(ctx, w, r, ds, %v, %q)", um, requestedVersion)
-	unit, err := ds.GetUnit(ctx, um, internal.AllFields)
-	if err != nil {
-		return err
-	}
-
-	// importedByCount is not supported when using a datasource proxy.
-	importedByCount := "0"
-	db, ok := ds.(*postgres.DB)
-	if ok {
-		importedBy, err := db.GetImportedBy(ctx, um.Path, um.ModulePath, importedByLimit)
-		if err != nil {
-			return err
-		}
-		// If we reached the query limit, then we don't know the total
-		// and we'll indicate that with a '+'. For example, if the limit
-		// is 101 and we get 101 results, then we'll show '100+ Imported by'.
-		importedByCount = strconv.Itoa(len(importedBy))
-		if len(importedBy) == importedByLimit {
-			importedByCount = strconv.Itoa(len(importedBy)-1) + "+"
-		}
-	}
-
-	nestedModules, err := getNestedModules(ctx, ds, um)
-	if err != nil {
-		return err
-	}
-	subdirectories := getSubdirectories(um, unit.Subdirectories)
-	if err != nil {
-		return err
-	}
-	readme, err := readmeContent(ctx, um, unit.Readme)
-	if err != nil {
-		return err
-	}
-
-	var (
-		docBody, docOutline, mobileOutline safehtml.HTML
-		files                              []*File
-	)
-	if unit.Documentation != nil {
-		docHTML := getHTML(ctx, unit)
-		// TODO: Deprecate godoc.Parse. The sidenav and body can
-		// either be rendered using separate functions, or all this content can
-		// be passed to the template via the UnitPage struct.
-		b, err := godoc.Parse(docHTML, godoc.BodySection)
-		if err != nil {
-			return err
-		}
-		docBody = b
-		o, err := godoc.Parse(docHTML, godoc.SidenavSection)
-		if err != nil {
-			return err
-		}
-		docOutline = o
-		m, err := godoc.Parse(docHTML, godoc.SidenavMobileSection)
-		if err != nil {
-			return err
-		}
-		mobileOutline = m
-
-		files, err = sourceFiles(unit)
-		if err != nil {
-			return err
-		}
-	}
 
 	tab := r.FormValue("tab")
 	if tab == "" {
@@ -257,16 +137,13 @@
 	basePage := s.newBasePage(r, title)
 	basePage.AllowWideContent = true
 	canShowDetails := um.IsRedistributable || tabSettings.AlwaysShowDetails
-	_, expandReadme := r.URL.Query()["readme"]
 	page := UnitPage{
-		basePage:       basePage,
-		Unit:           unit,
-		Subdirectories: subdirectories,
-		NestedModules:  nestedModules,
-		Breadcrumb:     displayBreadcrumb(um, requestedVersion),
-		Title:          title,
-		Tabs:           unitTabs,
-		SelectedTab:    tabSettings,
+		basePage:    basePage,
+		Unit:        um,
+		Breadcrumb:  displayBreadcrumb(um, requestedVersion),
+		Title:       title,
+		Tabs:        unitTabs,
+		SelectedTab: tabSettings,
 		URLPath: constructPackageURL(
 			um.Path,
 			um.ModulePath,
@@ -277,8 +154,6 @@
 			um.ModulePath,
 			linkVersion(um.Version, um.ModulePath),
 		),
-		Licenses:        transformLicenseMetadata(um.Licenses),
-		LastCommitTime:  elapsedTime(um.CommitTime),
 		DisplayVersion:  displayVersion(um.Version, um.ModulePath),
 		LinkVersion:     linkVersion(um.Version, um.ModulePath),
 		LatestURL:       constructPackageURL(um.Path, um.ModulePath, middleware.LatestMinorVersionPlaceholder),
@@ -286,23 +161,23 @@
 		PageType:        pageType(um),
 		CanShowDetails:  canShowDetails,
 		UnitContentName: tabSettings.DisplayName,
-		Readme:          readme,
-		ExpandReadme:    expandReadme,
-		DocOutline:      docOutline,
-		DocBody:         docBody,
-		SourceFiles:     files,
-		MobileOutline:   mobileOutline,
-		ImportedByCount: importedByCount,
 	}
-
-	if tab != tabDetails {
-		packageDetails, err := fetchDetailsForPackage(r, tab, ds, um)
+	if tab == tabDetails {
+		_, expandReadme := r.URL.Query()["readme"]
+		d, err := fetchMainDetails(ctx, ds, um)
 		if err != nil {
 			return err
 		}
-		page.Details = packageDetails
+		d.ExpandReadme = expandReadme
+		page.Details = d
 	}
-
+	if tab != tabDetails {
+		d, err := fetchDetailsForPackage(r, tab, ds, um)
+		if err != nil {
+			return err
+		}
+		page.Details = d
+	}
 	s.servePage(ctx, w, tabSettings.TemplateName, page)
 	return nil
 }
diff --git a/internal/frontend/unit_main.go b/internal/frontend/unit_main.go
new file mode 100644
index 0000000..0ac8364
--- /dev/null
+++ b/internal/frontend/unit_main.go
@@ -0,0 +1,157 @@
+// Copyright 2020 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"
+	"strconv"
+
+	"github.com/google/safehtml"
+	"golang.org/x/pkgsite/internal"
+	"golang.org/x/pkgsite/internal/godoc"
+	"golang.org/x/pkgsite/internal/postgres"
+)
+
+// MainDetails contains data needed to render the unit template.
+type MainDetails struct {
+	// NestedModules are nested modules relative to the path for the unit.
+	NestedModules []*NestedModule
+
+	// Subdirectories are packages in subdirectories relative to the path for
+	// the unit.
+	Subdirectories []*Subdirectory
+
+	// Licenses contains license metadata used in the header.
+	Licenses []LicenseMetadata
+
+	// NumImports is the number of imports for the package.
+	NumImports int
+
+	// CommitTime is time that this version was published, or the time that
+	// has elapsed since this version was committed if it was done so recently.
+	CommitTime string
+
+	// Readme is the rendered readme HTML.
+	Readme safehtml.HTML
+
+	// ImportedByCount is the number of packages that import this path.
+	// When the count is > limit it will read as 'limit+'. This field
+	// is not supported when using a datasource proxy.
+	ImportedByCount string
+
+	DocBody       safehtml.HTML
+	DocOutline    safehtml.HTML
+	MobileOutline safehtml.HTML
+	IsPackage     bool
+
+	// SourceFiles contains .go files for the package.
+	SourceFiles []*File
+
+	// ExpandReadme is holds the expandable readme state.
+	ExpandReadme bool
+}
+
+// File is a source file for a package.
+type File struct {
+	Name string
+	URL  string
+}
+
+// NestedModule is a nested module relative to the path of a given unit.
+// This content is used in the Directories section of the unit page.
+type NestedModule struct {
+	Suffix string // suffix after the unit path
+	URL    string
+}
+
+// Subdirectory is a package in a subdirectory relative to the path of a given
+// unit. This content is used in the Directories section of the unit page.
+type Subdirectory struct {
+	Suffix   string
+	URL      string
+	Synopsis string
+}
+
+func fetchMainDetails(ctx context.Context, ds internal.DataSource, um *internal.UnitMeta) (_ *MainDetails, err error) {
+	unit, err := ds.GetUnit(ctx, um, internal.AllFields)
+	if err != nil {
+		return nil, err
+	}
+
+	// importedByCount is not supported when using a datasource proxy.
+	importedByCount := "0"
+	db, ok := ds.(*postgres.DB)
+	if ok {
+		importedBy, err := db.GetImportedBy(ctx, um.Path, um.ModulePath, importedByLimit)
+		if err != nil {
+			return nil, err
+		}
+		// If we reached the query limit, then we don't know the total
+		// and we'll indicate that with a '+'. For example, if the limit
+		// is 101 and we get 101 results, then we'll show '100+ Imported by'.
+		importedByCount = strconv.Itoa(len(importedBy))
+		if len(importedBy) == importedByLimit {
+			importedByCount = strconv.Itoa(len(importedBy)-1) + "+"
+		}
+	}
+
+	nestedModules, err := getNestedModules(ctx, ds, um)
+	if err != nil {
+		return nil, err
+	}
+	subdirectories := getSubdirectories(um, unit.Subdirectories)
+	if err != nil {
+		return nil, err
+	}
+	readme, err := readmeContent(ctx, um, unit.Readme)
+	if err != nil {
+		return nil, err
+	}
+
+	var (
+		docBody, docOutline, mobileOutline safehtml.HTML
+		files                              []*File
+	)
+	if unit.Documentation != nil {
+		docHTML := getHTML(ctx, unit)
+		// TODO: Deprecate godoc.Parse. The sidenav and body can
+		// either be rendered using separate functions, or all this content can
+		// be passed to the template via the UnitPage struct.
+		b, err := godoc.Parse(docHTML, godoc.BodySection)
+		if err != nil {
+			return nil, err
+		}
+		docBody = b
+		o, err := godoc.Parse(docHTML, godoc.SidenavSection)
+		if err != nil {
+			return nil, err
+		}
+		docOutline = o
+		m, err := godoc.Parse(docHTML, godoc.SidenavMobileSection)
+		if err != nil {
+			return nil, err
+		}
+		mobileOutline = m
+
+		files, err = sourceFiles(unit)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return &MainDetails{
+		NestedModules:   nestedModules,
+		Subdirectories:  subdirectories,
+		Licenses:        transformLicenseMetadata(um.Licenses),
+		CommitTime:      elapsedTime(um.CommitTime),
+		Readme:          readme,
+		DocOutline:      docOutline,
+		DocBody:         docBody,
+		SourceFiles:     files,
+		MobileOutline:   mobileOutline,
+		NumImports:      len(unit.Imports),
+		ImportedByCount: importedByCount,
+		IsPackage:       unit.IsPackage(),
+	}, nil
+}