database: Fetch and index fork and stars for GitHub and BitBucket.

Store and show if a package is a fork, how many imports and stars
(followers in BitBucket) for each package in the search result list.
Currently we have over 90% packages coming from these two VCS's.
Corresponding UI change is made to show these additional information
under each package's import path.

Change-Id: I669755d4b905f360918d38e8600534a61a449ba4
Reviewed-on: https://go-review.googlesource.com/24173
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/database/database.go b/database/database.go
index 914ec97..2f952ac 100644
--- a/database/database.go
+++ b/database/database.go
@@ -60,8 +60,11 @@
 }
 
 type Package struct {
-	Path     string `json:"path"`
-	Synopsis string `json:"synopsis,omitempty"`
+	Path        string `json:"path"`
+	ImportCount int    `json:"import_count`
+	Synopsis    string `json:"synopsis,omitempty"`
+	Fork        bool   `json:"fork,omitempty"`
+	Stars       int    `json:"stars,omitempty"`
 }
 
 type byPath []Package
diff --git a/database/indexae.go b/database/indexae.go
index 8000761..877e6ea 100644
--- a/database/indexae.go
+++ b/database/indexae.go
@@ -27,6 +27,8 @@
 	Synopsis    string
 	Score       float64
 	ImportCount float64
+	Stars       float64
+	Fork        search.Atom
 }
 
 // PutIndex creates or updates a package entry in the search index. id identifies the document in the index.
@@ -55,9 +57,15 @@
 
 	// Update document information accordingly.
 	if pdoc != nil {
-		pkg.Name = search.Atom(pdoc.ProjectName)
+		pkg.Name = search.Atom(pdoc.Name)
 		pkg.Path = pdoc.ImportPath
 		pkg.Synopsis = pdoc.Synopsis
+		pkg.Stars = float64(pdoc.Stars)
+		var fork string
+		if forkAvailable(pdoc.ImportPath) {
+			fork = fmt.Sprint(pdoc.Fork) // "true" or "false"
+		}
+		pkg.Fork = search.Atom(fork)
 	}
 	if score >= 0 {
 		pkg.Score = score
@@ -70,6 +78,10 @@
 	return nil
 }
 
+func forkAvailable(p string) bool {
+	return strings.HasPrefix(p, "github.com") || strings.HasPrefix(p, "bitbucket.org")
+}
+
 // Search searches the packages index for a given query. A path-like query string
 // will be passed in unchanged, whereas single words will be stemmed.
 func Search(c context.Context, q string) ([]Package, error) {
@@ -86,15 +98,26 @@
 		},
 	}
 	for it := index.Search(c, parseQuery2(q), opt); ; {
-		var pkg PackageDocument
-		_, err := it.Next(&pkg)
+		var pd PackageDocument
+		_, err := it.Next(&pd)
 		if err == search.Done {
 			break
 		}
 		if err != nil {
 			return nil, err
 		}
-		pkgs = append(pkgs, Package{pkg.Path, pkg.Synopsis})
+		pkg := Package{
+			Path:        pd.Path,
+			ImportCount: int(pd.ImportCount),
+			Synopsis:    pd.Synopsis,
+		}
+		if pd.Fork == "true" {
+			pkg.Fork = true
+		}
+		if pd.Stars > 0 {
+			pkg.Stars = int(pd.Stars)
+		}
+		pkgs = append(pkgs, pkg)
 	}
 	return pkgs, nil
 }
diff --git a/doc/builder.go b/doc/builder.go
index 4b20837..a4614f0 100644
--- a/doc/builder.go
+++ b/doc/builder.go
@@ -400,6 +400,13 @@
 	// Version control: belongs to a dead end fork
 	DeadEndFork bool
 
+	// Whether the package is a fork of another one.
+	Fork bool
+
+	// How many stars (for a GitHub project) or followers (for a BitBucket
+	// project) the repository of this package has.
+	Stars int
+
 	// The time this object was created.
 	Updated time.Time
 
@@ -498,6 +505,8 @@
 		VCS:            dir.VCS,
 		DeadEndFork:    dir.DeadEndFork,
 		Subdirectories: dir.Subdirectories,
+		Fork:           dir.Fork,
+		Stars:          dir.Stars,
 	}
 
 	var b builder
diff --git a/gddo-server/assets/site.css b/gddo-server/assets/site.css
index cf11b78..52789c2 100644
--- a/gddo-server/assets/site.css
+++ b/gddo-server/assets/site.css
@@ -126,3 +126,14 @@
         font-size:16px;
     }
 }
+
+.synopsis {
+  opacity: 0.87;
+}
+
+.additional-info {
+    display: block;
+    opacity: 0.54;
+    text-transform: uppercase;
+    font-size: 0.75em;
+}
diff --git a/gddo-server/assets/templates/common.html b/gddo-server/assets/templates/common.html
index dc3d85b..2754b88 100644
--- a/gddo-server/assets/templates/common.html
+++ b/gddo-server/assets/templates/common.html
@@ -37,11 +37,30 @@
 </div>{{end}}
 
 {{define "Pkgs"}}
