Merge "jiri: fix typo and add example for "jiri test run -pkgs""
diff --git a/cl.go b/cl.go
index 8926d37..77e72b2 100644
--- a/cl.go
+++ b/cl.go
@@ -331,6 +331,31 @@
 #
 `
 
+// currentProject returns the Project containing the current working directory.
+// The current working directory must be inside JIRI_ROOT.
+func currentProject(jirix *jiri.X) (project.Project, error) {
+	dir, err := os.Getwd()
+	if err != nil {
+		return project.Project{}, fmt.Errorf("os.Getwd() failed: %v", err)
+	}
+
+	// Error if current working dir is not inside jirix.Root.
+	if !strings.HasPrefix(dir, jirix.Root) {
+		return project.Project{}, fmt.Errorf("'jiri cl mail' must be run from within a project in JIRI_ROOT")
+	}
+
+	// Walk up the path until we find a project at that path, or hit the jirix.Root.
+	for dir != jirix.Root {
+		p, err := project.ProjectAtPath(jirix, dir)
+		if err != nil {
+			dir = filepath.Dir(dir)
+			continue
+		}
+		return p, nil
+	}
+	return project.Project{}, fmt.Errorf("directory %q is not contained in a project", dir)
+}
+
 // runCLMail is a wrapper that sets up and runs a review instance.
 func runCLMail(jirix *jiri.X, _ []string) error {
 	// Sanity checks for the <presubmitFlag> flag.
@@ -341,14 +366,17 @@
 
 	host := hostFlag
 	if host == "" {
-		var err error
-		if host, err = project.GerritHost(jirix); err != nil {
+		p, err := currentProject(jirix)
+		if err != nil {
 			return err
 		}
+		if p.GerritHost == "" {
+			return fmt.Errorf("No gerrit host found.  Please use the '--host' flag, or add a 'gerrithost' attribute for project %q.", p.Name)
+		}
+		host = p.GerritHost
 	}
 
 	// Create and run the review.
-
 	review, err := newReview(jirix, gerrit.CLOpts{
 		Autosubmit:   autosubmitFlag,
 		Ccs:          parseEmails(ccsFlag),
diff --git a/googlesource/.api b/googlesource/.api
index 6101b39..067834e 100644
--- a/googlesource/.api
+++ b/googlesource/.api
@@ -1,5 +1,5 @@
 pkg googlesource, func GetRepoStatuses(*jiri.X, string) (RepoStatuses, error)
-pkg googlesource, func IsGoogleSourceHost(string) bool
+pkg googlesource, func IsGoogleSourceRemote(string) bool
 pkg googlesource, type RepoStatus struct
 pkg googlesource, type RepoStatus struct, Branches map[string]string
 pkg googlesource, type RepoStatus struct, CloneUrl string
diff --git a/googlesource/googlesource.go b/googlesource/googlesource.go
index 01547a9..0642db6 100644
--- a/googlesource/googlesource.go
+++ b/googlesource/googlesource.go
@@ -100,6 +100,17 @@
 
 // GetRepoStatuses returns the RepoStatus of all public projects hosted on the
 // remote host.  Host must be a googlesource host.
+//
+// NOTE(nlacasse): Googlesource uses gitiles as its git repo browser.  gitiles
+// has a completely undocumented feature that allows one to query the state of
+// all repositories in a single request.  See "doGetJson" method in
+// https://gerrit.googlesource.com/gitiles/+/master/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
+//
+// It's possible that gitiles will stop responding to this request at some
+// future version, or that googlesource will move away from gitiles entirely.
+// If that happens we can still get all the repo information in one request by
+// using the /projects/ endpoint on Gerrit.  See
+// https://review.typo3.org/Documentation/rest-api-projects.html#list-projects
 func GetRepoStatuses(jirix *jiri.X, host string) (RepoStatuses, error) {
 	u, err := url.Parse(host)
 	if err != nil {
@@ -109,6 +120,7 @@
 		return nil, fmt.Errorf("remote host scheme is not http(s): %s", host)
 	}
 
+	u.Path = "/"
 	q := u.Query()
 	q.Set("format", "json")
 	q.Set("b", "master")
@@ -141,9 +153,9 @@
 	return repoStatuses, nil
 }
 
-var googleSourceHostRegExp = regexp.MustCompile(`(?i)https?://.*\.googlesource.com/.*`)
+var googleSourceRemoteRegExp = regexp.MustCompile(`(?i)https?://.*\.googlesource.com.*`)
 
-// IsGoogleSourceHost returns true if the host url is a googlesource url.
-func IsGoogleSourceHost(host string) bool {
-	return googleSourceHostRegExp.MatchString(host)
+// IsGoogleSourceRemote returns true if the host url is a googlesource remote.
+func IsGoogleSourceRemote(host string) bool {
+	return googleSourceRemoteRegExp.MatchString(host)
 }
diff --git a/jiritest/.api b/jiritest/.api
index ebd8c36..5ba67b4 100644
--- a/jiritest/.api
+++ b/jiritest/.api
@@ -1,6 +1,5 @@
 pkg jiritest, func NewFakeJiriRoot(*testing.T) (*FakeJiriRoot, func())
 pkg jiritest, func NewX(*testing.T) (*jiri.X, func())
-pkg jiritest, method (FakeJiriRoot) AddHost(project.Host) error
 pkg jiritest, method (FakeJiriRoot) AddProject(project.Project) error
 pkg jiritest, method (FakeJiriRoot) AddTool(project.Tool) error
 pkg jiritest, method (FakeJiriRoot) CreateRemoteProject(string) error
diff --git a/jiritest/fake.go b/jiritest/fake.go
index c3e8195..517ec84 100644
--- a/jiritest/fake.go
+++ b/jiritest/fake.go
@@ -86,21 +86,6 @@
 		t.Fatal(err)
 	}
 
-	// Add "gerrit" and "git" hosts to the manifest, as required by the "jiri"
-	// tool.
-	if err := fake.AddHost(project.Host{
-		Name:     "gerrit",
-		Location: "git://example.com/gerrit",
-	}); err != nil {
-		t.Fatal(err)
-	}
-	if err := fake.AddHost(project.Host{
-		Name:     "git",
-		Location: "git://example.com/git",
-	}); err != nil {
-		t.Fatal(err)
-	}
-
 	// Update the contents of the fake JIRI_ROOT instance based on
 	// the information recorded in the remote manifest.
 	if err := fake.UpdateUniverse(false); err != nil {
@@ -120,19 +105,6 @@
 	}
 }
 