-    <table class="table table-condensed">
+  <table class="table table-condensed">
+  <thead><tr><th>Path</th><th>Synopsis</th></tr></thead>
+  <tbody>{{range .}}<tr><td>{{if .Path|isValidImportPath}}<a href="/{{.Path}}">{{.Path|importPath}}</a>{{else}}{{.Path|importPath}}{{end}}</td><td>{{.Synopsis|importPath}}</td></tr>
+  {{end}}</tbody>
+  </table>
+{{end}}
+
+{{define "SearchPkgs"}}
+  <table class="table table-condensed">
     <thead><tr><th>Path</th><th>Synopsis</th></tr></thead>
-    <tbody>{{range .}}<tr><td>{{if .Path|isValidImportPath}}<a href="/{{.Path}}">{{.Path|importPath}}</a>{{else}}{{.Path|importPath}}{{end}}</td><td>{{.Synopsis|importPath}}</td></tr>
+    <tbody>{{range .}}
+      <tr><td>
+        {{if .Path|isValidImportPath}}
+        <a href="/{{.Path}}">{{.Path|importPath}}</a>
+          <ul class="list-inline">
+            <li class="additional-info">{{.ImportCount}} imports</li>
+            {{if .Fork}}<li class="additional-info">· fork</li>{{end}}
+            {{if .Stars}}<li class="additional-info">· {{.Stars}} stars</li>{{end}}
+          </ul>
+        {{else}}{{.Path|importPath}}</td>
+        {{end}}
+      <td class="synopsis">{{.Synopsis|importPath}}</td></tr>
     {{end}}</tbody>
-    </table>
+  </table>
 {{end}}
 
 {{define "PkgCmdHeader"}}{{with .pdoc}}
diff --git a/gddo-server/assets/templates/results.html b/gddo-server/assets/templates/results.html
index 5160a9b..ad56976 100644
--- a/gddo-server/assets/templates/results.html
+++ b/gddo-server/assets/templates/results.html
@@ -4,10 +4,10 @@
   <div class="well">
     {{template "SearchBox" .q}}
   </div>
-  <p>Try this search on <a href="http://go-search.org/search?q={{.q}}">Go-Search</a> 
+  <p>Try this search on <a href="http://go-search.org/search?q={{.q}}">Go-Search</a>
   or <a href="https://github.com/search?q={{.q}}+language:go">GitHub</a>.
   {{if .pkgs}}
-    {{template "Pkgs" .pkgs}}
+    {{template "SearchPkgs" .pkgs}}
   {{else}}
     <p>No packages found.
   {{end}}
diff --git a/gosrc/bitbucket.go b/gosrc/bitbucket.go
index 437349c..3593cc5 100644
--- a/gosrc/bitbucket.go
+++ b/gosrc/bitbucket.go
@@ -30,6 +30,8 @@
 	ForkOf      struct {
 		Scm string
 	} `json:"fork_of"`
+	Followers int  `json:"followers"`
+	IsFork    bool `json:"is_fork"`
 }
 
 func getBitbucketDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
@@ -115,6 +117,8 @@
 		Subdirectories: contents.Directories,
 		VCS:            match["vcs"],
 		DeadEndFork:    isBitbucketDeadEndFork(repo),
+		Fork:           repo.IsFork,
+		Stars:          repo.Followers,
 	}, nil
 }
 
diff --git a/gosrc/github.go b/gosrc/github.go
index 5ae969f..e02fb32 100644
--- a/gosrc/github.go
+++ b/gosrc/github.go
@@ -162,6 +162,7 @@
 
 	var repo = struct {
 		Fork      bool      `json:"fork"`
+		Stars     int       `json:"stars"`
 		CreatedAt time.Time `json:"created_at"`
 		PushedAt  time.Time `json:"pushed_at"`
 	}{}
@@ -183,6 +184,8 @@
 		Subdirectories: subdirs,
 		VCS:            "git",
 		DeadEndFork:    isDeadEndFork,
+		Fork:           repo.Fork,
+		Stars:          repo.Stars,
 	}, nil
 }
 
diff --git a/gosrc/gosrc.go b/gosrc/gosrc.go
index 1505737..b30c360 100644
--- a/gosrc/gosrc.go
+++ b/gosrc/gosrc.go
@@ -70,6 +70,13 @@
 	// followed by one %d (source line number), or be empty string if not available.
 	// Example: "%s#L%d".
 	LineFmt string
+
+	// Whether the repository of this directory is a fork of another one.
+	Fork bool
+
+	// How many stars (for a GitHub project) or followers (for a BitBucket
+	// project) the repository of this directory has.
+	Stars int
 }
 
 // Project represents a repository.