-// AddHost adds the given host to a remote manifest.
-func (fake FakeJiriRoot) AddHost(host project.Host) error {
-	manifest, err := fake.ReadRemoteManifest()
-	if err != nil {
-		return err
-	}
-	manifest.Hosts = append(manifest.Hosts, host)
-	if err := fake.WriteRemoteManifest(manifest); err != nil {
-		return err
-	}
-	return nil
-}
-
 // AddProject adds the given project to a remote manifest.
 func (fake FakeJiriRoot) AddProject(project project.Project) error {
 	manifest, err := fake.ReadRemoteManifest()
diff --git a/project/.api b/project/.api
index 5e8cc9a..ddf91f4 100644
--- a/project/.api
+++ b/project/.api
@@ -7,9 +7,7 @@
 pkg project, func CurrentManifest(*jiri.X) (*Manifest, error)
 pkg project, func CurrentProjectKey(*jiri.X) (ProjectKey, error)
 pkg project, func DataDirPath(*jiri.X, string) (string, error)
-pkg project, func GerritHost(*jiri.X) (string, error)
 pkg project, func GetProjectStates(*jiri.X, bool) (map[ProjectKey]*ProjectState, error)
-pkg project, func GitHost(*jiri.X) (string, error)
 pkg project, func InstallTools(*jiri.X, string) error
 pkg project, func LocalProjects(*jiri.X, ScanMode) (Projects, error)
 pkg project, func MakeProjectKey(string, string) ProjectKey
@@ -17,6 +15,7 @@
 pkg project, func ManifestFromFile(*jiri.X, string) (*Manifest, error)
 pkg project, func ParseNames(*jiri.X, []string, map[string]struct{}) (Projects, error)
 pkg project, func PollProjects(*jiri.X, map[string]struct{}) (Update, error)
+pkg project, func ProjectAtPath(*jiri.X, string) (Project, error)
 pkg project, func ProjectFromFile(*jiri.X, string) (*Project, error)
 pkg project, func ReadManifest(*jiri.X) (Projects, Tools, error)
 pkg project, func TransitionBinDir(*jiri.X) error
@@ -43,10 +42,6 @@
 pkg project, type FileImport struct
 pkg project, type FileImport struct, File string
 pkg project, type FileImport struct, XMLName struct{}
-pkg project, type GitHook struct
-pkg project, type GitHook struct, Name string
-pkg project, type GitHook struct, Path string
-pkg project, type GitHook struct, XMLName struct{}
 pkg project, type Hook struct
 pkg project, type Hook struct, Args []HookArg
 pkg project, type Hook struct, Interpreter string
@@ -58,12 +53,6 @@
 pkg project, type HookArg struct, Arg string
 pkg project, type HookArg struct, XMLName struct{}
 pkg project, type Hooks map[string]Hook
-pkg project, type Host struct
-pkg project, type Host struct, GitHooks []GitHook
-pkg project, type Host struct, Location string
-pkg project, type Host struct, Name string
-pkg project, type Host struct, XMLName struct{}
-pkg project, type Hosts map[string]Host
 pkg project, type Import struct
 pkg project, type Import struct, Manifest string
 pkg project, type Import struct, Root string
@@ -72,13 +61,14 @@
 pkg project, type Manifest struct
 pkg project, type Manifest struct, FileImports []FileImport
 pkg project, type Manifest struct, Hooks []Hook
-pkg project, type Manifest struct, Hosts []Host
 pkg project, type Manifest struct, Imports []Import
 pkg project, type Manifest struct, Label string
 pkg project, type Manifest struct, Projects []Project
 pkg project, type Manifest struct, Tools []Tool
 pkg project, type Manifest struct, XMLName struct{}
 pkg project, type Project struct
+pkg project, type Project struct, GerritHost string
+pkg project, type Project struct, GitHooks string
 pkg project, type Project struct, Name string
 pkg project, type Project struct, Path string
 pkg project, type Project struct, Protocol string
diff --git a/project/paths.go b/project/paths.go
index 2c0a2d9..7e81e78 100644
--- a/project/paths.go
+++ b/project/paths.go
@@ -18,7 +18,7 @@
 // breaks.  We should revisit the whole data directory thing, and in particular
 // see if we can get rid of tools having to know their own names.
 func DataDirPath(jirix *jiri.X, toolName string) (string, error) {
-	_, projects, tools, _, err := readManifest(jirix)
+	projects, tools, _, err := readManifest(jirix)
 	if err != nil {
 		return "", err
 	}
@@ -40,25 +40,3 @@
 	}
 	return filepath.Join(project.Path, tool.Data), nil
 }
-
-func getHost(jirix *jiri.X, name string) (string, error) {
-	hosts, _, _, _, err := readManifest(jirix)
-	if err != nil {
-		return "", err
-	}
-	host, found := hosts[name]
-	if !found {
-		return "", fmt.Errorf("host %s not found in manifest", name)
-	}
-	return host.Location, nil
-}
-
-// GerritHost returns the URL that hosts the Gerrit code review system.
-func GerritHost(jirix *jiri.X) (string, error) {
-	return getHost(jirix, "gerrit")
-}
-
-// GitHost returns the URL that hosts the git repositories.
-func GitHost(jirix *jiri.X) (string, error) {
-	return getHost(jirix, "git")
-}
diff --git a/project/project.go b/project/project.go
index a877633..85421c1 100644
--- a/project/project.go
+++ b/project/project.go
@@ -9,6 +9,7 @@
 	"encoding/xml"
 	"fmt"
 	"io/ioutil"
+	"net/url"
 	"os"
 	"path/filepath"
 	"sort"
@@ -41,7 +42,6 @@
 // Manifest represents a setting used for updating the universe.
 type Manifest struct {
 	Hooks       []Hook       `xml:"hooks>hook"`
-	Hosts       []Host       `xml:"hosts>host"`
 	Imports     []Import     `xml:"imports>import"`
 	FileImports []FileImport `xml:"imports>fileimport"`
 	Label       string       `xml:"label,attr,omitempty"`
@@ -80,16 +80,12 @@
 var (
 	newlineBytes       = []byte("\n")
 	emptyHooksBytes    = []byte("\n  <hooks></hooks>\n")
-	emptyGitHooksBytes = []byte("\n      <githooks></githooks>\n")
-	emptyHostsBytes    = []byte("\n  <hosts></hosts>\n")
 	emptyImportsBytes  = []byte("\n  <imports></imports>\n")
 	emptyProjectsBytes = []byte("\n  <projects></projects>\n")
 	emptyToolsBytes    = []byte("\n  <tools></tools>\n")
 
 	endElemBytes       = []byte("/>\n")
 	endHookBytes       = []byte("></hook>\n")
-	endGitHookBytes    = []byte("></githook>\n")
-	endHostBytes       = []byte("></host>\n")
 	endImportBytes     = []byte("></import>\n")
 	endFileImportBytes = []byte("></fileimport>\n")
 	endProjectBytes    = []byte("></project>\n")
@@ -106,7 +102,6 @@
 	x.Label = m.Label
 	// First make copies of all slices.
 	x.Hooks = append([]Hook(nil), m.Hooks...)
-	x.Hosts = append([]Host(nil), m.Hosts...)
 	x.Imports = append([]Import(nil), m.Imports...)
 	x.FileImports = append([]FileImport(nil), m.FileImports...)
 	x.Projects = append([]Project(nil), m.Projects...)
@@ -115,9 +110,6 @@
 	for index, hook := range x.Hooks {
 		x.Hooks[index].Args = append([]HookArg(nil), hook.Args...)
 	}
-	for index, host := range x.Hosts {
-		x.Hosts[index].GitHooks = append([]GitHook(nil), host.GitHooks...)
-	}
 	return x
 }
 
@@ -134,14 +126,10 @@
 	// It's hard (impossible?) to get xml.Marshal to elide some of the empty
 	// elements, or produce short empty elements, so we post-process the data.
 	data = bytes.Replace(data, emptyHooksBytes, newlineBytes, -1)
-	data = bytes.Replace(data, emptyGitHooksBytes, newlineBytes, -1)
-	data = bytes.Replace(data, emptyHostsBytes, newlineBytes, -1)
 	data = bytes.Replace(data, emptyImportsBytes, newlineBytes, -1)
 	data = bytes.Replace(data, emptyProjectsBytes, newlineBytes, -1)
 	data = bytes.Replace(data, emptyToolsBytes, newlineBytes, -1)
 	data = bytes.Replace(data, endHookBytes, endElemBytes, -1)
-	data = bytes.Replace(data, endGitHookBytes, endElemBytes, -1)
-	data = bytes.Replace(data, endHostBytes, endElemBytes, -1)
 	data = bytes.Replace(data, endImportBytes, endElemBytes, -1)
 	data = bytes.Replace(data, endFileImportBytes, endElemBytes, -1)
 	data = bytes.Replace(data, endProjectBytes, endElemBytes, -1)
@@ -172,11 +160,6 @@
 }
 
 func (m *Manifest) fillDefaults() error {
-	for index := range m.Hosts {
-		if err := m.Hosts[index].validate(); err != nil {
-			return err
-		}
-	}
 	for index := range m.Imports {
 		if err := m.Imports[index].fillDefaults(); err != nil {
 			return err
@@ -201,11 +184,6 @@
 }
 
 func (m *Manifest) unfillDefaults() error {
-	for index := range m.Hosts {
-		if err := m.Hosts[index].validate(); err != nil {
-			return err
-		}
-	}
 	for index := range m.Imports {
 		if err := m.Imports[index].unfillDefaults(); err != nil {
 			return err
@@ -253,37 +231,6 @@
 	XMLName struct{} `xml:"arg"`
 }
 
-// Hosts map host name to their detailed description.
-type Hosts map[string]Host
-
-// Host represents the locations of git and gerrit repository hosts.
-type Host struct {
-	// Name is the host name.
-	Name string `xml:"name,attr,omitempty"`
-	// Location is the url of the host.
-	Location string `xml:"location,attr,omitempty"`
-	// Git hooks to apply to repos from this host.
-	GitHooks []GitHook `xml:"githooks>githook"`
-	XMLName  struct{}  `xml:"host"`
-}
-
-func (h *Host) validate() error {
-	if len(h.GitHooks) > 0 && h.Name != "git" {
-		return fmt.Errorf("bad host: githook provided for non-git host: %+v", *h)
-	}
-	return nil
-}
-
-// GitHook represents the name and source of git hooks.
-type GitHook struct {
-	// The hook name, as required by git (e.g. commit-msg, pre-rebase, etc.)
-	Name string `xml:"name,attr,omitempty"`
-	// The filename of the hook implementation.  When editing the manifest,
-	// specify this path as relative to the manifest dir.
-	Path    string   `xml:"path,attr,omitempty"`
-	XMLName struct{} `xml:"githook"`
-}
-
 // Import represents a remote manifest import.
 type Import struct {
 	// Manifest file to use from the remote manifest project.
@@ -427,7 +374,12 @@
 	// Revision is the revision the project should be advanced to
 	// during "jiri update". If not set, "HEAD" is used as the
 	// default.
-	Revision string   `xml:"revision,attr,omitempty"`
+	Revision string `xml:"revision,attr,omitempty"`
+	// GerritHost is the gerrit host where project CLs will be sent.
+	GerritHost string `xml:"gerrithost,attr,omitempty"`
+	// GitHooks is a directory containing git hooks that will be installed for
+	// this project.
+	GitHooks string   `xml:"githooks,attr,omitempty"`
 	XMLName  struct{} `xml:"project"`
 }
 
@@ -642,18 +594,15 @@
 		manifest.Projects = append(manifest.Projects, project)
 	}
 
-	// Add all hosts, tools, and hooks from the current manifest to the
+	// Add all tools and hooks from the current manifest to the
 	// snapshot manifest.
-	hosts, _, tools, hooks, err := readManifest(jirix)
+	_, tools, hooks, err := readManifest(jirix)
 	if err != nil {
 		return err
 	}
 	for _, tool := range tools {
 		manifest.Tools = append(manifest.Tools, tool)
 	}
-	for _, host := range hosts {
-		manifest.Hosts = append(manifest.Hosts, host)
-	}
 	for _, hook := range hooks {
 		manifest.Hooks = append(manifest.Hooks, hook)
 	}
@@ -797,14 +746,14 @@
 	if err != nil {
 		return nil, err
 	}
-	_, remoteProjects, _, _, err := readManifest(jirix)
+	remoteProjects, _, _, err := readManifest(jirix)
 	if err != nil {
 		return nil, err
 	}
 
 	// Compute difference between local and remote.
 	update := Update{}
-	ops := computeOperations(localProjects, remoteProjects, false, nil)
+	ops := computeOperations(localProjects, remoteProjects, false, "")
 	s := jirix.NewSeq()
 	for _, op := range ops {
 		name := op.Project().Name
@@ -861,7 +810,7 @@
 // ReadManifest retrieves and parses the manifest that determines what
 // projects and tools are part of the jiri universe.
 func ReadManifest(jirix *jiri.X) (Projects, Tools, error) {
-	_, p, t, _, e := readManifest(jirix)
+	p, t, _, e := readManifest(jirix)
 	return p, t, e
 }
 
@@ -879,19 +828,19 @@
 		}, "get manifest origin").Done()
 }
 
-func readManifest(jirix *jiri.X) (Hosts, Projects, Tools, Hooks, error) {
+func readManifest(jirix *jiri.X) (Projects, Tools, Hooks, error) {
 	jirix.TimerPush("read manifest")
 	defer jirix.TimerPop()
 	file, err := jirix.ResolveManifestPath(jirix.Manifest())
 	if err != nil {
-		return nil, nil, nil, nil, err
+		return nil, nil, nil, err
 	}
 	var imp importer
-	hosts, projects, tools, hooks := Hosts{}, Projects{}, Tools{}, Hooks{}
-	if err := imp.Load(jirix, jirix.Root, file, "", hosts, projects, tools, hooks); err != nil {
-		return nil, nil, nil, nil, err
+	projects, tools, hooks := Projects{}, Tools{}, Hooks{}
+	if err := imp.Load(jirix, jirix.Root, file, "", projects, tools, hooks); err != nil {
+		return nil, nil, nil, err
 	}
-	return hosts, projects, tools, hooks, nil
+	return projects, tools, hooks, nil
 }
 
 func updateManifestProjects(jirix *jiri.X) error {
@@ -944,13 +893,13 @@
 	if err := updateManifestProjects(jirix); err != nil {
 		return err
 	}
-	remoteHosts, remoteProjects, remoteTools, remoteHooks, err := readManifest(jirix)
+	remoteProjects, remoteTools, remoteHooks, err := readManifest(jirix)
 	if err != nil {
 		return err
 	}
 	s := jirix.NewSeq()
 	// 1. Update all local projects to match their remote counterparts.
-	if err := updateProjects(jirix, remoteProjects, gc, remoteHosts); err != nil {
+	if err := updateProjects(jirix, remoteProjects, gc); err != nil {
 		return err
 	}
 	// 2. Build all tools in a temporary directory.
@@ -1214,9 +1163,9 @@
 	return true, nil
 }
 
-// projectAtPath returns a Project struct corresponding to the project at the
+// ProjectAtPath returns a Project struct corresponding to the project at the
 // path in the filesystem.
-func projectAtPath(jirix *jiri.X, path string) (Project, error) {
+func ProjectAtPath(jirix *jiri.X, path string) (Project, error) {
 	metadataFile := filepath.Join(path, jiri.ProjectMetaDir, jiri.ProjectMetaFile)
 	project, err := ProjectFromFile(jirix, metadataFile)
 	if err != nil {
@@ -1234,7 +1183,7 @@
 		return err
 	}
 	if isLocal {
-		project, err := projectAtPath(jirix, path)
+		project, err := ProjectAtPath(jirix, path)
 		if err != nil {
 			return err
 		}
@@ -1465,13 +1414,13 @@
 	return nil
 }
 
-func (imp *importer) Load(jirix *jiri.X, root, file, key string, hosts Hosts, projects Projects, tools Tools, hooks Hooks) error {
+func (imp *importer) Load(jirix *jiri.X, root, file, key string, projects Projects, tools Tools, hooks Hooks) error {
 	return imp.importNoCycles(file, key, func() error {
-		return imp.load(jirix, root, file, hosts, projects, tools, hooks)
+		return imp.load(jirix, root, file, projects, tools, hooks)
 	})
 }
 
-func (imp *importer) load(jirix *jiri.X, root, file string, hosts Hosts, projects Projects, tools Tools, hooks Hooks) error {
+func (imp *importer) load(jirix *jiri.X, root, file string, projects Projects, tools Tools, hooks Hooks) error {
 	m, err := ManifestFromFile(jirix, file)
 	if err != nil {
 		return err
@@ -1491,14 +1440,14 @@
 				return err
 			}
 		}
-		if err := imp.Load(jirix, newRoot, newFile, _import.remoteKey(), hosts, projects, tools, hooks); err != nil {
+		if err := imp.Load(jirix, newRoot, newFile, _import.remoteKey(), projects, tools, hooks); err != nil {
 			return err
 		}
 	}
 	// Process all file imports.
 	for _, fileImport := range m.FileImports {
 		newFile := filepath.Join(filepath.Dir(file), fileImport.File)
-		if err := imp.Load(jirix, root, newFile, "", hosts, projects, tools, hooks); err != nil {
+		if err := imp.Load(jirix, root, newFile, "", projects, tools, hooks); err != nil {
 			return err
 		}
 	}
@@ -1520,10 +1469,6 @@
 		hook.Path = filepath.Join(project.Path, hook.Path)
 		hooks[hook.Name] = hook
 	}
-	// Process all hosts.
-	for _, host := range m.Hosts {
-		hosts[host.Name] = host
-	}
 	return nil
 }
 
@@ -1554,10 +1499,7 @@
 			localProject = &p
 		}
 		// Since &remote.Project is never nil, we'll never produce a delete op.
-		//
-		// TODO(toddw): How do we retrieve the hosts, which are necessary for
-		// githooks to be installed during the create operation?
-		op := computeOp(localProject, &remote.Project, false, nil)
+		op := computeOp(localProject, &remote.Project, false, newRoot)
 		if err := op.Test(jirix, newFsUpdates()); err != nil {
 			return err
 		}
@@ -1624,6 +1566,24 @@
 	}
 }
 
+// collectGoogleSourceHosts returns a slice of googlesource hosts for the given
+// projects.  Each host will appear once in the slice.
+func collectGoogleSourceHosts(ps Projects) []string {
+	hostsMap := map[string]bool{}
+	for _, p := range ps {
+		if !googlesource.IsGoogleSourceRemote(p.Remote) {
+			continue
+		}
+		u, err := url.Parse(p.Remote)
+		if err != nil {
+			continue
+		}
+		host := u.Scheme + "://" + u.Host
+		hostsMap[host] = true
+	}
+	return set.StringBool.ToSlice(hostsMap)
+}
+
 // getRemoteHeadRevisions attempts to get the repo statuses from remote for HEAD
 // projects so we can detect when a local project is already up-to-date.
 func getRemoteHeadRevisions(jirix *jiri.X, remoteProjects Projects) {
@@ -1637,21 +1597,24 @@
 	if !someAtHead {
 		return
 	}
-	gitHost, gitHostErr := GitHost(jirix)
-	if gitHostErr != nil || !googlesource.IsGoogleSourceHost(gitHost) {
-		return
-	}
-	repoStatuses, err := googlesource.GetRepoStatuses(jirix, gitHost)
-	if err != nil {
-		// Log the error but don't fail.
-		fmt.Fprintf(jirix.Stderr(), "Error fetching repo statuses from remote: %v\n", err)
-		return
+	gsHosts := collectGoogleSourceHosts(remoteProjects)
+	allRepoStatuses := googlesource.RepoStatuses{}
+	for _, host := range gsHosts {
+		repoStatuses, err := googlesource.GetRepoStatuses(jirix, host)
+		if err != nil {
+			// Log the error but don't fail.
+			fmt.Fprintf(jirix.Stderr(), "Error fetching repo statuses from remote: %v\n", err)
+			continue
+		}
+		for repo, status := range repoStatuses {
+			allRepoStatuses[repo] = status
+		}
 	}
 	for name, rp := range remoteProjects {
 		if rp.Revision != "HEAD" {
 			continue
 		}
-		status, ok := repoStatuses[rp.Name]
+		status, ok := allRepoStatuses[rp.Name]
 		if !ok {
 			continue
 		}
@@ -1664,7 +1627,7 @@
 	}
 }
 
-func updateProjects(jirix *jiri.X, remoteProjects Projects, gc bool, hosts Hosts) error {
+func updateProjects(jirix *jiri.X, remoteProjects Projects, gc bool) error {
 	jirix.TimerPush("update projects")
 	defer jirix.TimerPop()
 
@@ -1677,7 +1640,7 @@
 		return err
 	}
 	getRemoteHeadRevisions(jirix, remoteProjects)
-	ops := computeOperations(localProjects, remoteProjects, gc, hosts)
+	ops := computeOperations(localProjects, remoteProjects, gc, "")
 	updates := newFsUpdates()
 	for _, op := range ops {
 		if err := op.Test(jirix, updates); err != nil {
@@ -1820,7 +1783,7 @@
 // createOperation represents the creation of a project.
 type createOperation struct {
 	commonOperation
-	hosts Hosts
+	root string
 }
 
 func (op createOperation) Run(jirix *jiri.X, manifest *Manifest) (e error) {
@@ -1847,24 +1810,32 @@
 		// overriding existing hooks.  Customizing your git hooks with jiri is a bad
 		// idea anyway, since jiri won't know to not delete the project when you
 		// switch between manifests or do a cleanup.
-		host, found := op.hosts["git"]
-		if found && strings.HasPrefix(op.project.Remote, host.Location) {
-			gitHookDir := filepath.Join(tmpDir, ".git", "hooks")
-			for _, githook := range host.GitHooks {
-				// TODO(nlacasse): GitHook paths are relative to the manifest
-				// file.  Currently all manifests live in
-				// JIRI_ROOT/.manifest/v2, but that is changing.  I think
-				// GitHooks should be associated with projects, and their paths
-				// should be relative to the project root.
-				mdir := filepath.Join(jirix.Root, ".manifest", "v2")
-				src, err := s.ReadFile(filepath.Join(mdir, githook.Path))
+		gitHooksDstDir := filepath.Join(tmpDir, ".git", "hooks")
+		if op.project.GitHooks != "" {
+			gitHooksSrcDir := filepath.Join(jirix.Root, op.root, op.project.GitHooks)
+			// Copy the specified GitHooks directory into the project's git
+			// hook directory.  We walk the file system, creating directories
+			// and copying files as we encounter them.
+			copyFn := func(path string, info os.FileInfo, err error) error {
 				if err != nil {
 					return err
 				}
-				dst := filepath.Join(gitHookDir, githook.Name)
-				if err := s.WriteFile(dst, src, perm).Done(); err != nil {
+				relPath, err := filepath.Rel(gitHooksSrcDir, path)
+				if err != nil {
 					return err
 				}
+				dst := filepath.Join(gitHooksDstDir, relPath)
+				if info.IsDir() {
+					return s.MkdirAll(dst, perm).Done()
+				}
+				src, err := s.ReadFile(path)
+				if err != nil {
+					return err
+				}
+				return s.WriteFile(dst, src, perm).Done()
+			}
+			if err := filepath.Walk(gitHooksSrcDir, copyFn); err != nil {
+				return err
 			}
 		}
 
@@ -2125,7 +2096,7 @@
 // system and manifest file respectively) and outputs a collection of
 // operations that describe the actions needed to update the target
 // projects.
-func computeOperations(localProjects, remoteProjects Projects, gc bool, hosts Hosts) operations {
+func computeOperations(localProjects, remoteProjects Projects, gc bool, root string) operations {
 	result := operations{}
 	allProjects := map[ProjectKey]bool{}
 	for _, p := range localProjects {
@@ -2142,13 +2113,13 @@
 		if project, ok := remoteProjects[key]; ok {
 			remote = &project
 		}
-		result = append(result, computeOp(local, remote, gc, hosts))
+		result = append(result, computeOp(local, remote, gc, root))
 	}
 	sort.Sort(result)
 	return result
 }
 
-func computeOp(local, remote *Project, gc bool, hosts Hosts) operation {
+func computeOp(local, remote *Project, gc bool, root string) operation {
 	switch {
 	case local != nil && remote != nil:
 		if local.Path != remote.Path {
@@ -2183,7 +2154,7 @@
 			destination: remote.Path,
 			project:     *remote,
 			source:      "",
-		}, hosts}
+		}, root}
 	default:
 		panic("jiri: computeOp called with nil local and remote")
 	}
diff --git a/project/project_test.go b/project/project_test.go
index 6981995..346a37d 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -118,16 +118,6 @@
 		}
 		manifest.Projects = append(manifest.Projects, project)
 	}
-	manifest.Hosts = []project.Host{
-		{
-			Name:     "gerrit",
-			Location: "git://example.com/gerrit",
-		},
-		{
-			Name:     "git",
-			Location: "git://example.com/git",
-		},
-	}
 	commitManifest(t, jirix, &manifest, dir)
 }
 
@@ -932,14 +922,6 @@
 						},
 					},
 				},
-				Hosts: []project.Host{
-					{
-						Name: "git",
-						GitHooks: []project.GitHook{
-							{Name: "githook"},
-						},
-					},
-				},
 				Imports: []project.Import{
 					{
 						Manifest: "manifest",
@@ -958,6 +940,8 @@
 				},
 				Projects: []project.Project{
 					{
+						GerritHost:   "https://test-review.googlesource.com",
+						GitHooks:     "path/to/githooks",
 						Name:         "project",
 						Path:         "path",
 						Protocol:     "git",
@@ -982,20 +966,13 @@
       <arg>bar</arg>
     </hook>
   </hooks>
-  <hosts>
-    <host name="git">
-      <githooks>
-        <githook name="githook"/>
-      </githooks>
-    </host>
-  </hosts>
   <imports>
     <import manifest="manifest" remote="remote"/>
     <import name="localimport"/>
     <fileimport file="fileimport"/>
   </imports>
   <projects>
-    <project name="project" path="path" remote="remote" remotebranch="otherbranch" revision="rev"/>
+    <project name="project" path="path" remote="remote" remotebranch="otherbranch" revision="rev" gerrithost="https://test-review.googlesource.com" githooks="path/to/githooks"/>
   </projects>
   <tools>
     <tool data="tooldata" name="tool" project="toolproject"/>